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,418 @@
1
+ """Models for orchestration state and checkpoints.
2
+
3
+ This module contains models for representing orchestrator execution
4
+ state, execution context, and human-in-the-loop approval requests.
5
+ It also includes port configuration models for managing per-node and
6
+ per-type port customization.
7
+ """
8
+
9
+ from collections.abc import Mapping
10
+ from dataclasses import dataclass, field
11
+ from datetime import datetime
12
+ from typing import Any
13
+
14
+ from pydantic import BaseModel, ConfigDict
15
+
16
+ from hexdag.core.exceptions import ValidationError
17
+
18
+
19
+ @dataclass(frozen=True, slots=True)
20
+ class OrchestratorConfig:
21
+ """Configuration for orchestrator behavior.
22
+
23
+ This immutable configuration object centralizes all orchestrator settings,
24
+ making it easier to pass configurations around and test different settings.
25
+
26
+ Attributes
27
+ ----------
28
+ max_concurrent_nodes : int, default=10
29
+ Maximum number of nodes to execute concurrently in a wave.
30
+ Controls parallelism and resource usage.
31
+ strict_validation : bool, default=False
32
+ If True, raise errors on validation failures.
33
+ If False, log warnings and continue execution.
34
+ default_node_timeout : float | None, default=None
35
+ Default timeout in seconds for node execution.
36
+ None means no timeout. Can be overridden per-node.
37
+
38
+ Examples
39
+ --------
40
+ Example usage::
41
+
42
+ config = OrchestratorConfig(
43
+ max_concurrent_nodes=5,
44
+ strict_validation=True,
45
+ default_node_timeout=30.0
46
+ )
47
+ orchestrator = Orchestrator(config=config)
48
+
49
+ # Or use defaults
50
+ config = OrchestratorConfig()
51
+ config.max_concurrent_nodes
52
+ 10
53
+ """
54
+
55
+ max_concurrent_nodes: int = 10
56
+ strict_validation: bool = False
57
+ default_node_timeout: float | None = None
58
+
59
+ def __post_init__(self) -> None:
60
+ """Validate configuration parameters."""
61
+ if self.max_concurrent_nodes <= 0:
62
+ raise ValidationError(
63
+ "max_concurrent_nodes", "must be positive", self.max_concurrent_nodes
64
+ )
65
+
66
+ if self.default_node_timeout is not None and self.default_node_timeout <= 0:
67
+ raise ValidationError(
68
+ "default_node_timeout", "must be positive or None", self.default_node_timeout
69
+ )
70
+
71
+
72
+ class CheckpointState(BaseModel):
73
+ """Complete state for checkpoint/resume.
74
+
75
+ Saves everything needed to resume a DAG execution from where it left off,
76
+ including the full graph structure (handles both static and dynamic DAGs).
77
+
78
+ Attributes
79
+ ----------
80
+ run_id : str
81
+ Unique identifier for this execution run
82
+ dag_id : str
83
+ Stable identifier for the DAG (e.g., YAML file path, function name)
84
+ graph_snapshot : dict[str, Any]
85
+ Serialized DirectedGraph structure
86
+ Format: {"nodes": {...}, "edges": [...]}
87
+ initial_input : Any
88
+ Initial input data passed to the DAG
89
+ node_results : dict[str, Any]
90
+ Results from completed nodes (node_id -> output)
91
+ completed_node_ids : list[str]
92
+ Ordered list of completed node IDs (preserves execution order)
93
+ failed_node_ids : list[str]
94
+ List of node IDs that failed (for retry/debugging)
95
+ created_at : datetime
96
+ When execution started
97
+ updated_at : datetime
98
+ Last checkpoint save time
99
+ metadata : dict[str, Any]
100
+ Optional metadata (custom fields, tags, etc.)
101
+
102
+ Notes
103
+ -----
104
+ To resume execution:
105
+ 1. Load CheckpointState by run_id
106
+ 2. Deserialize graph_snapshot to DirectedGraph
107
+ 3. Filter out completed nodes using completed_node_ids
108
+ 4. Resume execution with filtered graph and node_results
109
+ """
110
+
111
+ model_config = ConfigDict(arbitrary_types_allowed=True)
112
+
113
+ run_id: str
114
+ dag_id: str
115
+ graph_snapshot: dict[str, Any] # Always save the graph
116
+ initial_input: Any
117
+ node_results: dict[str, Any] # node_id -> output
118
+ completed_node_ids: list[str] # Ordered
119
+ failed_node_ids: list[str] = []
120
+ created_at: datetime
121
+ updated_at: datetime
122
+ metadata: dict[str, Any] = {}
123
+
124
+
125
+ @dataclass(slots=True)
126
+ class NodeExecutionContext:
127
+ """Lightweight context tracking current execution position.
128
+
129
+ This flows through the execution pipeline and is NOT persisted in checkpoints.
130
+ It's only for tracking where we are during live execution (which node, wave, attempt).
131
+
132
+ Attributes
133
+ ----------
134
+ dag_id : str
135
+ Identifier for the DAG being executed
136
+ node_id : str | None
137
+ Current node being executed (None for DAG-level operations)
138
+ wave_index : int
139
+ Index of the current execution wave (for parallel execution tracking)
140
+ attempt : int
141
+ Attempt number (for retry scenarios)
142
+ metadata : dict[str, Any]
143
+ Additional metadata that can be attached to the context
144
+ """
145
+
146
+ dag_id: str
147
+ node_id: str | None = None
148
+ wave_index: int = 0
149
+ attempt: int = 1
150
+ metadata: dict[str, Any] = field(default_factory=dict)
151
+
152
+ def with_node(self, node_id: str, wave_index: int) -> "NodeExecutionContext":
153
+ """Create new context for a specific node execution.
154
+
155
+ Args
156
+ ----
157
+ node_id: The ID of the node being executed
158
+ wave_index: The wave index for parallel execution tracking
159
+
160
+ Returns
161
+ -------
162
+ NodeExecutionContext
163
+ New context with updated node and wave information
164
+ """
165
+ return NodeExecutionContext(
166
+ dag_id=self.dag_id,
167
+ node_id=node_id,
168
+ wave_index=wave_index,
169
+ attempt=self.attempt,
170
+ metadata=self.metadata.copy(),
171
+ )
172
+
173
+ def with_attempt(self, attempt: int) -> "NodeExecutionContext":
174
+ """Create new context with updated attempt number.
175
+
176
+ Args
177
+ ----
178
+ attempt: The attempt number (for retry scenarios)
179
+
180
+ Returns
181
+ -------
182
+ NodeExecutionContext
183
+ New context with updated attempt number
184
+ """
185
+ return NodeExecutionContext(
186
+ dag_id=self.dag_id,
187
+ node_id=self.node_id,
188
+ wave_index=self.wave_index,
189
+ attempt=attempt,
190
+ metadata=self.metadata.copy(),
191
+ )
192
+
193
+
194
+ @dataclass(frozen=True, slots=True)
195
+ class PortConfig:
196
+ """Configuration for a single port instance.
197
+
198
+ A PortConfig wraps a port implementation with optional metadata,
199
+ allowing for fine-grained control over port behavior per node.
200
+
201
+ Attributes
202
+ ----------
203
+ port : Any
204
+ The port implementation instance (e.g., LLM, Database, Memory adapter)
205
+ metadata : Mapping[str, Any] | None
206
+ Optional metadata for the port (e.g., timeouts, retry settings, rate limits)
207
+
208
+ Examples
209
+ --------
210
+ Example usage::
211
+
212
+ from hexdag.builtin.adapters.mock import MockLLM
213
+ config = PortConfig(
214
+ port=MockLLM(),
215
+ metadata={"timeout": 30, "max_retries": 3}
216
+ )
217
+ config.port
218
+ <MockLLM object>
219
+ config.get_metadata()
220
+ {'timeout': 30, 'max_retries': 3}
221
+ """
222
+
223
+ port: Any
224
+ metadata: Mapping[str, Any] | None = None
225
+
226
+ def __post_init__(self) -> None:
227
+ """Ensure metadata is immutable if provided."""
228
+ if self.metadata is not None:
229
+ object.__setattr__(self, "metadata", tuple(self.metadata.items()))
230
+
231
+ def get_metadata(self) -> dict[str, Any]:
232
+ """Get metadata as a dictionary.
233
+
234
+ Returns
235
+ -------
236
+ dict[str, Any]
237
+ Metadata dictionary (empty if no metadata)
238
+ """
239
+ if self.metadata is None:
240
+ return {}
241
+ return dict(self.metadata)
242
+
243
+
244
+ @dataclass(frozen=True, slots=True)
245
+ class PortsConfiguration:
246
+ """Complete port configuration with inheritance and overrides.
247
+
248
+ This model supports three levels of port configuration with clear inheritance:
249
+ 1. **Global defaults** - Apply to all nodes unless overridden
250
+ 2. **Per-type defaults** - Apply to all nodes of a specific type (e.g., "agent", "llm")
251
+ 3. **Per-node overrides** - Apply to specific nodes by name
252
+
253
+ **Resolution order**: per-node > per-type > global defaults
254
+
255
+ Attributes
256
+ ----------
257
+ global_ports : Mapping[str, PortConfig] | None
258
+ Default ports for all nodes
259
+ type_ports : Mapping[str, Mapping[str, PortConfig]] | None
260
+ Ports per node type, keyed by type name (e.g., {"agent": {"llm": config}})
261
+ node_ports : Mapping[str, Mapping[str, PortConfig]] | None
262
+ Ports per specific node name (e.g., {"researcher": {"llm": config}})
263
+
264
+ Examples
265
+ --------
266
+ Example usage::
267
+
268
+ from hexdag.builtin.adapters.mock import MockLLM
269
+ from hexdag.builtin.adapters.openai import OpenAIAdapter
270
+ from hexdag.builtin.adapters.anthropic import AnthropicAdapter
271
+
272
+ # Global default: All nodes use MockLLM
273
+ config = PortsConfiguration(
274
+ global_ports={"llm": PortConfig(MockLLM())},
275
+ type_ports={
276
+ # Override for all "agent" type nodes
277
+ "agent": {"llm": PortConfig(OpenAIAdapter(model="gpt-4"))}
278
+ },
279
+ node_ports={
280
+ # Override for specific "researcher" node
281
+ "researcher": {"llm": PortConfig(AnthropicAdapter(model="claude-3"))}
282
+ }
283
+ )
284
+
285
+ # Resolution for different nodes:
286
+ # - "researcher" node: AnthropicAdapter (per-node override)
287
+ researcher_ports = config.resolve_ports("researcher", "agent")
288
+ assert isinstance(researcher_ports["llm"].port, AnthropicAdapter)
289
+
290
+ # - Other "agent" nodes: OpenAIAdapter (per-type default)
291
+ agent_ports = config.resolve_ports("analyzer", "agent")
292
+ assert isinstance(agent_ports["llm"].port, OpenAIAdapter)
293
+
294
+ # - Other nodes: MockLLM (global default)
295
+ function_ports = config.resolve_ports("transformer", "function")
296
+ assert isinstance(function_ports["llm"].port, MockLLM)
297
+
298
+ Notes
299
+ -----
300
+ This design enables:
301
+ - **Cost optimization**: Use cheaper models for simple nodes, expensive ones for complex tasks
302
+ - **Performance tuning**: Different timeout/retry settings per node type
303
+ - **Testing flexibility**: Mock some nodes, use real adapters for others
304
+ - **Multi-tenant support**: Different credentials per node/type
305
+ """
306
+
307
+ global_ports: Mapping[str, PortConfig] | None = None
308
+ type_ports: Mapping[str, Mapping[str, PortConfig]] | None = None
309
+ node_ports: Mapping[str, Mapping[str, PortConfig]] | None = None
310
+
311
+ def __post_init__(self) -> None:
312
+ """Ensure all mappings are immutable."""
313
+ if self.global_ports is not None:
314
+ object.__setattr__(self, "global_ports", tuple(self.global_ports.items()))
315
+ if self.type_ports is not None:
316
+ type_items = tuple(
317
+ (node_type, tuple(ports.items())) for node_type, ports in self.type_ports.items()
318
+ )
319
+ object.__setattr__(self, "type_ports", type_items)
320
+ if self.node_ports is not None:
321
+ node_items = tuple(
322
+ (node_name, tuple(ports.items())) for node_name, ports in self.node_ports.items()
323
+ )
324
+ object.__setattr__(self, "node_ports", node_items)
325
+
326
+ def resolve_ports(self, node_name: str, node_type: str | None = None) -> dict[str, PortConfig]:
327
+ """Resolve ports for a specific node following inheritance rules.
328
+
329
+ Combines ports from all three levels (global, type, node) with proper
330
+ precedence: per-node > per-type > global defaults.
331
+
332
+ Parameters
333
+ ----------
334
+ node_name : str
335
+ Name of the node to resolve ports for
336
+ node_type : str | None
337
+ Type of the node (e.g., "llm", "agent", "function", "loop")
338
+
339
+ Returns
340
+ -------
341
+ dict[str, PortConfig]
342
+ Resolved ports for the node with PortConfig wrappers
343
+
344
+ Examples
345
+ --------
346
+ Example usage::
347
+
348
+ config = PortsConfiguration(
349
+ global_ports={"llm": PortConfig(MockLLM())},
350
+ type_ports={"agent": {"llm": PortConfig(OpenAIAdapter())}},
351
+ node_ports={"researcher": {"llm": PortConfig(AnthropicAdapter())}}
352
+ )
353
+
354
+ # Researcher node gets Anthropic (per-node override)
355
+ researcher_ports = config.resolve_ports("researcher", "agent")
356
+ assert isinstance(researcher_ports["llm"].port, AnthropicAdapter)
357
+
358
+ # Other agent nodes get OpenAI (per-type default)
359
+ agent_ports = config.resolve_ports("analyzer", "agent")
360
+ assert isinstance(agent_ports["llm"].port, OpenAIAdapter)
361
+
362
+ # Function nodes get Mock (global default)
363
+ function_ports = config.resolve_ports("transformer", "function")
364
+ assert isinstance(function_ports["llm"].port, MockLLM)
365
+ """
366
+ result: dict[str, PortConfig] = {}
367
+
368
+ # 1. Start with global defaults (lowest priority)
369
+ if self.global_ports is not None:
370
+ result.update(dict(self.global_ports))
371
+
372
+ # 2. Apply per-type defaults (overrides global)
373
+ if self.type_ports is not None and node_type is not None:
374
+ type_dict = dict(self.type_ports)
375
+ if node_type in type_dict:
376
+ result.update(dict(type_dict[node_type]))
377
+
378
+ # 3. Apply per-node overrides (highest priority)
379
+ if self.node_ports is not None:
380
+ node_dict = dict(self.node_ports)
381
+ if node_name in node_dict:
382
+ result.update(dict(node_dict[node_name]))
383
+
384
+ return result
385
+
386
+ def to_flat_dict(self, node_name: str, node_type: str | None = None) -> dict[str, Any]:
387
+ """Convert resolved ports to flat dictionary of port instances.
388
+
389
+ This extracts the actual port instances from PortConfig wrappers,
390
+ providing backward compatibility with the current orchestrator
391
+ interface that expects `dict[str, Any]` ports.
392
+
393
+ Parameters
394
+ ----------
395
+ node_name : str
396
+ Name of the node
397
+ node_type : str | None
398
+ Type of the node
399
+
400
+ Returns
401
+ -------
402
+ dict[str, Any]
403
+ Dictionary mapping port names to port instances (unwrapped)
404
+
405
+ Examples
406
+ --------
407
+ Example usage::
408
+
409
+ config = PortsConfiguration(
410
+ global_ports={"llm": PortConfig(MockLLM())}
411
+ )
412
+
413
+ ports = config.to_flat_dict("my_node")
414
+ assert "llm" in ports
415
+ assert isinstance(ports["llm"], MockLLM) # Unwrapped
416
+ """
417
+ resolved = self.resolve_ports(node_name, node_type)
418
+ return {port_name: config.port for port_name, config in resolved.items()}