hexdag 0.5.0.dev1__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 (261) hide show
  1. hexdag/__init__.py +116 -0
  2. hexdag/__main__.py +30 -0
  3. hexdag/adapters/executors/__init__.py +5 -0
  4. hexdag/adapters/executors/local_executor.py +316 -0
  5. hexdag/builtin/__init__.py +6 -0
  6. hexdag/builtin/adapters/__init__.py +51 -0
  7. hexdag/builtin/adapters/anthropic/__init__.py +5 -0
  8. hexdag/builtin/adapters/anthropic/anthropic_adapter.py +151 -0
  9. hexdag/builtin/adapters/database/__init__.py +6 -0
  10. hexdag/builtin/adapters/database/csv/csv_adapter.py +249 -0
  11. hexdag/builtin/adapters/database/pgvector/__init__.py +5 -0
  12. hexdag/builtin/adapters/database/pgvector/pgvector_adapter.py +478 -0
  13. hexdag/builtin/adapters/database/sqlalchemy/sqlalchemy_adapter.py +252 -0
  14. hexdag/builtin/adapters/database/sqlite/__init__.py +5 -0
  15. hexdag/builtin/adapters/database/sqlite/sqlite_adapter.py +410 -0
  16. hexdag/builtin/adapters/local/README.md +59 -0
  17. hexdag/builtin/adapters/local/__init__.py +7 -0
  18. hexdag/builtin/adapters/local/local_observer_manager.py +696 -0
  19. hexdag/builtin/adapters/memory/__init__.py +47 -0
  20. hexdag/builtin/adapters/memory/file_memory_adapter.py +297 -0
  21. hexdag/builtin/adapters/memory/in_memory_memory.py +216 -0
  22. hexdag/builtin/adapters/memory/schemas.py +57 -0
  23. hexdag/builtin/adapters/memory/session_memory.py +178 -0
  24. hexdag/builtin/adapters/memory/sqlite_memory_adapter.py +215 -0
  25. hexdag/builtin/adapters/memory/state_memory.py +280 -0
  26. hexdag/builtin/adapters/mock/README.md +89 -0
  27. hexdag/builtin/adapters/mock/__init__.py +15 -0
  28. hexdag/builtin/adapters/mock/hexdag.toml +50 -0
  29. hexdag/builtin/adapters/mock/mock_database.py +225 -0
  30. hexdag/builtin/adapters/mock/mock_embedding.py +223 -0
  31. hexdag/builtin/adapters/mock/mock_llm.py +177 -0
  32. hexdag/builtin/adapters/mock/mock_tool_adapter.py +192 -0
  33. hexdag/builtin/adapters/mock/mock_tool_router.py +232 -0
  34. hexdag/builtin/adapters/openai/__init__.py +5 -0
  35. hexdag/builtin/adapters/openai/openai_adapter.py +634 -0
  36. hexdag/builtin/adapters/secret/__init__.py +7 -0
  37. hexdag/builtin/adapters/secret/local_secret_adapter.py +248 -0
  38. hexdag/builtin/adapters/unified_tool_router.py +280 -0
  39. hexdag/builtin/macros/__init__.py +17 -0
  40. hexdag/builtin/macros/conversation_agent.py +390 -0
  41. hexdag/builtin/macros/llm_macro.py +151 -0
  42. hexdag/builtin/macros/reasoning_agent.py +423 -0
  43. hexdag/builtin/macros/tool_macro.py +380 -0
  44. hexdag/builtin/nodes/__init__.py +38 -0
  45. hexdag/builtin/nodes/_discovery.py +123 -0
  46. hexdag/builtin/nodes/agent_node.py +696 -0
  47. hexdag/builtin/nodes/base_node_factory.py +242 -0
  48. hexdag/builtin/nodes/composite_node.py +926 -0
  49. hexdag/builtin/nodes/data_node.py +201 -0
  50. hexdag/builtin/nodes/expression_node.py +487 -0
  51. hexdag/builtin/nodes/function_node.py +454 -0
  52. hexdag/builtin/nodes/llm_node.py +491 -0
  53. hexdag/builtin/nodes/loop_node.py +920 -0
  54. hexdag/builtin/nodes/mapped_input.py +518 -0
  55. hexdag/builtin/nodes/port_call_node.py +269 -0
  56. hexdag/builtin/nodes/tool_call_node.py +195 -0
  57. hexdag/builtin/nodes/tool_utils.py +390 -0
  58. hexdag/builtin/prompts/__init__.py +68 -0
  59. hexdag/builtin/prompts/base.py +422 -0
  60. hexdag/builtin/prompts/chat_prompts.py +303 -0
  61. hexdag/builtin/prompts/error_correction_prompts.py +320 -0
  62. hexdag/builtin/prompts/tool_prompts.py +160 -0
  63. hexdag/builtin/tools/builtin_tools.py +84 -0
  64. hexdag/builtin/tools/database_tools.py +164 -0
  65. hexdag/cli/__init__.py +17 -0
  66. hexdag/cli/__main__.py +7 -0
  67. hexdag/cli/commands/__init__.py +27 -0
  68. hexdag/cli/commands/build_cmd.py +812 -0
  69. hexdag/cli/commands/create_cmd.py +208 -0
  70. hexdag/cli/commands/docs_cmd.py +293 -0
  71. hexdag/cli/commands/generate_types_cmd.py +252 -0
  72. hexdag/cli/commands/init_cmd.py +188 -0
  73. hexdag/cli/commands/pipeline_cmd.py +494 -0
  74. hexdag/cli/commands/plugin_dev_cmd.py +529 -0
  75. hexdag/cli/commands/plugins_cmd.py +441 -0
  76. hexdag/cli/commands/studio_cmd.py +101 -0
  77. hexdag/cli/commands/validate_cmd.py +221 -0
  78. hexdag/cli/main.py +84 -0
  79. hexdag/core/__init__.py +83 -0
  80. hexdag/core/config/__init__.py +20 -0
  81. hexdag/core/config/loader.py +479 -0
  82. hexdag/core/config/models.py +150 -0
  83. hexdag/core/configurable.py +294 -0
  84. hexdag/core/context/__init__.py +37 -0
  85. hexdag/core/context/execution_context.py +378 -0
  86. hexdag/core/docs/__init__.py +26 -0
  87. hexdag/core/docs/extractors.py +678 -0
  88. hexdag/core/docs/generators.py +890 -0
  89. hexdag/core/docs/models.py +120 -0
  90. hexdag/core/domain/__init__.py +10 -0
  91. hexdag/core/domain/dag.py +1225 -0
  92. hexdag/core/exceptions.py +234 -0
  93. hexdag/core/expression_parser.py +569 -0
  94. hexdag/core/logging.py +449 -0
  95. hexdag/core/models/__init__.py +17 -0
  96. hexdag/core/models/base.py +138 -0
  97. hexdag/core/orchestration/__init__.py +46 -0
  98. hexdag/core/orchestration/body_executor.py +481 -0
  99. hexdag/core/orchestration/components/__init__.py +97 -0
  100. hexdag/core/orchestration/components/adapter_lifecycle_manager.py +113 -0
  101. hexdag/core/orchestration/components/checkpoint_manager.py +134 -0
  102. hexdag/core/orchestration/components/execution_coordinator.py +360 -0
  103. hexdag/core/orchestration/components/health_check_manager.py +176 -0
  104. hexdag/core/orchestration/components/input_mapper.py +143 -0
  105. hexdag/core/orchestration/components/lifecycle_manager.py +583 -0
  106. hexdag/core/orchestration/components/node_executor.py +377 -0
  107. hexdag/core/orchestration/components/secret_manager.py +202 -0
  108. hexdag/core/orchestration/components/wave_executor.py +158 -0
  109. hexdag/core/orchestration/constants.py +17 -0
  110. hexdag/core/orchestration/events/README.md +312 -0
  111. hexdag/core/orchestration/events/__init__.py +104 -0
  112. hexdag/core/orchestration/events/batching.py +330 -0
  113. hexdag/core/orchestration/events/decorators.py +139 -0
  114. hexdag/core/orchestration/events/events.py +573 -0
  115. hexdag/core/orchestration/events/observers/__init__.py +30 -0
  116. hexdag/core/orchestration/events/observers/core_observers.py +690 -0
  117. hexdag/core/orchestration/events/observers/models.py +111 -0
  118. hexdag/core/orchestration/events/taxonomy.py +269 -0
  119. hexdag/core/orchestration/hook_context.py +237 -0
  120. hexdag/core/orchestration/hooks.py +437 -0
  121. hexdag/core/orchestration/models.py +418 -0
  122. hexdag/core/orchestration/orchestrator.py +910 -0
  123. hexdag/core/orchestration/orchestrator_factory.py +275 -0
  124. hexdag/core/orchestration/port_wrappers.py +327 -0
  125. hexdag/core/orchestration/prompt/__init__.py +32 -0
  126. hexdag/core/orchestration/prompt/template.py +332 -0
  127. hexdag/core/pipeline_builder/__init__.py +21 -0
  128. hexdag/core/pipeline_builder/component_instantiator.py +386 -0
  129. hexdag/core/pipeline_builder/include_tag.py +265 -0
  130. hexdag/core/pipeline_builder/pipeline_config.py +133 -0
  131. hexdag/core/pipeline_builder/py_tag.py +223 -0
  132. hexdag/core/pipeline_builder/tag_discovery.py +268 -0
  133. hexdag/core/pipeline_builder/yaml_builder.py +1196 -0
  134. hexdag/core/pipeline_builder/yaml_validator.py +569 -0
  135. hexdag/core/ports/__init__.py +65 -0
  136. hexdag/core/ports/api_call.py +133 -0
  137. hexdag/core/ports/database.py +489 -0
  138. hexdag/core/ports/embedding.py +215 -0
  139. hexdag/core/ports/executor.py +237 -0
  140. hexdag/core/ports/file_storage.py +117 -0
  141. hexdag/core/ports/healthcheck.py +87 -0
  142. hexdag/core/ports/llm.py +551 -0
  143. hexdag/core/ports/memory.py +70 -0
  144. hexdag/core/ports/observer_manager.py +130 -0
  145. hexdag/core/ports/secret.py +145 -0
  146. hexdag/core/ports/tool_router.py +94 -0
  147. hexdag/core/ports_builder.py +623 -0
  148. hexdag/core/protocols.py +273 -0
  149. hexdag/core/resolver.py +304 -0
  150. hexdag/core/schema/__init__.py +9 -0
  151. hexdag/core/schema/generator.py +742 -0
  152. hexdag/core/secrets.py +242 -0
  153. hexdag/core/types.py +413 -0
  154. hexdag/core/utils/async_warnings.py +206 -0
  155. hexdag/core/utils/schema_conversion.py +78 -0
  156. hexdag/core/utils/sql_validation.py +86 -0
  157. hexdag/core/validation/secure_json.py +148 -0
  158. hexdag/core/yaml_macro.py +517 -0
  159. hexdag/mcp_server.py +3120 -0
  160. hexdag/studio/__init__.py +10 -0
  161. hexdag/studio/build_ui.py +92 -0
  162. hexdag/studio/server/__init__.py +1 -0
  163. hexdag/studio/server/main.py +100 -0
  164. hexdag/studio/server/routes/__init__.py +9 -0
  165. hexdag/studio/server/routes/execute.py +208 -0
  166. hexdag/studio/server/routes/export.py +558 -0
  167. hexdag/studio/server/routes/files.py +207 -0
  168. hexdag/studio/server/routes/plugins.py +419 -0
  169. hexdag/studio/server/routes/validate.py +220 -0
  170. hexdag/studio/ui/index.html +13 -0
  171. hexdag/studio/ui/package-lock.json +2992 -0
  172. hexdag/studio/ui/package.json +31 -0
  173. hexdag/studio/ui/postcss.config.js +6 -0
  174. hexdag/studio/ui/public/hexdag.svg +5 -0
  175. hexdag/studio/ui/src/App.tsx +251 -0
  176. hexdag/studio/ui/src/components/Canvas.tsx +408 -0
  177. hexdag/studio/ui/src/components/ContextMenu.tsx +187 -0
  178. hexdag/studio/ui/src/components/FileBrowser.tsx +123 -0
  179. hexdag/studio/ui/src/components/Header.tsx +181 -0
  180. hexdag/studio/ui/src/components/HexdagNode.tsx +193 -0
  181. hexdag/studio/ui/src/components/NodeInspector.tsx +512 -0
  182. hexdag/studio/ui/src/components/NodePalette.tsx +262 -0
  183. hexdag/studio/ui/src/components/NodePortsSection.tsx +403 -0
  184. hexdag/studio/ui/src/components/PluginManager.tsx +347 -0
  185. hexdag/studio/ui/src/components/PortsEditor.tsx +481 -0
  186. hexdag/studio/ui/src/components/PythonEditor.tsx +195 -0
  187. hexdag/studio/ui/src/components/ValidationPanel.tsx +105 -0
  188. hexdag/studio/ui/src/components/YamlEditor.tsx +196 -0
  189. hexdag/studio/ui/src/components/index.ts +8 -0
  190. hexdag/studio/ui/src/index.css +92 -0
  191. hexdag/studio/ui/src/main.tsx +10 -0
  192. hexdag/studio/ui/src/types/index.ts +123 -0
  193. hexdag/studio/ui/src/vite-env.d.ts +1 -0
  194. hexdag/studio/ui/tailwind.config.js +29 -0
  195. hexdag/studio/ui/tsconfig.json +37 -0
  196. hexdag/studio/ui/tsconfig.node.json +13 -0
  197. hexdag/studio/ui/vite.config.ts +35 -0
  198. hexdag/visualization/__init__.py +69 -0
  199. hexdag/visualization/dag_visualizer.py +1020 -0
  200. hexdag-0.5.0.dev1.dist-info/METADATA +369 -0
  201. hexdag-0.5.0.dev1.dist-info/RECORD +261 -0
  202. hexdag-0.5.0.dev1.dist-info/WHEEL +4 -0
  203. hexdag-0.5.0.dev1.dist-info/entry_points.txt +4 -0
  204. hexdag-0.5.0.dev1.dist-info/licenses/LICENSE +190 -0
  205. hexdag_plugins/.gitignore +43 -0
  206. hexdag_plugins/README.md +73 -0
  207. hexdag_plugins/__init__.py +1 -0
  208. hexdag_plugins/azure/LICENSE +21 -0
  209. hexdag_plugins/azure/README.md +414 -0
  210. hexdag_plugins/azure/__init__.py +21 -0
  211. hexdag_plugins/azure/azure_blob_adapter.py +450 -0
  212. hexdag_plugins/azure/azure_cosmos_adapter.py +383 -0
  213. hexdag_plugins/azure/azure_keyvault_adapter.py +314 -0
  214. hexdag_plugins/azure/azure_openai_adapter.py +415 -0
  215. hexdag_plugins/azure/pyproject.toml +107 -0
  216. hexdag_plugins/azure/tests/__init__.py +1 -0
  217. hexdag_plugins/azure/tests/test_azure_blob_adapter.py +350 -0
  218. hexdag_plugins/azure/tests/test_azure_cosmos_adapter.py +323 -0
  219. hexdag_plugins/azure/tests/test_azure_keyvault_adapter.py +330 -0
  220. hexdag_plugins/azure/tests/test_azure_openai_adapter.py +329 -0
  221. hexdag_plugins/hexdag_etl/README.md +168 -0
  222. hexdag_plugins/hexdag_etl/__init__.py +53 -0
  223. hexdag_plugins/hexdag_etl/examples/01_simple_pandas_transform.py +270 -0
  224. hexdag_plugins/hexdag_etl/examples/02_simple_pandas_only.py +149 -0
  225. hexdag_plugins/hexdag_etl/examples/03_file_io_pipeline.py +109 -0
  226. hexdag_plugins/hexdag_etl/examples/test_pandas_transform.py +84 -0
  227. hexdag_plugins/hexdag_etl/hexdag.toml +25 -0
  228. hexdag_plugins/hexdag_etl/hexdag_etl/__init__.py +48 -0
  229. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/__init__.py +13 -0
  230. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/api_extract.py +230 -0
  231. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/base_node_factory.py +181 -0
  232. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/file_io.py +415 -0
  233. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/outlook.py +492 -0
  234. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/pandas_transform.py +563 -0
  235. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/sql_extract_load.py +112 -0
  236. hexdag_plugins/hexdag_etl/pyproject.toml +82 -0
  237. hexdag_plugins/hexdag_etl/test_transform.py +54 -0
  238. hexdag_plugins/hexdag_etl/tests/test_plugin_integration.py +62 -0
  239. hexdag_plugins/mysql_adapter/LICENSE +21 -0
  240. hexdag_plugins/mysql_adapter/README.md +224 -0
  241. hexdag_plugins/mysql_adapter/__init__.py +6 -0
  242. hexdag_plugins/mysql_adapter/mysql_adapter.py +408 -0
  243. hexdag_plugins/mysql_adapter/pyproject.toml +93 -0
  244. hexdag_plugins/mysql_adapter/tests/test_mysql_adapter.py +259 -0
  245. hexdag_plugins/storage/README.md +184 -0
  246. hexdag_plugins/storage/__init__.py +19 -0
  247. hexdag_plugins/storage/file/__init__.py +5 -0
  248. hexdag_plugins/storage/file/local.py +325 -0
  249. hexdag_plugins/storage/ports/__init__.py +5 -0
  250. hexdag_plugins/storage/ports/vector_store.py +236 -0
  251. hexdag_plugins/storage/sql/__init__.py +7 -0
  252. hexdag_plugins/storage/sql/base.py +187 -0
  253. hexdag_plugins/storage/sql/mysql.py +27 -0
  254. hexdag_plugins/storage/sql/postgresql.py +27 -0
  255. hexdag_plugins/storage/tests/__init__.py +1 -0
  256. hexdag_plugins/storage/tests/test_local_file_storage.py +161 -0
  257. hexdag_plugins/storage/tests/test_sql_adapters.py +212 -0
  258. hexdag_plugins/storage/vector/__init__.py +7 -0
  259. hexdag_plugins/storage/vector/chromadb.py +223 -0
  260. hexdag_plugins/storage/vector/in_memory.py +285 -0
  261. hexdag_plugins/storage/vector/pgvector.py +502 -0
@@ -0,0 +1,158 @@
1
+ """WaveExecutor - Handles parallel execution of nodes within waves.
2
+
3
+ Extracted from Orchestrator to provide a focused component for concurrent
4
+ wave-based execution with semaphore-based concurrency limiting.
5
+ """
6
+
7
+ import asyncio
8
+ import time
9
+ from typing import Any
10
+
11
+ from hexdag.core.domain.dag import DirectedGraph
12
+ from hexdag.core.orchestration.events import WaveCompleted, WaveStarted
13
+ from hexdag.core.orchestration.models import NodeExecutionContext
14
+
15
+
16
+ class WaveExecutor:
17
+ """Executes waves of nodes concurrently with resource management.
18
+
19
+ Responsibilities:
20
+ - Execute all waves in topological order
21
+ - Manage concurrency limits via semaphore
22
+ - Handle timeout for overall wave execution
23
+ - Emit wave-level events
24
+
25
+ Parameters
26
+ ----------
27
+ max_concurrent_nodes : int
28
+ Maximum number of nodes that can execute concurrently
29
+ semaphore : asyncio.Semaphore | None
30
+ Optional semaphore for concurrency control. If None, creates one.
31
+
32
+ Examples
33
+ --------
34
+ Example usage::
35
+
36
+ executor = WaveExecutor(max_concurrent_nodes=10)
37
+ cancelled = await executor.execute_all_waves(
38
+ waves=waves,
39
+ node_executor_fn=orchestrator._execute_node,
40
+ ...
41
+ )
42
+ """
43
+
44
+ def __init__(
45
+ self,
46
+ max_concurrent_nodes: int = 10,
47
+ semaphore: asyncio.Semaphore | None = None,
48
+ ):
49
+ self.max_concurrent_nodes = max_concurrent_nodes
50
+ self._semaphore = semaphore or asyncio.Semaphore(max_concurrent_nodes)
51
+
52
+ async def execute_all_waves(
53
+ self,
54
+ waves: list[list[str]],
55
+ node_executor_fn: Any, # Callable for executing a single node
56
+ graph: DirectedGraph,
57
+ node_results: dict[str, Any],
58
+ initial_input: Any,
59
+ context: NodeExecutionContext,
60
+ coordinator: Any, # ExecutionCoordinator
61
+ timeout: float | None,
62
+ validate: bool,
63
+ **kwargs: Any,
64
+ ) -> bool:
65
+ """Execute all waves with optional timeout."""
66
+ from hexdag.core.context import get_observer_manager
67
+
68
+ try:
69
+ async with asyncio.timeout(timeout):
70
+ for wave_idx, wave in enumerate(waves, 1):
71
+ wave_start_time = time.time()
72
+
73
+ wave_event = WaveStarted(wave_index=wave_idx, nodes=wave)
74
+ await coordinator.notify_observer(get_observer_manager(), wave_event)
75
+
76
+ wave_results = await self._execute_wave(
77
+ wave=wave,
78
+ node_executor_fn=node_executor_fn,
79
+ graph=graph,
80
+ node_results=node_results,
81
+ initial_input=initial_input,
82
+ context=context,
83
+ wave_index=wave_idx,
84
+ validate=validate,
85
+ **kwargs,
86
+ )
87
+ node_results.update(wave_results)
88
+
89
+ wave_completed = WaveCompleted(
90
+ wave_index=wave_idx,
91
+ duration_ms=(time.time() - wave_start_time) * 1000,
92
+ )
93
+ await coordinator.notify_observer(get_observer_manager(), wave_completed)
94
+
95
+ return False
96
+
97
+ except TimeoutError:
98
+ return True
99
+
100
+ async def _execute_wave(
101
+ self,
102
+ wave: list[str],
103
+ node_executor_fn: Any,
104
+ graph: DirectedGraph,
105
+ node_results: dict[str, Any],
106
+ initial_input: Any,
107
+ context: NodeExecutionContext,
108
+ wave_index: int,
109
+ validate: bool,
110
+ **kwargs: Any,
111
+ ) -> dict[str, Any]:
112
+ """Execute all nodes in a wave with concurrency limiting."""
113
+
114
+ async def execute_with_semaphore(node_name: str) -> tuple[str, Any]:
115
+ async with self._semaphore:
116
+ result = await node_executor_fn(
117
+ node_name=node_name,
118
+ graph=graph,
119
+ node_results=node_results,
120
+ initial_input=initial_input,
121
+ context=context,
122
+ wave_index=wave_index,
123
+ validate=validate,
124
+ **kwargs,
125
+ )
126
+ return node_name, result
127
+
128
+ wave_size = len(wave)
129
+ coros: list[Any] = [None] * wave_size
130
+ for i, node_name in enumerate(wave):
131
+ coros[i] = execute_with_semaphore(node_name)
132
+
133
+ wave_outputs = await asyncio.gather(*coros, return_exceptions=True)
134
+
135
+ wave_results = {}
136
+ exceptions: list[tuple[str | None, Exception]] | None = None
137
+
138
+ for output in wave_outputs:
139
+ if isinstance(output, Exception):
140
+ if exceptions is None:
141
+ exceptions = []
142
+ exceptions.append((None, output))
143
+ elif isinstance(output, BaseException):
144
+ raise output
145
+ else:
146
+ node_name, result = output
147
+ wave_results[node_name] = result
148
+
149
+ if exceptions:
150
+ if len(exceptions) == 1:
151
+ raise exceptions[0][1]
152
+ exception_list = [exc for _, exc in exceptions]
153
+ raise ExceptionGroup(
154
+ f"Multiple node failures in wave {wave_index} ({len(exceptions)} nodes failed)",
155
+ exception_list,
156
+ )
157
+
158
+ return wave_results
@@ -0,0 +1,17 @@
1
+ """Constants for orchestration execution.
2
+
3
+ This module defines shared constants used between orchestrator and executors
4
+ to avoid magic strings and ensure consistency.
5
+ """
6
+
7
+ # Executor context keys - used to store graph/results in ports context
8
+ # These are prefixed with _hexdag_ to avoid collisions with user ports
9
+ EXECUTOR_CONTEXT_GRAPH = "_hexdag_graph"
10
+ EXECUTOR_CONTEXT_NODE_RESULTS = "_hexdag_node_results"
11
+ EXECUTOR_CONTEXT_INITIAL_INPUT = "_hexdag_initial_input"
12
+
13
+ __all__ = [
14
+ "EXECUTOR_CONTEXT_GRAPH",
15
+ "EXECUTOR_CONTEXT_NODE_RESULTS",
16
+ "EXECUTOR_CONTEXT_INITIAL_INPUT",
17
+ ]
@@ -0,0 +1,312 @@
1
+ # 📊 Event System Architecture
2
+
3
+ > **A high-performance, production-ready event system for DAG orchestration with clean separation of observation and control.**
4
+
5
+ ## 🎯 Overview
6
+
7
+ The event system provides observability and control capabilities through a dual-manager architecture that separates **read-only observation** from **execution control**. Built on hexagonal architecture principles, it enables real-time monitoring and dynamic execution control without coupling.
8
+
9
+ ## 🏗️ Architecture
10
+
11
+ ```
12
+ ┌─────────────────────────────────────────────────────┐
13
+ │ Orchestrator │
14
+ │ │
15
+ │ ┌──────────────┐ ┌──────────────┐ │
16
+ │ │ Control │ │ Observer │ │
17
+ │ │ Manager │ │ Manager │ │
18
+ │ └──────────────┘ └──────────────┘ │
19
+ │ ▲ ▲ │
20
+ │ │ │ │
21
+ │ ControlResponse (fire & forget) │
22
+ │ │ │ │
23
+ └─────────┼─────────────────────────────┼─────────────┘
24
+ │ │
25
+ ┌─────▼──────┐ ┌──────▼──────┐
26
+ │ Control │ │ Observer │
27
+ │ Handlers │ │ Functions │
28
+ └────────────┘ └─────────────┘
29
+ ```
30
+
31
+ ### Dual-Manager System
32
+
33
+ #### **ObserverManager** - Fire-and-Forget Telemetry
34
+ *"Tell me what happened, but don't interfere"*
35
+
36
+ - **Purpose**: Logging, metrics, monitoring, alerts, audit trails
37
+ - **Behavior**: Async fire-and-forget with concurrent execution
38
+ - **Fault Isolation**: Observer failures never crash the pipeline
39
+ - **Event Filtering**: Subscribe only to relevant event types
40
+ - **Performance**: ThreadPoolExecutor for sync functions, semaphore-based concurrency
41
+
42
+ #### **ControlManager** - Policy Enforcement
43
+ *"Check if this should proceed and how"*
44
+
45
+ - **Purpose**: Retry policies, circuit breakers, rate limiting, authorization
46
+ - **Behavior**: Priority-based execution with veto pattern
47
+ - **Control Signals**: PROCEED, SKIP, RETRY, FALLBACK, FAIL, ERROR
48
+ - **Event Filtering**: Process only relevant events for ~90% efficiency gain
49
+ - **Performance**: Direct heap iteration O(n), lazy deletion with cleanup
50
+
51
+ ## 📁 Module Structure
52
+
53
+ ```
54
+ events/
55
+ ├── events.py # Pure event data classes
56
+ ├── observer_manager.py # Read-only observation
57
+ ├── control_manager.py # Execution control policies
58
+ ├── models.py # Protocols and base classes
59
+ ├── config.py # Configuration and null implementations
60
+ └── README.md # This file
61
+ ```
62
+
63
+ ### Component Details
64
+
65
+ #### **events.py** - Event Data Classes
66
+ Pure immutable data classes representing pipeline events:
67
+ ```python
68
+ NodeStarted, NodeCompleted, NodeFailed # Node lifecycle
69
+ WaveStarted, WaveCompleted # Wave execution
70
+ PipelineStarted, PipelineCompleted # Pipeline lifecycle
71
+ ToolCalled, ToolCompleted # Tool usage
72
+ LLMPromptSent, LLMResponseReceived # LLM interactions
73
+ ```
74
+
75
+ #### **models.py** - Shared Interfaces
76
+ ```python
77
+ Observer # Protocol for observers
78
+ ControlHandler # Protocol for control handlers
79
+ BaseEventManager # Common manager functionality
80
+ ExecutionContext # Runtime context with retry tracking
81
+ ControlResponse # Response with signal and data
82
+ ControlSignal # Enum: PROCEED, SKIP, RETRY, etc.
83
+ ```
84
+
85
+ ## 🚀 Usage Examples
86
+
87
+ ### Observer Pattern - Metrics Collection
88
+ ```python
89
+ from hexdag.builtin.events import ObserverManager, NodeStarted
90
+
91
+ # Create observer manager with concurrency control
92
+ observer_manager = ObserverManager(
93
+ max_concurrent_observers=10,
94
+ observer_timeout=5.0,
95
+ max_sync_workers=4
96
+ )
97
+
98
+ # Register a metrics observer for specific events
99
+ async def metrics_observer(event):
100
+ # Record metrics to Prometheus/DataDog/etc
101
+ if isinstance(event, NodeStarted):
102
+ metrics.increment(f"node.{event.name}.started")
103
+
104
+ observer_manager.register(
105
+ metrics_observer,
106
+ event_types=[NodeStarted, NodeCompleted, NodeFailed] # Filter
107
+ )
108
+
109
+ # Fire and forget - non-blocking
110
+ await observer_manager.notify(NodeStarted(name="processor", wave_index=1))
111
+ ```
112
+
113
+ ### Control Pattern - Retry Policy
114
+ ```python
115
+ from hexdag.builtin.events import (
116
+ ControlManager, ControlResponse, ControlSignal, NodeFailed
117
+ )
118
+
119
+ control_manager = ControlManager()
120
+
121
+ # High-priority retry policy
122
+ async def retry_policy(event, context):
123
+ if isinstance(event, NodeFailed):
124
+ if context.attempt < 3:
125
+ return ControlResponse(
126
+ signal=ControlSignal.RETRY,
127
+ data={"delay": 1.0 * context.attempt} # Exponential backoff
128
+ )
129
+ else:
130
+ return ControlResponse(signal=ControlSignal.FAIL)
131
+ return ControlResponse() # PROCEED by default
132
+
133
+ control_manager.register(
134
+ retry_policy,
135
+ priority=10, # High priority (lower = higher)
136
+ name="retry_policy",
137
+ event_types=[NodeFailed] # Only handle failures
138
+ )
139
+
140
+ # Check event against all policies
141
+ response = await control_manager.check(event, context)
142
+ if response.signal == ControlSignal.RETRY:
143
+ # Orchestrator handles retry with delay
144
+ pass
145
+ ```
146
+
147
+ ### Event Type Filtering
148
+ ```python
149
+ # Observer only for tool events - 90% fewer invocations
150
+ observer_manager.register(
151
+ tool_observer,
152
+ event_types=[ToolCalled, ToolCompleted]
153
+ )
154
+
155
+ # Control handler only for nodes
156
+ control_manager.register(
157
+ node_handler,
158
+ priority=50,
159
+ event_types=[NodeStarted, NodeCompleted, NodeFailed]
160
+ )
161
+
162
+ # Universal observer sees everything (no filter)
163
+ observer_manager.register(audit_observer)
164
+ ```
165
+
166
+ ## ⚡ Performance Optimizations
167
+
168
+ ### 1. **Efficient Priority Queue**
169
+ ```python
170
+ # Before: O(n log n) sorting
171
+ for handler in sorted(handlers, key=lambda h: h.priority):
172
+ ...
173
+
174
+ # After: O(n) direct heap iteration
175
+ for entry in self._handler_heap: # Already ordered
176
+ ...
177
+ ```
178
+
179
+ ### 2. **Resource Reuse**
180
+ ```python
181
+ # Before: Create semaphore per event
182
+ async def notify(self, event):
183
+ semaphore = asyncio.Semaphore(self._max_concurrent)
184
+
185
+ # After: Create once, reuse
186
+ def __init__(self):
187
+ self._semaphore = asyncio.Semaphore(max_concurrent)
188
+ ```
189
+
190
+ ### 3. **Consolidated Storage**
191
+ ```python
192
+ # Before: Triple storage
193
+ self._handlers = {}
194
+ self._priorities = {}
195
+ self._event_filters = {}
196
+
197
+ # After: Single HandlerEntry dataclass
198
+ @dataclass
199
+ class HandlerEntry:
200
+ priority: int
201
+ handler: ControlHandler
202
+ event_types: set[Type] | None
203
+ metadata: HandlerMetadata
204
+ ```
205
+
206
+ ### 4. **Benchmark Results**
207
+ ```
208
+ ControlManager: 100 handlers, 1000 checks → 0.04ms per check
209
+ ObserverManager: 50 observers with filtering → 12ms per notification
210
+ Event filtering: ~90% reduction in unnecessary processing
211
+ Sync functions: Non-blocking with ThreadPoolExecutor
212
+ ```
213
+
214
+ ## 🧪 Testing
215
+
216
+ Comprehensive test coverage with 40+ tests:
217
+
218
+ ```bash
219
+ # Run all event system tests
220
+ uv run pytest tests/hexdag/core/application/events/ -v
221
+
222
+ # Test files:
223
+ test_observer_manager.py # 13 tests - observation and filtering
224
+ test_control_manager.py # 13 tests - control flow and policies
225
+ test_events.py # 14 tests - event data classes
226
+ test_performance.py # 5 tests - performance benchmarks
227
+ ```
228
+
229
+ Coverage:
230
+ - `control_manager.py`: 79% coverage
231
+ - `observer_manager.py`: 89% coverage
232
+ - `events.py`: 100% coverage
233
+
234
+ ## 🔄 Migration Guide
235
+
236
+ ### From Old EventBus (10 files → 5 files)
237
+ ```python
238
+ # Old: Mixed concerns in EventBus
239
+ event_bus = EventBus()
240
+ event_bus.register_handler(handler)
241
+ event_bus.emit(event)
242
+
243
+ # New: Separated by purpose
244
+ # For monitoring/telemetry
245
+ observer_manager = ObserverManager()
246
+ observer_manager.register(observer)
247
+ await observer_manager.notify(event)
248
+
249
+ # For control/policies
250
+ control_manager = ControlManager()
251
+ control_manager.register(handler, priority=10)
252
+ response = await control_manager.check(event, context)
253
+ ```
254
+
255
+ ### Adding Event Filtering (New Feature)
256
+ ```python
257
+ # Register for specific events only
258
+ manager.register(
259
+ handler,
260
+ event_types=[NodeStarted, NodeCompleted] # O(1) filtering
261
+ )
262
+ ```
263
+
264
+ ## 🎯 Design Principles
265
+
266
+ ### Hexagonal Architecture
267
+ - **Ports**: Manager interfaces define contracts
268
+ - **Adapters**: Handlers/observers are pluggable implementations
269
+ - **Domain Isolation**: Events are pure data, no behavior
270
+
271
+ ### Event-Driven Patterns
272
+ - **Observer Pattern**: Decoupled monitoring
273
+ - **Chain of Responsibility**: Priority-based control
274
+ - **Veto Pattern**: First non-PROCEED wins
275
+ - **Fire-and-Forget**: Async observation
276
+
277
+ ### Production Readiness
278
+ - **Fault Isolation**: Failures don't cascade
279
+ - **Graceful Degradation**: Critical vs non-critical handlers
280
+ - **Timeout Protection**: Bounded execution time
281
+ - **Resource Management**: Proper cleanup, no leaks
282
+
283
+ ## 🚦 Control Signals
284
+
285
+ | Signal | Purpose | Example Use Case |
286
+ |--------|---------|-----------------|
287
+ | PROCEED | Continue normally | Default - no intervention |
288
+ | SKIP | Skip this operation | Feature flags, A/B testing |
289
+ | RETRY | Retry with optional delay | Transient failures |
290
+ | FALLBACK | Use alternative value | Circuit breaker open |
291
+ | FAIL | Stop execution | Critical validation failure |
292
+ | ERROR | Critical handler error | Infrastructure issue |
293
+
294
+ ## 🔮 Future Enhancements
295
+
296
+ - [ ] **Event Replay** - Debugging with event history
297
+ - [ ] **Event Persistence** - Durable event storage
298
+ - [ ] **Distributed Streaming** - Kafka/Pulsar integration
299
+ - [ ] **WebSocket Broadcasting** - Real-time UI updates
300
+ - [ ] **Event Dashboard** - Visualization and analytics
301
+ - [ ] **Hierarchical Events** - Topic-based routing
302
+ - [ ] **Event Schemas** - OpenAPI/AsyncAPI specs
303
+
304
+ ## 📚 Related Documentation
305
+
306
+ - [Orchestrator Integration](../orchestrator.py) - How events integrate with DAG execution
307
+ - [Test Examples](../../../../../tests/hexdag/core/application/events/) - Comprehensive test suite
308
+ - [Performance Tests](../../../../../tests/hexdag/core/application/events/test_performance.py) - Benchmark validations
309
+
310
+ ---
311
+
312
+ *Built for production, optimized for performance, designed for flexibility.*
@@ -0,0 +1,104 @@
1
+ """Event system for the Hex-DAG framework.
2
+
3
+ Clean, simplified event system with clear separation of concerns:
4
+ - events.py: Event data classes (just data, no behavior)
5
+ - observers/: Observer implementations for monitoring and observability
6
+ """
7
+
8
+ # Event classes
9
+ from .events import (
10
+ CheckpointRestored,
11
+ CheckpointSaved,
12
+ Event,
13
+ HealthCheckCompleted,
14
+ LLMPromptSent,
15
+ LLMResponseReceived,
16
+ NodeCancelled,
17
+ NodeCompleted,
18
+ NodeFailed,
19
+ NodeSkipped,
20
+ NodeStarted,
21
+ PipelineCancelled,
22
+ PipelineCompleted,
23
+ PipelineStarted,
24
+ PolicyEvaluated,
25
+ PolicyFallback,
26
+ PolicyRetry,
27
+ PolicySkipped,
28
+ PolicyTriggered,
29
+ ToolCalled,
30
+ ToolCompleted,
31
+ WaveCompleted,
32
+ WaveStarted,
33
+ )
34
+
35
+ # Observer implementations
36
+ from .observers import (
37
+ AlertingObserver,
38
+ DataQualityObserver,
39
+ ExecutionTracerObserver,
40
+ PerformanceMetricsObserver,
41
+ ResourceMonitorObserver,
42
+ SimpleLoggingObserver,
43
+ )
44
+
45
+ # Event taxonomy - grouped event types for observer filtering
46
+ NODE_LIFECYCLE_EVENTS = (NodeStarted, NodeCompleted, NodeFailed, NodeCancelled, NodeSkipped)
47
+ WAVE_EVENTS = (WaveStarted, WaveCompleted)
48
+ PIPELINE_EVENTS = (PipelineStarted, PipelineCompleted, PipelineCancelled)
49
+ LLM_EVENTS = (LLMPromptSent, LLMResponseReceived)
50
+ TOOL_EVENTS = (ToolCalled, ToolCompleted)
51
+ POLICY_EVENTS = (PolicyEvaluated, PolicyTriggered, PolicyRetry, PolicySkipped, PolicyFallback)
52
+ CHECKPOINT_EVENTS = (CheckpointSaved, CheckpointRestored)
53
+ HEALTH_EVENTS = (HealthCheckCompleted,)
54
+
55
+ # Commonly used combinations
56
+ ALL_NODE_EVENTS = NODE_LIFECYCLE_EVENTS + WAVE_EVENTS
57
+ ALL_EXECUTION_EVENTS = NODE_LIFECYCLE_EVENTS + WAVE_EVENTS + PIPELINE_EVENTS
58
+ ALL_MONITORING_EVENTS = NODE_LIFECYCLE_EVENTS + LLM_EVENTS + TOOL_EVENTS + POLICY_EVENTS
59
+
60
+ __all__ = [
61
+ # Events
62
+ "Event",
63
+ "NodeStarted",
64
+ "NodeCompleted",
65
+ "NodeFailed",
66
+ "NodeCancelled",
67
+ "NodeSkipped",
68
+ "WaveStarted",
69
+ "WaveCompleted",
70
+ "PipelineStarted",
71
+ "PipelineCompleted",
72
+ "PipelineCancelled",
73
+ "LLMPromptSent",
74
+ "LLMResponseReceived",
75
+ "ToolCalled",
76
+ "ToolCompleted",
77
+ "PolicyEvaluated",
78
+ "PolicyTriggered",
79
+ "PolicySkipped",
80
+ "PolicyFallback",
81
+ "PolicyRetry",
82
+ "CheckpointSaved",
83
+ "CheckpointRestored",
84
+ "HealthCheckCompleted",
85
+ # Core Observers
86
+ "PerformanceMetricsObserver",
87
+ "AlertingObserver",
88
+ "ExecutionTracerObserver",
89
+ "SimpleLoggingObserver",
90
+ "ResourceMonitorObserver",
91
+ "DataQualityObserver",
92
+ # Event Taxonomy
93
+ "NODE_LIFECYCLE_EVENTS",
94
+ "WAVE_EVENTS",
95
+ "PIPELINE_EVENTS",
96
+ "LLM_EVENTS",
97
+ "TOOL_EVENTS",
98
+ "POLICY_EVENTS",
99
+ "CHECKPOINT_EVENTS",
100
+ "HEALTH_EVENTS",
101
+ "ALL_NODE_EVENTS",
102
+ "ALL_EXECUTION_EVENTS",
103
+ "ALL_MONITORING_EVENTS",
104
+ ]