omnibase_infra 0.2.6__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 (833) hide show
  1. omnibase_infra/__init__.py +101 -0
  2. omnibase_infra/adapters/adapter_onex_tool_execution.py +451 -0
  3. omnibase_infra/capabilities/__init__.py +15 -0
  4. omnibase_infra/capabilities/capability_inference_rules.py +211 -0
  5. omnibase_infra/capabilities/contract_capability_extractor.py +221 -0
  6. omnibase_infra/capabilities/intent_type_extractor.py +160 -0
  7. omnibase_infra/cli/__init__.py +1 -0
  8. omnibase_infra/cli/commands.py +216 -0
  9. omnibase_infra/clients/__init__.py +0 -0
  10. omnibase_infra/configs/widget_mapping.yaml +176 -0
  11. omnibase_infra/constants_topic_patterns.py +26 -0
  12. omnibase_infra/contracts/handlers/filesystem/handler_contract.yaml +264 -0
  13. omnibase_infra/contracts/handlers/mcp/handler_contract.yaml +141 -0
  14. omnibase_infra/decorators/__init__.py +29 -0
  15. omnibase_infra/decorators/allow_any.py +109 -0
  16. omnibase_infra/dlq/__init__.py +90 -0
  17. omnibase_infra/dlq/constants_dlq.py +57 -0
  18. omnibase_infra/dlq/models/__init__.py +26 -0
  19. omnibase_infra/dlq/models/enum_replay_status.py +37 -0
  20. omnibase_infra/dlq/models/model_dlq_replay_record.py +135 -0
  21. omnibase_infra/dlq/models/model_dlq_tracking_config.py +184 -0
  22. omnibase_infra/dlq/service_dlq_tracking.py +611 -0
  23. omnibase_infra/enums/__init__.py +132 -0
  24. omnibase_infra/enums/enum_any_type_violation.py +104 -0
  25. omnibase_infra/enums/enum_backend_type.py +27 -0
  26. omnibase_infra/enums/enum_capture_outcome.py +42 -0
  27. omnibase_infra/enums/enum_capture_state.py +88 -0
  28. omnibase_infra/enums/enum_chain_violation_type.py +119 -0
  29. omnibase_infra/enums/enum_circuit_state.py +51 -0
  30. omnibase_infra/enums/enum_confirmation_event_type.py +27 -0
  31. omnibase_infra/enums/enum_consumer_group_purpose.py +92 -0
  32. omnibase_infra/enums/enum_contract_type.py +84 -0
  33. omnibase_infra/enums/enum_dedupe_strategy.py +46 -0
  34. omnibase_infra/enums/enum_dispatch_status.py +191 -0
  35. omnibase_infra/enums/enum_environment.py +46 -0
  36. omnibase_infra/enums/enum_execution_shape_violation.py +103 -0
  37. omnibase_infra/enums/enum_handler_error_type.py +111 -0
  38. omnibase_infra/enums/enum_handler_loader_error.py +178 -0
  39. omnibase_infra/enums/enum_handler_source_mode.py +86 -0
  40. omnibase_infra/enums/enum_handler_source_type.py +87 -0
  41. omnibase_infra/enums/enum_handler_type.py +77 -0
  42. omnibase_infra/enums/enum_handler_type_category.py +61 -0
  43. omnibase_infra/enums/enum_infra_transport_type.py +73 -0
  44. omnibase_infra/enums/enum_introspection_reason.py +154 -0
  45. omnibase_infra/enums/enum_kafka_acks.py +99 -0
  46. omnibase_infra/enums/enum_message_category.py +213 -0
  47. omnibase_infra/enums/enum_node_archetype.py +74 -0
  48. omnibase_infra/enums/enum_node_output_type.py +185 -0
  49. omnibase_infra/enums/enum_non_retryable_error_category.py +224 -0
  50. omnibase_infra/enums/enum_policy_type.py +32 -0
  51. omnibase_infra/enums/enum_registration_state.py +261 -0
  52. omnibase_infra/enums/enum_registration_status.py +33 -0
  53. omnibase_infra/enums/enum_registry_response_status.py +28 -0
  54. omnibase_infra/enums/enum_response_status.py +26 -0
  55. omnibase_infra/enums/enum_retry_error_category.py +98 -0
  56. omnibase_infra/enums/enum_security_rule_id.py +103 -0
  57. omnibase_infra/enums/enum_selection_strategy.py +91 -0
  58. omnibase_infra/enums/enum_topic_standard.py +42 -0
  59. omnibase_infra/enums/enum_validation_severity.py +78 -0
  60. omnibase_infra/errors/__init__.py +160 -0
  61. omnibase_infra/errors/error_architecture_violation.py +152 -0
  62. omnibase_infra/errors/error_binding_resolution.py +128 -0
  63. omnibase_infra/errors/error_chain_propagation.py +188 -0
  64. omnibase_infra/errors/error_compute_registry.py +95 -0
  65. omnibase_infra/errors/error_consul.py +132 -0
  66. omnibase_infra/errors/error_container_wiring.py +243 -0
  67. omnibase_infra/errors/error_event_bus_registry.py +105 -0
  68. omnibase_infra/errors/error_infra.py +610 -0
  69. omnibase_infra/errors/error_message_type_registry.py +101 -0
  70. omnibase_infra/errors/error_policy_registry.py +115 -0
  71. omnibase_infra/errors/error_vault.py +123 -0
  72. omnibase_infra/event_bus/__init__.py +72 -0
  73. omnibase_infra/event_bus/configs/kafka_event_bus_config.yaml +84 -0
  74. omnibase_infra/event_bus/event_bus_inmemory.py +797 -0
  75. omnibase_infra/event_bus/event_bus_kafka.py +1716 -0
  76. omnibase_infra/event_bus/mixin_kafka_broadcast.py +180 -0
  77. omnibase_infra/event_bus/mixin_kafka_dlq.py +771 -0
  78. omnibase_infra/event_bus/models/__init__.py +29 -0
  79. omnibase_infra/event_bus/models/config/__init__.py +20 -0
  80. omnibase_infra/event_bus/models/config/model_kafka_event_bus_config.py +693 -0
  81. omnibase_infra/event_bus/models/model_dlq_event.py +206 -0
  82. omnibase_infra/event_bus/models/model_dlq_metrics.py +304 -0
  83. omnibase_infra/event_bus/models/model_event_headers.py +115 -0
  84. omnibase_infra/event_bus/models/model_event_message.py +60 -0
  85. omnibase_infra/event_bus/testing/__init__.py +26 -0
  86. omnibase_infra/event_bus/testing/adapter_protocol_event_publisher_inmemory.py +418 -0
  87. omnibase_infra/event_bus/testing/model_publisher_metrics.py +64 -0
  88. omnibase_infra/event_bus/topic_constants.py +376 -0
  89. omnibase_infra/handlers/__init__.py +82 -0
  90. omnibase_infra/handlers/filesystem/__init__.py +48 -0
  91. omnibase_infra/handlers/filesystem/enum_file_system_operation.py +35 -0
  92. omnibase_infra/handlers/filesystem/model_file_system_request.py +298 -0
  93. omnibase_infra/handlers/filesystem/model_file_system_result.py +166 -0
  94. omnibase_infra/handlers/handler_consul.py +795 -0
  95. omnibase_infra/handlers/handler_db.py +1046 -0
  96. omnibase_infra/handlers/handler_filesystem.py +1478 -0
  97. omnibase_infra/handlers/handler_graph.py +2015 -0
  98. omnibase_infra/handlers/handler_http.py +926 -0
  99. omnibase_infra/handlers/handler_intent.py +387 -0
  100. omnibase_infra/handlers/handler_manifest_persistence.contract.yaml +184 -0
  101. omnibase_infra/handlers/handler_manifest_persistence.py +1539 -0
  102. omnibase_infra/handlers/handler_mcp.py +1430 -0
  103. omnibase_infra/handlers/handler_qdrant.py +1076 -0
  104. omnibase_infra/handlers/handler_vault.py +428 -0
  105. omnibase_infra/handlers/mcp/__init__.py +19 -0
  106. omnibase_infra/handlers/mcp/adapter_onex_to_mcp.py +446 -0
  107. omnibase_infra/handlers/mcp/protocols.py +178 -0
  108. omnibase_infra/handlers/mcp/transport_streamable_http.py +352 -0
  109. omnibase_infra/handlers/mixins/__init__.py +47 -0
  110. omnibase_infra/handlers/mixins/mixin_consul_initialization.py +349 -0
  111. omnibase_infra/handlers/mixins/mixin_consul_kv.py +338 -0
  112. omnibase_infra/handlers/mixins/mixin_consul_service.py +542 -0
  113. omnibase_infra/handlers/mixins/mixin_consul_topic_index.py +585 -0
  114. omnibase_infra/handlers/mixins/mixin_vault_initialization.py +338 -0
  115. omnibase_infra/handlers/mixins/mixin_vault_retry.py +412 -0
  116. omnibase_infra/handlers/mixins/mixin_vault_secrets.py +450 -0
  117. omnibase_infra/handlers/mixins/mixin_vault_token.py +365 -0
  118. omnibase_infra/handlers/models/__init__.py +286 -0
  119. omnibase_infra/handlers/models/consul/__init__.py +81 -0
  120. omnibase_infra/handlers/models/consul/enum_consul_operation_type.py +57 -0
  121. omnibase_infra/handlers/models/consul/model_consul_deregister_payload.py +51 -0
  122. omnibase_infra/handlers/models/consul/model_consul_handler_config.py +153 -0
  123. omnibase_infra/handlers/models/consul/model_consul_handler_payload.py +89 -0
  124. omnibase_infra/handlers/models/consul/model_consul_kv_get_found_payload.py +55 -0
  125. omnibase_infra/handlers/models/consul/model_consul_kv_get_not_found_payload.py +49 -0
  126. omnibase_infra/handlers/models/consul/model_consul_kv_get_recurse_payload.py +50 -0
  127. omnibase_infra/handlers/models/consul/model_consul_kv_item.py +33 -0
  128. omnibase_infra/handlers/models/consul/model_consul_kv_put_payload.py +41 -0
  129. omnibase_infra/handlers/models/consul/model_consul_register_payload.py +53 -0
  130. omnibase_infra/handlers/models/consul/model_consul_retry_config.py +66 -0
  131. omnibase_infra/handlers/models/consul/model_payload_consul.py +66 -0
  132. omnibase_infra/handlers/models/consul/registry_payload_consul.py +214 -0
  133. omnibase_infra/handlers/models/graph/__init__.py +35 -0
  134. omnibase_infra/handlers/models/graph/enum_graph_operation_type.py +20 -0
  135. omnibase_infra/handlers/models/graph/model_graph_execute_payload.py +38 -0
  136. omnibase_infra/handlers/models/graph/model_graph_handler_config.py +54 -0
  137. omnibase_infra/handlers/models/graph/model_graph_handler_payload.py +44 -0
  138. omnibase_infra/handlers/models/graph/model_graph_query_payload.py +40 -0
  139. omnibase_infra/handlers/models/graph/model_graph_record.py +22 -0
  140. omnibase_infra/handlers/models/http/__init__.py +50 -0
  141. omnibase_infra/handlers/models/http/enum_http_operation_type.py +29 -0
  142. omnibase_infra/handlers/models/http/model_http_body_content.py +45 -0
  143. omnibase_infra/handlers/models/http/model_http_get_payload.py +88 -0
  144. omnibase_infra/handlers/models/http/model_http_handler_payload.py +90 -0
  145. omnibase_infra/handlers/models/http/model_http_post_payload.py +88 -0
  146. omnibase_infra/handlers/models/http/model_payload_http.py +66 -0
  147. omnibase_infra/handlers/models/http/registry_payload_http.py +212 -0
  148. omnibase_infra/handlers/models/mcp/__init__.py +23 -0
  149. omnibase_infra/handlers/models/mcp/enum_mcp_operation_type.py +24 -0
  150. omnibase_infra/handlers/models/mcp/model_mcp_handler_config.py +40 -0
  151. omnibase_infra/handlers/models/mcp/model_mcp_tool_call.py +32 -0
  152. omnibase_infra/handlers/models/mcp/model_mcp_tool_result.py +45 -0
  153. omnibase_infra/handlers/models/model_consul_handler_response.py +96 -0
  154. omnibase_infra/handlers/models/model_db_describe_response.py +83 -0
  155. omnibase_infra/handlers/models/model_db_query_payload.py +95 -0
  156. omnibase_infra/handlers/models/model_db_query_response.py +60 -0
  157. omnibase_infra/handlers/models/model_filesystem_config.py +98 -0
  158. omnibase_infra/handlers/models/model_filesystem_delete_payload.py +54 -0
  159. omnibase_infra/handlers/models/model_filesystem_delete_result.py +77 -0
  160. omnibase_infra/handlers/models/model_filesystem_directory_entry.py +75 -0
  161. omnibase_infra/handlers/models/model_filesystem_ensure_directory_payload.py +54 -0
  162. omnibase_infra/handlers/models/model_filesystem_ensure_directory_result.py +60 -0
  163. omnibase_infra/handlers/models/model_filesystem_list_directory_payload.py +60 -0
  164. omnibase_infra/handlers/models/model_filesystem_list_directory_result.py +68 -0
  165. omnibase_infra/handlers/models/model_filesystem_read_payload.py +62 -0
  166. omnibase_infra/handlers/models/model_filesystem_read_result.py +61 -0
  167. omnibase_infra/handlers/models/model_filesystem_write_payload.py +70 -0
  168. omnibase_infra/handlers/models/model_filesystem_write_result.py +55 -0
  169. omnibase_infra/handlers/models/model_graph_handler_response.py +98 -0
  170. omnibase_infra/handlers/models/model_handler_response.py +103 -0
  171. omnibase_infra/handlers/models/model_http_handler_response.py +101 -0
  172. omnibase_infra/handlers/models/model_manifest_metadata.py +75 -0
  173. omnibase_infra/handlers/models/model_manifest_persistence_config.py +62 -0
  174. omnibase_infra/handlers/models/model_manifest_query_payload.py +90 -0
  175. omnibase_infra/handlers/models/model_manifest_query_result.py +97 -0
  176. omnibase_infra/handlers/models/model_manifest_retrieve_payload.py +44 -0
  177. omnibase_infra/handlers/models/model_manifest_retrieve_result.py +98 -0
  178. omnibase_infra/handlers/models/model_manifest_store_payload.py +47 -0
  179. omnibase_infra/handlers/models/model_manifest_store_result.py +67 -0
  180. omnibase_infra/handlers/models/model_operation_context.py +187 -0
  181. omnibase_infra/handlers/models/model_qdrant_handler_response.py +98 -0
  182. omnibase_infra/handlers/models/model_retry_state.py +162 -0
  183. omnibase_infra/handlers/models/model_vault_handler_response.py +98 -0
  184. omnibase_infra/handlers/models/qdrant/__init__.py +44 -0
  185. omnibase_infra/handlers/models/qdrant/enum_qdrant_operation_type.py +26 -0
  186. omnibase_infra/handlers/models/qdrant/model_qdrant_collection_payload.py +42 -0
  187. omnibase_infra/handlers/models/qdrant/model_qdrant_delete_payload.py +36 -0
  188. omnibase_infra/handlers/models/qdrant/model_qdrant_handler_config.py +42 -0
  189. omnibase_infra/handlers/models/qdrant/model_qdrant_handler_payload.py +54 -0
  190. omnibase_infra/handlers/models/qdrant/model_qdrant_search_payload.py +42 -0
  191. omnibase_infra/handlers/models/qdrant/model_qdrant_search_result.py +30 -0
  192. omnibase_infra/handlers/models/qdrant/model_qdrant_upsert_payload.py +36 -0
  193. omnibase_infra/handlers/models/vault/__init__.py +69 -0
  194. omnibase_infra/handlers/models/vault/enum_vault_operation_type.py +35 -0
  195. omnibase_infra/handlers/models/vault/model_payload_vault.py +66 -0
  196. omnibase_infra/handlers/models/vault/model_vault_delete_payload.py +57 -0
  197. omnibase_infra/handlers/models/vault/model_vault_handler_config.py +148 -0
  198. omnibase_infra/handlers/models/vault/model_vault_handler_payload.py +101 -0
  199. omnibase_infra/handlers/models/vault/model_vault_list_payload.py +58 -0
  200. omnibase_infra/handlers/models/vault/model_vault_renew_token_payload.py +67 -0
  201. omnibase_infra/handlers/models/vault/model_vault_retry_config.py +66 -0
  202. omnibase_infra/handlers/models/vault/model_vault_secret_payload.py +106 -0
  203. omnibase_infra/handlers/models/vault/model_vault_write_payload.py +66 -0
  204. omnibase_infra/handlers/models/vault/registry_payload_vault.py +213 -0
  205. omnibase_infra/handlers/registration_storage/__init__.py +43 -0
  206. omnibase_infra/handlers/registration_storage/handler_registration_storage_mock.py +392 -0
  207. omnibase_infra/handlers/registration_storage/handler_registration_storage_postgres.py +922 -0
  208. omnibase_infra/handlers/registration_storage/models/__init__.py +23 -0
  209. omnibase_infra/handlers/registration_storage/models/model_delete_registration_request.py +58 -0
  210. omnibase_infra/handlers/registration_storage/models/model_update_registration_request.py +73 -0
  211. omnibase_infra/handlers/registration_storage/protocol_registration_persistence.py +191 -0
  212. omnibase_infra/handlers/service_discovery/__init__.py +43 -0
  213. omnibase_infra/handlers/service_discovery/handler_service_discovery_consul.py +1051 -0
  214. omnibase_infra/handlers/service_discovery/handler_service_discovery_mock.py +258 -0
  215. omnibase_infra/handlers/service_discovery/models/__init__.py +22 -0
  216. omnibase_infra/handlers/service_discovery/models/model_discovery_result.py +64 -0
  217. omnibase_infra/handlers/service_discovery/models/model_registration_result.py +138 -0
  218. omnibase_infra/handlers/service_discovery/models/model_service_info.py +109 -0
  219. omnibase_infra/handlers/service_discovery/protocol_discovery_operations.py +170 -0
  220. omnibase_infra/idempotency/__init__.py +94 -0
  221. omnibase_infra/idempotency/models/__init__.py +43 -0
  222. omnibase_infra/idempotency/models/model_idempotency_check_result.py +85 -0
  223. omnibase_infra/idempotency/models/model_idempotency_guard_config.py +130 -0
  224. omnibase_infra/idempotency/models/model_idempotency_record.py +86 -0
  225. omnibase_infra/idempotency/models/model_idempotency_store_health_check_result.py +81 -0
  226. omnibase_infra/idempotency/models/model_idempotency_store_metrics.py +140 -0
  227. omnibase_infra/idempotency/models/model_postgres_idempotency_store_config.py +299 -0
  228. omnibase_infra/idempotency/protocol_idempotency_store.py +184 -0
  229. omnibase_infra/idempotency/store_inmemory.py +265 -0
  230. omnibase_infra/idempotency/store_postgres.py +923 -0
  231. omnibase_infra/infrastructure/__init__.py +0 -0
  232. omnibase_infra/migrations/001_create_event_ledger.sql +166 -0
  233. omnibase_infra/migrations/001_drop_event_ledger.sql +18 -0
  234. omnibase_infra/mixins/__init__.py +71 -0
  235. omnibase_infra/mixins/mixin_async_circuit_breaker.py +656 -0
  236. omnibase_infra/mixins/mixin_dict_like_accessors.py +146 -0
  237. omnibase_infra/mixins/mixin_envelope_extraction.py +119 -0
  238. omnibase_infra/mixins/mixin_node_introspection.py +2670 -0
  239. omnibase_infra/mixins/mixin_retry_execution.py +386 -0
  240. omnibase_infra/mixins/protocol_circuit_breaker_aware.py +133 -0
  241. omnibase_infra/models/__init__.py +144 -0
  242. omnibase_infra/models/bindings/__init__.py +59 -0
  243. omnibase_infra/models/bindings/constants.py +144 -0
  244. omnibase_infra/models/bindings/model_binding_resolution_result.py +103 -0
  245. omnibase_infra/models/bindings/model_operation_binding.py +44 -0
  246. omnibase_infra/models/bindings/model_operation_bindings_subcontract.py +152 -0
  247. omnibase_infra/models/bindings/model_parsed_binding.py +52 -0
  248. omnibase_infra/models/corpus/__init__.py +17 -0
  249. omnibase_infra/models/corpus/model_capture_config.py +133 -0
  250. omnibase_infra/models/corpus/model_capture_result.py +86 -0
  251. omnibase_infra/models/discovery/__init__.py +42 -0
  252. omnibase_infra/models/discovery/model_dependency_spec.py +319 -0
  253. omnibase_infra/models/discovery/model_discovered_capabilities.py +50 -0
  254. omnibase_infra/models/discovery/model_introspection_config.py +330 -0
  255. omnibase_infra/models/discovery/model_introspection_performance_metrics.py +169 -0
  256. omnibase_infra/models/discovery/model_introspection_task_config.py +116 -0
  257. omnibase_infra/models/dispatch/__init__.py +155 -0
  258. omnibase_infra/models/dispatch/model_debug_trace_snapshot.py +114 -0
  259. omnibase_infra/models/dispatch/model_dispatch_context.py +439 -0
  260. omnibase_infra/models/dispatch/model_dispatch_error.py +336 -0
  261. omnibase_infra/models/dispatch/model_dispatch_log_context.py +400 -0
  262. omnibase_infra/models/dispatch/model_dispatch_metadata.py +228 -0
  263. omnibase_infra/models/dispatch/model_dispatch_metrics.py +496 -0
  264. omnibase_infra/models/dispatch/model_dispatch_outcome.py +317 -0
  265. omnibase_infra/models/dispatch/model_dispatch_outputs.py +231 -0
  266. omnibase_infra/models/dispatch/model_dispatch_result.py +436 -0
  267. omnibase_infra/models/dispatch/model_dispatch_route.py +279 -0
  268. omnibase_infra/models/dispatch/model_dispatcher_metrics.py +275 -0
  269. omnibase_infra/models/dispatch/model_dispatcher_registration.py +352 -0
  270. omnibase_infra/models/dispatch/model_materialized_dispatch.py +141 -0
  271. omnibase_infra/models/dispatch/model_parsed_topic.py +135 -0
  272. omnibase_infra/models/dispatch/model_topic_parser.py +725 -0
  273. omnibase_infra/models/dispatch/model_tracing_context.py +285 -0
  274. omnibase_infra/models/errors/__init__.py +45 -0
  275. omnibase_infra/models/errors/model_handler_validation_error.py +594 -0
  276. omnibase_infra/models/errors/model_infra_error_context.py +99 -0
  277. omnibase_infra/models/errors/model_message_type_registry_error_context.py +71 -0
  278. omnibase_infra/models/errors/model_timeout_error_context.py +110 -0
  279. omnibase_infra/models/handlers/__init__.py +80 -0
  280. omnibase_infra/models/handlers/model_bootstrap_handler_descriptor.py +162 -0
  281. omnibase_infra/models/handlers/model_contract_discovery_result.py +82 -0
  282. omnibase_infra/models/handlers/model_handler_descriptor.py +200 -0
  283. omnibase_infra/models/handlers/model_handler_identifier.py +215 -0
  284. omnibase_infra/models/handlers/model_handler_source_config.py +220 -0
  285. omnibase_infra/models/health/__init__.py +9 -0
  286. omnibase_infra/models/health/model_health_check_result.py +40 -0
  287. omnibase_infra/models/lifecycle/__init__.py +39 -0
  288. omnibase_infra/models/logging/__init__.py +51 -0
  289. omnibase_infra/models/logging/model_log_context.py +756 -0
  290. omnibase_infra/models/mcp/__init__.py +15 -0
  291. omnibase_infra/models/mcp/model_mcp_contract_config.py +80 -0
  292. omnibase_infra/models/mcp/model_mcp_server_config.py +67 -0
  293. omnibase_infra/models/mcp/model_mcp_tool_definition.py +73 -0
  294. omnibase_infra/models/mcp/model_mcp_tool_parameter.py +35 -0
  295. omnibase_infra/models/model_node_identity.py +126 -0
  296. omnibase_infra/models/model_retry_error_classification.py +78 -0
  297. omnibase_infra/models/projection/__init__.py +43 -0
  298. omnibase_infra/models/projection/model_capability_fields.py +112 -0
  299. omnibase_infra/models/projection/model_registration_projection.py +434 -0
  300. omnibase_infra/models/projection/model_registration_snapshot.py +322 -0
  301. omnibase_infra/models/projection/model_sequence_info.py +182 -0
  302. omnibase_infra/models/projection/model_snapshot_topic_config.py +591 -0
  303. omnibase_infra/models/projectors/__init__.py +41 -0
  304. omnibase_infra/models/projectors/model_projector_column.py +289 -0
  305. omnibase_infra/models/projectors/model_projector_discovery_result.py +65 -0
  306. omnibase_infra/models/projectors/model_projector_index.py +270 -0
  307. omnibase_infra/models/projectors/model_projector_schema.py +415 -0
  308. omnibase_infra/models/projectors/model_projector_validation_error.py +63 -0
  309. omnibase_infra/models/projectors/util_sql_identifiers.py +115 -0
  310. omnibase_infra/models/registration/__init__.py +68 -0
  311. omnibase_infra/models/registration/commands/__init__.py +15 -0
  312. omnibase_infra/models/registration/commands/model_node_registration_acked.py +108 -0
  313. omnibase_infra/models/registration/events/__init__.py +56 -0
  314. omnibase_infra/models/registration/events/model_node_became_active.py +103 -0
  315. omnibase_infra/models/registration/events/model_node_liveness_expired.py +103 -0
  316. omnibase_infra/models/registration/events/model_node_registration_accepted.py +98 -0
  317. omnibase_infra/models/registration/events/model_node_registration_ack_received.py +98 -0
  318. omnibase_infra/models/registration/events/model_node_registration_ack_timed_out.py +112 -0
  319. omnibase_infra/models/registration/events/model_node_registration_initiated.py +107 -0
  320. omnibase_infra/models/registration/events/model_node_registration_rejected.py +104 -0
  321. omnibase_infra/models/registration/model_event_bus_topic_entry.py +59 -0
  322. omnibase_infra/models/registration/model_introspection_metrics.py +253 -0
  323. omnibase_infra/models/registration/model_node_capabilities.py +190 -0
  324. omnibase_infra/models/registration/model_node_event_bus_config.py +99 -0
  325. omnibase_infra/models/registration/model_node_heartbeat_event.py +126 -0
  326. omnibase_infra/models/registration/model_node_introspection_event.py +195 -0
  327. omnibase_infra/models/registration/model_node_metadata.py +79 -0
  328. omnibase_infra/models/registration/model_node_registration.py +162 -0
  329. omnibase_infra/models/registration/model_node_registration_record.py +162 -0
  330. omnibase_infra/models/registry/__init__.py +29 -0
  331. omnibase_infra/models/registry/model_domain_constraint.py +202 -0
  332. omnibase_infra/models/registry/model_message_type_entry.py +271 -0
  333. omnibase_infra/models/resilience/__init__.py +9 -0
  334. omnibase_infra/models/resilience/model_circuit_breaker_config.py +227 -0
  335. omnibase_infra/models/routing/__init__.py +25 -0
  336. omnibase_infra/models/routing/model_routing_entry.py +52 -0
  337. omnibase_infra/models/routing/model_routing_subcontract.py +70 -0
  338. omnibase_infra/models/runtime/__init__.py +49 -0
  339. omnibase_infra/models/runtime/model_contract_security_config.py +41 -0
  340. omnibase_infra/models/runtime/model_discovery_error.py +81 -0
  341. omnibase_infra/models/runtime/model_discovery_result.py +162 -0
  342. omnibase_infra/models/runtime/model_discovery_warning.py +74 -0
  343. omnibase_infra/models/runtime/model_failed_plugin_load.py +63 -0
  344. omnibase_infra/models/runtime/model_handler_contract.py +296 -0
  345. omnibase_infra/models/runtime/model_loaded_handler.py +129 -0
  346. omnibase_infra/models/runtime/model_plugin_load_context.py +93 -0
  347. omnibase_infra/models/runtime/model_plugin_load_summary.py +124 -0
  348. omnibase_infra/models/security/__init__.py +50 -0
  349. omnibase_infra/models/security/classification_levels.py +99 -0
  350. omnibase_infra/models/security/model_environment_policy.py +145 -0
  351. omnibase_infra/models/security/model_handler_security_policy.py +107 -0
  352. omnibase_infra/models/security/model_security_error.py +81 -0
  353. omnibase_infra/models/security/model_security_validation_result.py +328 -0
  354. omnibase_infra/models/security/model_security_warning.py +67 -0
  355. omnibase_infra/models/snapshot/__init__.py +27 -0
  356. omnibase_infra/models/snapshot/model_field_change.py +65 -0
  357. omnibase_infra/models/snapshot/model_snapshot.py +270 -0
  358. omnibase_infra/models/snapshot/model_snapshot_diff.py +203 -0
  359. omnibase_infra/models/snapshot/model_subject_ref.py +81 -0
  360. omnibase_infra/models/types/__init__.py +71 -0
  361. omnibase_infra/models/validation/__init__.py +89 -0
  362. omnibase_infra/models/validation/model_any_type_validation_result.py +118 -0
  363. omnibase_infra/models/validation/model_any_type_violation.py +141 -0
  364. omnibase_infra/models/validation/model_category_match_result.py +345 -0
  365. omnibase_infra/models/validation/model_chain_violation.py +166 -0
  366. omnibase_infra/models/validation/model_coverage_metrics.py +316 -0
  367. omnibase_infra/models/validation/model_execution_shape_rule.py +159 -0
  368. omnibase_infra/models/validation/model_execution_shape_validation.py +208 -0
  369. omnibase_infra/models/validation/model_execution_shape_validation_result.py +294 -0
  370. omnibase_infra/models/validation/model_execution_shape_violation.py +122 -0
  371. omnibase_infra/models/validation/model_localhandler_validation_result.py +139 -0
  372. omnibase_infra/models/validation/model_localhandler_violation.py +100 -0
  373. omnibase_infra/models/validation/model_output_validation_params.py +74 -0
  374. omnibase_infra/models/validation/model_validate_and_raise_params.py +84 -0
  375. omnibase_infra/models/validation/model_validation_error_params.py +84 -0
  376. omnibase_infra/models/validation/model_validation_outcome.py +287 -0
  377. omnibase_infra/nodes/__init__.py +57 -0
  378. omnibase_infra/nodes/architecture_validator/__init__.py +79 -0
  379. omnibase_infra/nodes/architecture_validator/contract.yaml +252 -0
  380. omnibase_infra/nodes/architecture_validator/contract_architecture_validator.yaml +203 -0
  381. omnibase_infra/nodes/architecture_validator/mixins/__init__.py +16 -0
  382. omnibase_infra/nodes/architecture_validator/mixins/mixin_file_path_rule.py +92 -0
  383. omnibase_infra/nodes/architecture_validator/models/__init__.py +36 -0
  384. omnibase_infra/nodes/architecture_validator/models/model_architecture_validation_request.py +56 -0
  385. omnibase_infra/nodes/architecture_validator/models/model_architecture_validation_result.py +311 -0
  386. omnibase_infra/nodes/architecture_validator/models/model_architecture_violation.py +163 -0
  387. omnibase_infra/nodes/architecture_validator/models/model_rule_check_result.py +265 -0
  388. omnibase_infra/nodes/architecture_validator/models/model_validation_request.py +105 -0
  389. omnibase_infra/nodes/architecture_validator/models/model_validation_result.py +314 -0
  390. omnibase_infra/nodes/architecture_validator/node.py +262 -0
  391. omnibase_infra/nodes/architecture_validator/node_architecture_validator.py +383 -0
  392. omnibase_infra/nodes/architecture_validator/protocols/__init__.py +9 -0
  393. omnibase_infra/nodes/architecture_validator/protocols/protocol_architecture_rule.py +225 -0
  394. omnibase_infra/nodes/architecture_validator/registry/__init__.py +28 -0
  395. omnibase_infra/nodes/architecture_validator/registry/registry_infra_architecture_validator.py +106 -0
  396. omnibase_infra/nodes/architecture_validator/validators/__init__.py +104 -0
  397. omnibase_infra/nodes/architecture_validator/validators/validator_no_direct_dispatch.py +422 -0
  398. omnibase_infra/nodes/architecture_validator/validators/validator_no_handler_publishing.py +481 -0
  399. omnibase_infra/nodes/architecture_validator/validators/validator_no_orchestrator_fsm.py +491 -0
  400. omnibase_infra/nodes/contract_registry_reducer/__init__.py +29 -0
  401. omnibase_infra/nodes/contract_registry_reducer/contract.yaml +255 -0
  402. omnibase_infra/nodes/contract_registry_reducer/models/__init__.py +38 -0
  403. omnibase_infra/nodes/contract_registry_reducer/models/model_contract_registry_state.py +266 -0
  404. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_cleanup_topic_references.py +55 -0
  405. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_deactivate_contract.py +58 -0
  406. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_mark_stale.py +49 -0
  407. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_update_heartbeat.py +71 -0
  408. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_update_topic.py +66 -0
  409. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_upsert_contract.py +92 -0
  410. omnibase_infra/nodes/contract_registry_reducer/node.py +121 -0
  411. omnibase_infra/nodes/contract_registry_reducer/reducer.py +784 -0
  412. omnibase_infra/nodes/contract_registry_reducer/registry/__init__.py +9 -0
  413. omnibase_infra/nodes/contract_registry_reducer/registry/registry_infra_contract_registry_reducer.py +101 -0
  414. omnibase_infra/nodes/effects/README.md +358 -0
  415. omnibase_infra/nodes/effects/__init__.py +26 -0
  416. omnibase_infra/nodes/effects/contract.yaml +167 -0
  417. omnibase_infra/nodes/effects/models/__init__.py +32 -0
  418. omnibase_infra/nodes/effects/models/model_backend_result.py +190 -0
  419. omnibase_infra/nodes/effects/models/model_effect_idempotency_config.py +92 -0
  420. omnibase_infra/nodes/effects/models/model_registry_request.py +132 -0
  421. omnibase_infra/nodes/effects/models/model_registry_response.py +263 -0
  422. omnibase_infra/nodes/effects/protocol_consul_client.py +89 -0
  423. omnibase_infra/nodes/effects/protocol_effect_idempotency_store.py +143 -0
  424. omnibase_infra/nodes/effects/protocol_postgres_adapter.py +96 -0
  425. omnibase_infra/nodes/effects/registry_effect.py +525 -0
  426. omnibase_infra/nodes/effects/store_effect_idempotency_inmemory.py +425 -0
  427. omnibase_infra/nodes/handlers/consul/contract.yaml +85 -0
  428. omnibase_infra/nodes/handlers/db/contract.yaml +72 -0
  429. omnibase_infra/nodes/handlers/graph/contract.yaml +127 -0
  430. omnibase_infra/nodes/handlers/http/contract.yaml +74 -0
  431. omnibase_infra/nodes/handlers/intent/contract.yaml +66 -0
  432. omnibase_infra/nodes/handlers/mcp/contract.yaml +69 -0
  433. omnibase_infra/nodes/handlers/vault/contract.yaml +91 -0
  434. omnibase_infra/nodes/node_intent_storage_effect/__init__.py +50 -0
  435. omnibase_infra/nodes/node_intent_storage_effect/contract.yaml +194 -0
  436. omnibase_infra/nodes/node_intent_storage_effect/models/__init__.py +24 -0
  437. omnibase_infra/nodes/node_intent_storage_effect/models/model_intent_storage_input.py +141 -0
  438. omnibase_infra/nodes/node_intent_storage_effect/models/model_intent_storage_output.py +130 -0
  439. omnibase_infra/nodes/node_intent_storage_effect/node.py +94 -0
  440. omnibase_infra/nodes/node_intent_storage_effect/registry/__init__.py +35 -0
  441. omnibase_infra/nodes/node_intent_storage_effect/registry/registry_infra_intent_storage.py +294 -0
  442. omnibase_infra/nodes/node_ledger_projection_compute/__init__.py +50 -0
  443. omnibase_infra/nodes/node_ledger_projection_compute/contract.yaml +104 -0
  444. omnibase_infra/nodes/node_ledger_projection_compute/node.py +284 -0
  445. omnibase_infra/nodes/node_ledger_projection_compute/registry/__init__.py +29 -0
  446. omnibase_infra/nodes/node_ledger_projection_compute/registry/registry_infra_ledger_projection.py +118 -0
  447. omnibase_infra/nodes/node_ledger_write_effect/__init__.py +82 -0
  448. omnibase_infra/nodes/node_ledger_write_effect/contract.yaml +200 -0
  449. omnibase_infra/nodes/node_ledger_write_effect/handlers/__init__.py +22 -0
  450. omnibase_infra/nodes/node_ledger_write_effect/handlers/handler_ledger_append.py +372 -0
  451. omnibase_infra/nodes/node_ledger_write_effect/handlers/handler_ledger_query.py +597 -0
  452. omnibase_infra/nodes/node_ledger_write_effect/models/__init__.py +31 -0
  453. omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_append_result.py +54 -0
  454. omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_entry.py +92 -0
  455. omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_query.py +53 -0
  456. omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_query_result.py +41 -0
  457. omnibase_infra/nodes/node_ledger_write_effect/node.py +89 -0
  458. omnibase_infra/nodes/node_ledger_write_effect/protocols/__init__.py +13 -0
  459. omnibase_infra/nodes/node_ledger_write_effect/protocols/protocol_ledger_persistence.py +127 -0
  460. omnibase_infra/nodes/node_ledger_write_effect/registry/__init__.py +9 -0
  461. omnibase_infra/nodes/node_ledger_write_effect/registry/registry_infra_ledger_write.py +121 -0
  462. omnibase_infra/nodes/node_registration_orchestrator/README.md +542 -0
  463. omnibase_infra/nodes/node_registration_orchestrator/__init__.py +120 -0
  464. omnibase_infra/nodes/node_registration_orchestrator/contract.yaml +482 -0
  465. omnibase_infra/nodes/node_registration_orchestrator/dispatchers/__init__.py +53 -0
  466. omnibase_infra/nodes/node_registration_orchestrator/dispatchers/dispatcher_node_introspected.py +376 -0
  467. omnibase_infra/nodes/node_registration_orchestrator/dispatchers/dispatcher_node_registration_acked.py +376 -0
  468. omnibase_infra/nodes/node_registration_orchestrator/dispatchers/dispatcher_runtime_tick.py +373 -0
  469. omnibase_infra/nodes/node_registration_orchestrator/handlers/__init__.py +62 -0
  470. omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_heartbeat.py +376 -0
  471. omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_introspected.py +694 -0
  472. omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_registration_acked.py +458 -0
  473. omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_runtime_tick.py +364 -0
  474. omnibase_infra/nodes/node_registration_orchestrator/introspection_event_router.py +544 -0
  475. omnibase_infra/nodes/node_registration_orchestrator/models/__init__.py +75 -0
  476. omnibase_infra/nodes/node_registration_orchestrator/models/model_consul_intent_payload.py +194 -0
  477. omnibase_infra/nodes/node_registration_orchestrator/models/model_consul_registration_intent.py +67 -0
  478. omnibase_infra/nodes/node_registration_orchestrator/models/model_intent_execution_result.py +50 -0
  479. omnibase_infra/nodes/node_registration_orchestrator/models/model_node_liveness_expired.py +107 -0
  480. omnibase_infra/nodes/node_registration_orchestrator/models/model_orchestrator_config.py +67 -0
  481. omnibase_infra/nodes/node_registration_orchestrator/models/model_orchestrator_input.py +41 -0
  482. omnibase_infra/nodes/node_registration_orchestrator/models/model_orchestrator_output.py +166 -0
  483. omnibase_infra/nodes/node_registration_orchestrator/models/model_postgres_intent_payload.py +235 -0
  484. omnibase_infra/nodes/node_registration_orchestrator/models/model_postgres_upsert_intent.py +68 -0
  485. omnibase_infra/nodes/node_registration_orchestrator/models/model_reducer_execution_result.py +384 -0
  486. omnibase_infra/nodes/node_registration_orchestrator/models/model_reducer_state.py +60 -0
  487. omnibase_infra/nodes/node_registration_orchestrator/models/model_registration_intent.py +177 -0
  488. omnibase_infra/nodes/node_registration_orchestrator/models/model_registry_intent.py +247 -0
  489. omnibase_infra/nodes/node_registration_orchestrator/node.py +195 -0
  490. omnibase_infra/nodes/node_registration_orchestrator/plugin.py +909 -0
  491. omnibase_infra/nodes/node_registration_orchestrator/protocols.py +439 -0
  492. omnibase_infra/nodes/node_registration_orchestrator/registry/__init__.py +41 -0
  493. omnibase_infra/nodes/node_registration_orchestrator/registry/registry_infra_node_registration_orchestrator.py +528 -0
  494. omnibase_infra/nodes/node_registration_orchestrator/timeout_coordinator.py +393 -0
  495. omnibase_infra/nodes/node_registration_orchestrator/wiring.py +743 -0
  496. omnibase_infra/nodes/node_registration_reducer/__init__.py +15 -0
  497. omnibase_infra/nodes/node_registration_reducer/contract.yaml +301 -0
  498. omnibase_infra/nodes/node_registration_reducer/models/__init__.py +38 -0
  499. omnibase_infra/nodes/node_registration_reducer/models/model_validation_result.py +113 -0
  500. omnibase_infra/nodes/node_registration_reducer/node.py +139 -0
  501. omnibase_infra/nodes/node_registration_reducer/registry/__init__.py +9 -0
  502. omnibase_infra/nodes/node_registration_reducer/registry/registry_infra_node_registration_reducer.py +79 -0
  503. omnibase_infra/nodes/node_registration_storage_effect/__init__.py +41 -0
  504. omnibase_infra/nodes/node_registration_storage_effect/contract.yaml +220 -0
  505. omnibase_infra/nodes/node_registration_storage_effect/models/__init__.py +44 -0
  506. omnibase_infra/nodes/node_registration_storage_effect/models/model_delete_result.py +132 -0
  507. omnibase_infra/nodes/node_registration_storage_effect/models/model_registration_record.py +199 -0
  508. omnibase_infra/nodes/node_registration_storage_effect/models/model_registration_update.py +155 -0
  509. omnibase_infra/nodes/node_registration_storage_effect/models/model_storage_health_check_details.py +123 -0
  510. omnibase_infra/nodes/node_registration_storage_effect/models/model_storage_health_check_result.py +117 -0
  511. omnibase_infra/nodes/node_registration_storage_effect/models/model_storage_query.py +100 -0
  512. omnibase_infra/nodes/node_registration_storage_effect/models/model_storage_result.py +136 -0
  513. omnibase_infra/nodes/node_registration_storage_effect/models/model_upsert_result.py +127 -0
  514. omnibase_infra/nodes/node_registration_storage_effect/node.py +112 -0
  515. omnibase_infra/nodes/node_registration_storage_effect/protocols/__init__.py +22 -0
  516. omnibase_infra/nodes/node_registration_storage_effect/protocols/protocol_registration_persistence.py +333 -0
  517. omnibase_infra/nodes/node_registration_storage_effect/registry/__init__.py +23 -0
  518. omnibase_infra/nodes/node_registration_storage_effect/registry/registry_infra_registration_storage.py +215 -0
  519. omnibase_infra/nodes/node_registry_effect/__init__.py +85 -0
  520. omnibase_infra/nodes/node_registry_effect/contract.yaml +677 -0
  521. omnibase_infra/nodes/node_registry_effect/handlers/__init__.py +70 -0
  522. omnibase_infra/nodes/node_registry_effect/handlers/handler_consul_deregister.py +211 -0
  523. omnibase_infra/nodes/node_registry_effect/handlers/handler_consul_register.py +212 -0
  524. omnibase_infra/nodes/node_registry_effect/handlers/handler_partial_retry.py +417 -0
  525. omnibase_infra/nodes/node_registry_effect/handlers/handler_postgres_deactivate.py +215 -0
  526. omnibase_infra/nodes/node_registry_effect/handlers/handler_postgres_upsert.py +208 -0
  527. omnibase_infra/nodes/node_registry_effect/models/__init__.py +43 -0
  528. omnibase_infra/nodes/node_registry_effect/models/model_partial_retry_request.py +92 -0
  529. omnibase_infra/nodes/node_registry_effect/node.py +165 -0
  530. omnibase_infra/nodes/node_registry_effect/registry/__init__.py +27 -0
  531. omnibase_infra/nodes/node_registry_effect/registry/registry_infra_registry_effect.py +196 -0
  532. omnibase_infra/nodes/node_service_discovery_effect/__init__.py +111 -0
  533. omnibase_infra/nodes/node_service_discovery_effect/contract.yaml +246 -0
  534. omnibase_infra/nodes/node_service_discovery_effect/models/__init__.py +67 -0
  535. omnibase_infra/nodes/node_service_discovery_effect/models/enum_health_status.py +72 -0
  536. omnibase_infra/nodes/node_service_discovery_effect/models/enum_service_discovery_operation.py +58 -0
  537. omnibase_infra/nodes/node_service_discovery_effect/models/model_discovery_query.py +99 -0
  538. omnibase_infra/nodes/node_service_discovery_effect/models/model_discovery_result.py +98 -0
  539. omnibase_infra/nodes/node_service_discovery_effect/models/model_health_check_config.py +121 -0
  540. omnibase_infra/nodes/node_service_discovery_effect/models/model_query_metadata.py +63 -0
  541. omnibase_infra/nodes/node_service_discovery_effect/models/model_registration_result.py +130 -0
  542. omnibase_infra/nodes/node_service_discovery_effect/models/model_service_discovery_health_check_details.py +111 -0
  543. omnibase_infra/nodes/node_service_discovery_effect/models/model_service_discovery_health_check_result.py +119 -0
  544. omnibase_infra/nodes/node_service_discovery_effect/models/model_service_info.py +106 -0
  545. omnibase_infra/nodes/node_service_discovery_effect/models/model_service_registration.py +121 -0
  546. omnibase_infra/nodes/node_service_discovery_effect/node.py +111 -0
  547. omnibase_infra/nodes/node_service_discovery_effect/protocols/__init__.py +14 -0
  548. omnibase_infra/nodes/node_service_discovery_effect/protocols/protocol_discovery_operations.py +279 -0
  549. omnibase_infra/nodes/node_service_discovery_effect/registry/__init__.py +13 -0
  550. omnibase_infra/nodes/node_service_discovery_effect/registry/registry_infra_service_discovery.py +222 -0
  551. omnibase_infra/nodes/reducers/__init__.py +30 -0
  552. omnibase_infra/nodes/reducers/models/__init__.py +37 -0
  553. omnibase_infra/nodes/reducers/models/model_payload_consul_register.py +87 -0
  554. omnibase_infra/nodes/reducers/models/model_payload_ledger_append.py +133 -0
  555. omnibase_infra/nodes/reducers/models/model_payload_postgres_upsert_registration.py +60 -0
  556. omnibase_infra/nodes/reducers/models/model_registration_confirmation.py +166 -0
  557. omnibase_infra/nodes/reducers/models/model_registration_state.py +433 -0
  558. omnibase_infra/nodes/reducers/registration_reducer.py +1138 -0
  559. omnibase_infra/observability/__init__.py +143 -0
  560. omnibase_infra/observability/constants_metrics.py +91 -0
  561. omnibase_infra/observability/factory_observability_sink.py +525 -0
  562. omnibase_infra/observability/handlers/__init__.py +118 -0
  563. omnibase_infra/observability/handlers/handler_logging_structured.py +967 -0
  564. omnibase_infra/observability/handlers/handler_metrics_prometheus.py +1120 -0
  565. omnibase_infra/observability/handlers/model_logging_handler_config.py +71 -0
  566. omnibase_infra/observability/handlers/model_logging_handler_response.py +77 -0
  567. omnibase_infra/observability/handlers/model_metrics_handler_config.py +172 -0
  568. omnibase_infra/observability/handlers/model_metrics_handler_payload.py +135 -0
  569. omnibase_infra/observability/handlers/model_metrics_handler_response.py +101 -0
  570. omnibase_infra/observability/hooks/__init__.py +74 -0
  571. omnibase_infra/observability/hooks/hook_observability.py +1223 -0
  572. omnibase_infra/observability/models/__init__.py +30 -0
  573. omnibase_infra/observability/models/enum_required_log_context_key.py +77 -0
  574. omnibase_infra/observability/models/model_buffered_log_entry.py +117 -0
  575. omnibase_infra/observability/models/model_logging_sink_config.py +73 -0
  576. omnibase_infra/observability/models/model_metrics_sink_config.py +156 -0
  577. omnibase_infra/observability/sinks/__init__.py +69 -0
  578. omnibase_infra/observability/sinks/sink_logging_structured.py +809 -0
  579. omnibase_infra/observability/sinks/sink_metrics_prometheus.py +710 -0
  580. omnibase_infra/plugins/__init__.py +27 -0
  581. omnibase_infra/plugins/examples/__init__.py +28 -0
  582. omnibase_infra/plugins/examples/plugin_json_normalizer.py +271 -0
  583. omnibase_infra/plugins/examples/plugin_json_normalizer_error_handling.py +210 -0
  584. omnibase_infra/plugins/models/__init__.py +21 -0
  585. omnibase_infra/plugins/models/model_plugin_context.py +76 -0
  586. omnibase_infra/plugins/models/model_plugin_input_data.py +58 -0
  587. omnibase_infra/plugins/models/model_plugin_output_data.py +62 -0
  588. omnibase_infra/plugins/plugin_compute_base.py +449 -0
  589. omnibase_infra/projectors/__init__.py +30 -0
  590. omnibase_infra/projectors/contracts/__init__.py +63 -0
  591. omnibase_infra/projectors/contracts/registration_projector.yaml +370 -0
  592. omnibase_infra/projectors/projection_reader_registration.py +1559 -0
  593. omnibase_infra/projectors/snapshot_publisher_registration.py +1329 -0
  594. omnibase_infra/protocols/__init__.py +104 -0
  595. omnibase_infra/protocols/protocol_capability_projection.py +253 -0
  596. omnibase_infra/protocols/protocol_capability_query.py +251 -0
  597. omnibase_infra/protocols/protocol_container_aware.py +200 -0
  598. omnibase_infra/protocols/protocol_dispatch_engine.py +152 -0
  599. omnibase_infra/protocols/protocol_event_bus_like.py +127 -0
  600. omnibase_infra/protocols/protocol_event_projector.py +96 -0
  601. omnibase_infra/protocols/protocol_idempotency_store.py +142 -0
  602. omnibase_infra/protocols/protocol_message_dispatcher.py +247 -0
  603. omnibase_infra/protocols/protocol_message_type_registry.py +306 -0
  604. omnibase_infra/protocols/protocol_plugin_compute.py +368 -0
  605. omnibase_infra/protocols/protocol_projector_schema_validator.py +82 -0
  606. omnibase_infra/protocols/protocol_registry_metrics.py +215 -0
  607. omnibase_infra/protocols/protocol_snapshot_publisher.py +396 -0
  608. omnibase_infra/protocols/protocol_snapshot_store.py +567 -0
  609. omnibase_infra/runtime/__init__.py +445 -0
  610. omnibase_infra/runtime/binding_config_resolver.py +2771 -0
  611. omnibase_infra/runtime/binding_resolver.py +753 -0
  612. omnibase_infra/runtime/chain_aware_dispatch.py +467 -0
  613. omnibase_infra/runtime/constants_notification.py +75 -0
  614. omnibase_infra/runtime/constants_security.py +70 -0
  615. omnibase_infra/runtime/contract_handler_discovery.py +587 -0
  616. omnibase_infra/runtime/contract_loaders/__init__.py +51 -0
  617. omnibase_infra/runtime/contract_loaders/handler_routing_loader.py +464 -0
  618. omnibase_infra/runtime/contract_loaders/operation_bindings_loader.py +789 -0
  619. omnibase_infra/runtime/dispatch_context_enforcer.py +427 -0
  620. omnibase_infra/runtime/emit_daemon/__init__.py +97 -0
  621. omnibase_infra/runtime/emit_daemon/cli.py +844 -0
  622. omnibase_infra/runtime/emit_daemon/client.py +811 -0
  623. omnibase_infra/runtime/emit_daemon/config.py +535 -0
  624. omnibase_infra/runtime/emit_daemon/daemon.py +812 -0
  625. omnibase_infra/runtime/emit_daemon/event_registry.py +477 -0
  626. omnibase_infra/runtime/emit_daemon/model_daemon_request.py +139 -0
  627. omnibase_infra/runtime/emit_daemon/model_daemon_response.py +191 -0
  628. omnibase_infra/runtime/emit_daemon/queue.py +618 -0
  629. omnibase_infra/runtime/enums/__init__.py +18 -0
  630. omnibase_infra/runtime/enums/enum_config_ref_scheme.py +33 -0
  631. omnibase_infra/runtime/enums/enum_scheduler_status.py +170 -0
  632. omnibase_infra/runtime/envelope_validator.py +179 -0
  633. omnibase_infra/runtime/event_bus_subcontract_wiring.py +466 -0
  634. omnibase_infra/runtime/handler_bootstrap_source.py +507 -0
  635. omnibase_infra/runtime/handler_contract_config_loader.py +603 -0
  636. omnibase_infra/runtime/handler_contract_source.py +750 -0
  637. omnibase_infra/runtime/handler_identity.py +81 -0
  638. omnibase_infra/runtime/handler_plugin_loader.py +2046 -0
  639. omnibase_infra/runtime/handler_registry.py +329 -0
  640. omnibase_infra/runtime/handler_source_resolver.py +367 -0
  641. omnibase_infra/runtime/invocation_security_enforcer.py +427 -0
  642. omnibase_infra/runtime/kafka_contract_source.py +984 -0
  643. omnibase_infra/runtime/kernel.py +40 -0
  644. omnibase_infra/runtime/mixin_policy_validation.py +522 -0
  645. omnibase_infra/runtime/mixin_semver_cache.py +402 -0
  646. omnibase_infra/runtime/mixins/__init__.py +24 -0
  647. omnibase_infra/runtime/mixins/mixin_projector_notification_publishing.py +566 -0
  648. omnibase_infra/runtime/mixins/mixin_projector_sql_operations.py +778 -0
  649. omnibase_infra/runtime/models/__init__.py +229 -0
  650. omnibase_infra/runtime/models/model_batch_lifecycle_result.py +217 -0
  651. omnibase_infra/runtime/models/model_binding_config.py +168 -0
  652. omnibase_infra/runtime/models/model_binding_config_cache_stats.py +135 -0
  653. omnibase_infra/runtime/models/model_binding_config_resolver_config.py +329 -0
  654. omnibase_infra/runtime/models/model_cached_secret.py +138 -0
  655. omnibase_infra/runtime/models/model_compute_key.py +138 -0
  656. omnibase_infra/runtime/models/model_compute_registration.py +97 -0
  657. omnibase_infra/runtime/models/model_config_cache_entry.py +61 -0
  658. omnibase_infra/runtime/models/model_config_ref.py +331 -0
  659. omnibase_infra/runtime/models/model_config_ref_parse_result.py +125 -0
  660. omnibase_infra/runtime/models/model_contract_load_result.py +224 -0
  661. omnibase_infra/runtime/models/model_domain_plugin_config.py +92 -0
  662. omnibase_infra/runtime/models/model_domain_plugin_result.py +270 -0
  663. omnibase_infra/runtime/models/model_duplicate_response.py +54 -0
  664. omnibase_infra/runtime/models/model_enabled_protocols_config.py +61 -0
  665. omnibase_infra/runtime/models/model_event_bus_config.py +54 -0
  666. omnibase_infra/runtime/models/model_failed_component.py +55 -0
  667. omnibase_infra/runtime/models/model_health_check_response.py +168 -0
  668. omnibase_infra/runtime/models/model_health_check_result.py +229 -0
  669. omnibase_infra/runtime/models/model_lifecycle_result.py +245 -0
  670. omnibase_infra/runtime/models/model_logging_config.py +42 -0
  671. omnibase_infra/runtime/models/model_optional_correlation_id.py +167 -0
  672. omnibase_infra/runtime/models/model_optional_string.py +94 -0
  673. omnibase_infra/runtime/models/model_optional_uuid.py +110 -0
  674. omnibase_infra/runtime/models/model_policy_context.py +100 -0
  675. omnibase_infra/runtime/models/model_policy_key.py +138 -0
  676. omnibase_infra/runtime/models/model_policy_registration.py +139 -0
  677. omnibase_infra/runtime/models/model_policy_result.py +103 -0
  678. omnibase_infra/runtime/models/model_policy_type_filter.py +157 -0
  679. omnibase_infra/runtime/models/model_projector_notification_config.py +171 -0
  680. omnibase_infra/runtime/models/model_projector_plugin_loader_config.py +47 -0
  681. omnibase_infra/runtime/models/model_protocol_registration_config.py +65 -0
  682. omnibase_infra/runtime/models/model_retry_policy.py +105 -0
  683. omnibase_infra/runtime/models/model_runtime_config.py +150 -0
  684. omnibase_infra/runtime/models/model_runtime_contract_config.py +268 -0
  685. omnibase_infra/runtime/models/model_runtime_scheduler_config.py +625 -0
  686. omnibase_infra/runtime/models/model_runtime_scheduler_metrics.py +233 -0
  687. omnibase_infra/runtime/models/model_runtime_tick.py +193 -0
  688. omnibase_infra/runtime/models/model_secret_cache_stats.py +82 -0
  689. omnibase_infra/runtime/models/model_secret_mapping.py +63 -0
  690. omnibase_infra/runtime/models/model_secret_resolver_config.py +107 -0
  691. omnibase_infra/runtime/models/model_secret_resolver_metrics.py +111 -0
  692. omnibase_infra/runtime/models/model_secret_source_info.py +72 -0
  693. omnibase_infra/runtime/models/model_secret_source_spec.py +66 -0
  694. omnibase_infra/runtime/models/model_security_config.py +109 -0
  695. omnibase_infra/runtime/models/model_shutdown_batch_result.py +75 -0
  696. omnibase_infra/runtime/models/model_shutdown_config.py +94 -0
  697. omnibase_infra/runtime/models/model_transition_notification_outbox_config.py +112 -0
  698. omnibase_infra/runtime/models/model_transition_notification_outbox_metrics.py +140 -0
  699. omnibase_infra/runtime/models/model_transition_notification_publisher_metrics.py +357 -0
  700. omnibase_infra/runtime/projector_plugin_loader.py +1462 -0
  701. omnibase_infra/runtime/projector_schema_manager.py +565 -0
  702. omnibase_infra/runtime/projector_shell.py +1330 -0
  703. omnibase_infra/runtime/protocol_contract_descriptor.py +92 -0
  704. omnibase_infra/runtime/protocol_contract_source.py +92 -0
  705. omnibase_infra/runtime/protocol_domain_plugin.py +474 -0
  706. omnibase_infra/runtime/protocol_handler_discovery.py +221 -0
  707. omnibase_infra/runtime/protocol_handler_plugin_loader.py +327 -0
  708. omnibase_infra/runtime/protocol_lifecycle_executor.py +435 -0
  709. omnibase_infra/runtime/protocol_policy.py +366 -0
  710. omnibase_infra/runtime/protocols/__init__.py +37 -0
  711. omnibase_infra/runtime/protocols/protocol_runtime_scheduler.py +468 -0
  712. omnibase_infra/runtime/publisher_topic_scoped.py +294 -0
  713. omnibase_infra/runtime/registry/__init__.py +93 -0
  714. omnibase_infra/runtime/registry/mixin_message_type_query.py +326 -0
  715. omnibase_infra/runtime/registry/mixin_message_type_registration.py +354 -0
  716. omnibase_infra/runtime/registry/registry_event_bus_binding.py +268 -0
  717. omnibase_infra/runtime/registry/registry_message_type.py +542 -0
  718. omnibase_infra/runtime/registry/registry_protocol_binding.py +445 -0
  719. omnibase_infra/runtime/registry_compute.py +1143 -0
  720. omnibase_infra/runtime/registry_contract_source.py +693 -0
  721. omnibase_infra/runtime/registry_dispatcher.py +678 -0
  722. omnibase_infra/runtime/registry_policy.py +1185 -0
  723. omnibase_infra/runtime/runtime_contract_config_loader.py +406 -0
  724. omnibase_infra/runtime/runtime_scheduler.py +1070 -0
  725. omnibase_infra/runtime/secret_resolver.py +2112 -0
  726. omnibase_infra/runtime/security_metadata_validator.py +776 -0
  727. omnibase_infra/runtime/service_kernel.py +1651 -0
  728. omnibase_infra/runtime/service_message_dispatch_engine.py +2350 -0
  729. omnibase_infra/runtime/service_runtime_host_process.py +3493 -0
  730. omnibase_infra/runtime/transition_notification_outbox.py +1190 -0
  731. omnibase_infra/runtime/transition_notification_publisher.py +765 -0
  732. omnibase_infra/runtime/util_container_wiring.py +1124 -0
  733. omnibase_infra/runtime/util_validation.py +314 -0
  734. omnibase_infra/runtime/util_version.py +98 -0
  735. omnibase_infra/runtime/util_wiring.py +723 -0
  736. omnibase_infra/schemas/schema_registration_projection.sql +320 -0
  737. omnibase_infra/schemas/schema_transition_notification_outbox.sql +245 -0
  738. omnibase_infra/services/__init__.py +89 -0
  739. omnibase_infra/services/corpus_capture.py +684 -0
  740. omnibase_infra/services/mcp/__init__.py +31 -0
  741. omnibase_infra/services/mcp/mcp_server_lifecycle.py +449 -0
  742. omnibase_infra/services/mcp/service_mcp_tool_discovery.py +411 -0
  743. omnibase_infra/services/mcp/service_mcp_tool_registry.py +329 -0
  744. omnibase_infra/services/mcp/service_mcp_tool_sync.py +565 -0
  745. omnibase_infra/services/registry_api/__init__.py +40 -0
  746. omnibase_infra/services/registry_api/main.py +261 -0
  747. omnibase_infra/services/registry_api/models/__init__.py +66 -0
  748. omnibase_infra/services/registry_api/models/model_capability_widget_mapping.py +38 -0
  749. omnibase_infra/services/registry_api/models/model_pagination_info.py +48 -0
  750. omnibase_infra/services/registry_api/models/model_registry_discovery_response.py +73 -0
  751. omnibase_infra/services/registry_api/models/model_registry_health_response.py +49 -0
  752. omnibase_infra/services/registry_api/models/model_registry_instance_view.py +88 -0
  753. omnibase_infra/services/registry_api/models/model_registry_node_view.py +88 -0
  754. omnibase_infra/services/registry_api/models/model_registry_summary.py +60 -0
  755. omnibase_infra/services/registry_api/models/model_response_list_instances.py +43 -0
  756. omnibase_infra/services/registry_api/models/model_response_list_nodes.py +51 -0
  757. omnibase_infra/services/registry_api/models/model_warning.py +49 -0
  758. omnibase_infra/services/registry_api/models/model_widget_defaults.py +28 -0
  759. omnibase_infra/services/registry_api/models/model_widget_mapping.py +51 -0
  760. omnibase_infra/services/registry_api/routes.py +371 -0
  761. omnibase_infra/services/registry_api/service.py +837 -0
  762. omnibase_infra/services/service_capability_query.py +945 -0
  763. omnibase_infra/services/service_health.py +898 -0
  764. omnibase_infra/services/service_node_selector.py +530 -0
  765. omnibase_infra/services/service_timeout_emitter.py +699 -0
  766. omnibase_infra/services/service_timeout_scanner.py +394 -0
  767. omnibase_infra/services/session/__init__.py +56 -0
  768. omnibase_infra/services/session/config_consumer.py +137 -0
  769. omnibase_infra/services/session/config_store.py +139 -0
  770. omnibase_infra/services/session/consumer.py +1007 -0
  771. omnibase_infra/services/session/protocol_session_aggregator.py +117 -0
  772. omnibase_infra/services/session/store.py +997 -0
  773. omnibase_infra/services/snapshot/__init__.py +31 -0
  774. omnibase_infra/services/snapshot/service_snapshot.py +647 -0
  775. omnibase_infra/services/snapshot/store_inmemory.py +637 -0
  776. omnibase_infra/services/snapshot/store_postgres.py +1279 -0
  777. omnibase_infra/shared/__init__.py +8 -0
  778. omnibase_infra/testing/__init__.py +10 -0
  779. omnibase_infra/testing/utils.py +23 -0
  780. omnibase_infra/topics/__init__.py +45 -0
  781. omnibase_infra/topics/platform_topic_suffixes.py +140 -0
  782. omnibase_infra/topics/util_topic_composition.py +95 -0
  783. omnibase_infra/types/__init__.py +48 -0
  784. omnibase_infra/types/type_cache_info.py +49 -0
  785. omnibase_infra/types/type_dsn.py +173 -0
  786. omnibase_infra/types/type_infra_aliases.py +60 -0
  787. omnibase_infra/types/typed_dict/__init__.py +29 -0
  788. omnibase_infra/types/typed_dict/typed_dict_envelope_build_params.py +115 -0
  789. omnibase_infra/types/typed_dict/typed_dict_introspection_cache.py +128 -0
  790. omnibase_infra/types/typed_dict/typed_dict_performance_metrics_cache.py +140 -0
  791. omnibase_infra/types/typed_dict_capabilities.py +64 -0
  792. omnibase_infra/utils/__init__.py +117 -0
  793. omnibase_infra/utils/correlation.py +208 -0
  794. omnibase_infra/utils/util_atomic_file.py +261 -0
  795. omnibase_infra/utils/util_consumer_group.py +232 -0
  796. omnibase_infra/utils/util_datetime.py +372 -0
  797. omnibase_infra/utils/util_db_transaction.py +239 -0
  798. omnibase_infra/utils/util_dsn_validation.py +333 -0
  799. omnibase_infra/utils/util_env_parsing.py +264 -0
  800. omnibase_infra/utils/util_error_sanitization.py +457 -0
  801. omnibase_infra/utils/util_pydantic_validators.py +477 -0
  802. omnibase_infra/utils/util_retry_optimistic.py +281 -0
  803. omnibase_infra/utils/util_semver.py +233 -0
  804. omnibase_infra/validation/__init__.py +307 -0
  805. omnibase_infra/validation/contracts/security.validation.yaml +114 -0
  806. omnibase_infra/validation/enums/__init__.py +11 -0
  807. omnibase_infra/validation/enums/enum_contract_violation_severity.py +13 -0
  808. omnibase_infra/validation/infra_validators.py +1514 -0
  809. omnibase_infra/validation/linter_contract.py +907 -0
  810. omnibase_infra/validation/mixin_any_type_classification.py +120 -0
  811. omnibase_infra/validation/mixin_any_type_exemption.py +580 -0
  812. omnibase_infra/validation/mixin_any_type_reporting.py +106 -0
  813. omnibase_infra/validation/mixin_execution_shape_violation_checks.py +596 -0
  814. omnibase_infra/validation/mixin_node_archetype_detection.py +254 -0
  815. omnibase_infra/validation/models/__init__.py +15 -0
  816. omnibase_infra/validation/models/model_contract_lint_result.py +101 -0
  817. omnibase_infra/validation/models/model_contract_violation.py +41 -0
  818. omnibase_infra/validation/service_validation_aggregator.py +395 -0
  819. omnibase_infra/validation/validation_exemptions.yaml +2033 -0
  820. omnibase_infra/validation/validator_any_type.py +715 -0
  821. omnibase_infra/validation/validator_chain_propagation.py +839 -0
  822. omnibase_infra/validation/validator_execution_shape.py +465 -0
  823. omnibase_infra/validation/validator_localhandler.py +261 -0
  824. omnibase_infra/validation/validator_registration_security.py +410 -0
  825. omnibase_infra/validation/validator_routing_coverage.py +1020 -0
  826. omnibase_infra/validation/validator_runtime_shape.py +915 -0
  827. omnibase_infra/validation/validator_security.py +513 -0
  828. omnibase_infra/validation/validator_topic_category.py +1152 -0
  829. omnibase_infra-0.2.6.dist-info/METADATA +197 -0
  830. omnibase_infra-0.2.6.dist-info/RECORD +833 -0
  831. omnibase_infra-0.2.6.dist-info/WHEEL +4 -0
  832. omnibase_infra-0.2.6.dist-info/entry_points.txt +5 -0
  833. omnibase_infra-0.2.6.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,2350 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2025 OmniNode Team
3
+ # ruff: noqa: TRY400
4
+ # TRY400 disabled: logger.error is intentional to avoid leaking sensitive data in stack traces
5
+ """
6
+ Message Dispatch Engine.
7
+
8
+ Runtime dispatch engine for routing messages based on topic category and
9
+ message type. Routes incoming messages to registered dispatchers and collects
10
+ dispatcher outputs for publishing.
11
+
12
+ Design Principles:
13
+ - **Pure Routing**: Routes messages to dispatchers, no workflow inference
14
+ - **Deterministic**: Same input always produces same dispatcher selection
15
+ - **Fan-out Support**: Multiple dispatchers can process the same message type
16
+ - **Freeze-After-Init**: Thread-safe after registration phase completes
17
+ - **Observable**: Structured logging and comprehensive metrics
18
+
19
+ Architecture:
20
+ The dispatch engine provides:
21
+ - Route registration for topic pattern matching
22
+ - Dispatcher registration by category and message type
23
+ - Message dispatch with category validation
24
+ - Metrics collection for observability
25
+ - Structured logging for debugging and monitoring
26
+
27
+ It does NOT:
28
+ - Infer workflow semantics from message content
29
+ - Manage dispatcher lifecycle (dispatchers are external)
30
+ - Perform message transformation or enrichment
31
+ - Make decisions about message ordering or priority
32
+
33
+ Data Flow:
34
+ ```
35
+ +------------------------------------------------------------------+
36
+ | Message Dispatch Engine |
37
+ +------------------------------------------------------------------+
38
+ | |
39
+ | 1. Parse Topic 2. Validate 3. Match Dispatchers |
40
+ | | | | |
41
+ | | topic string | category match | |
42
+ | |-------------------|----------------------| |
43
+ | | | | |
44
+ | | EnumMessageCategory | dispatchers[]|
45
+ | |<------------------| |------------>|
46
+ | | | | |
47
+ | 4. Execute Dispatchers 5. Collect Outputs 6. Return Result |
48
+ | | | | |
49
+ | | dispatcher outputs| aggregate | |
50
+ | |-------------------|----------------------| |
51
+ | | | | |
52
+ | | | ModelDispatchResult | |
53
+ | |<------------------|<---------------------| |
54
+ | |
55
+ +------------------------------------------------------------------+
56
+ ```
57
+
58
+ Thread Safety:
59
+ MessageDispatchEngine follows the "freeze after init" pattern:
60
+
61
+ 1. **Registration Phase** (single-threaded): Register routes and dispatchers
62
+ 2. **Freeze**: Call freeze() to prevent further modifications
63
+ 3. **Dispatch Phase** (multi-threaded safe): Route messages to dispatchers
64
+
65
+ After freeze(), the engine becomes read-only and can be safely shared
66
+ across threads for concurrent dispatch operations.
67
+
68
+ **Metrics Thread Safety (TOCTOU Prevention)**:
69
+ A core design goal of the metrics system is preventing TOCTOU (time-of-check-
70
+ to-time-of-use) race conditions. Without proper synchronization, concurrent
71
+ dispatch operations could:
72
+
73
+ 1. Read current metrics state (check)
74
+ 2. Compute new values based on that state
75
+ 3. Write updated values (use)
76
+
77
+ If another thread modifies the state between steps 1 and 3, the final write
78
+ would clobber that concurrent update, causing lost increments or corrupted
79
+ aggregations.
80
+
81
+ **Solution**: All read-modify-write operations on ``_structured_metrics`` are
82
+ performed atomically within a single ``_metrics_lock`` acquisition. This
83
+ ensures that the sequence (read → compute → write) completes without
84
+ interleaving from other threads.
85
+
86
+ **Why holding the lock during computation is acceptable**:
87
+ The computations within the lock (``record_execution()``, ``model_copy()``)
88
+ are:
89
+
90
+ - **Pure**: No I/O, no external calls, no blocking operations
91
+ - **Fast**: Simple arithmetic and Pydantic model copying (~microseconds)
92
+ - **Bounded**: Fixed computational complexity regardless of data size
93
+
94
+ The lock is NEVER held during I/O operations (dispatcher execution), ensuring
95
+ that slow dispatchers do not block metrics updates in other threads.
96
+
97
+ For production monitoring, use ``get_structured_metrics()`` which returns
98
+ a consistent snapshot.
99
+
100
+ Related:
101
+ - OMN-934: Message dispatch engine implementation
102
+ - EnvelopeRouter: Transport-agnostic orchestrator (reference for freeze pattern)
103
+
104
+ Category Support:
105
+ The engine supports three ONEX message categories for routing:
106
+ - EVENT: Domain events (e.g., UserCreatedEvent)
107
+ - COMMAND: Action requests (e.g., CreateUserCommand)
108
+ - INTENT: User intentions (e.g., ProvisionUserIntent)
109
+
110
+ Topic naming constraints:
111
+ - EVENT topics: Must contain ".events" segment
112
+ - COMMAND topics: Must contain ".commands" segment
113
+ - INTENT topics: Must contain ".intents" segment
114
+
115
+ Note on PROJECTION:
116
+ PROJECTION is NOT a message category for routing. Projections are
117
+ node output types (EnumNodeOutputType.PROJECTION) produced by REDUCER
118
+ nodes as local state outputs. Projections are:
119
+ - NOT routed via Kafka topics
120
+ - NOT part of EnumMessageCategory
121
+ - Applied locally by the runtime to a projection sink
122
+
123
+ See EnumNodeOutputType for projection semantics and CLAUDE.md
124
+ "Enum Usage" section for the distinction between message categories
125
+ and node output types.
126
+
127
+ .. versionadded:: 0.4.0
128
+ """
129
+
130
+ from __future__ import annotations
131
+
132
+ __all__ = ["MessageDispatchEngine"]
133
+
134
+ import asyncio
135
+ import inspect
136
+ import logging
137
+ import threading
138
+ import time
139
+ from collections.abc import Awaitable, Callable
140
+ from datetime import UTC, datetime
141
+ from typing import TYPE_CHECKING, TypedDict, Unpack, cast, overload
142
+ from uuid import UUID, uuid4
143
+
144
+ from pydantic import ValidationError
145
+
146
+ from omnibase_core.enums import EnumCoreErrorCode
147
+ from omnibase_core.models.errors import ModelOnexError
148
+ from omnibase_core.models.events.model_event_envelope import ModelEventEnvelope
149
+ from omnibase_core.types import JsonType, PrimitiveValue
150
+ from omnibase_infra.enums import (
151
+ EnumDispatchStatus,
152
+ EnumInfraTransportType,
153
+ EnumMessageCategory,
154
+ )
155
+ from omnibase_infra.errors import (
156
+ BindingResolutionError,
157
+ ModelInfraErrorContext,
158
+ ProtocolConfigurationError,
159
+ )
160
+ from omnibase_infra.models.bindings import (
161
+ ModelBindingResolutionResult,
162
+ ModelOperationBindingsSubcontract,
163
+ )
164
+ from omnibase_infra.models.dispatch.model_debug_trace_snapshot import (
165
+ ModelDebugTraceSnapshot,
166
+ )
167
+ from omnibase_infra.models.dispatch.model_dispatch_context import ModelDispatchContext
168
+ from omnibase_infra.models.dispatch.model_dispatch_log_context import (
169
+ ModelDispatchLogContext,
170
+ )
171
+ from omnibase_infra.models.dispatch.model_dispatch_metrics import ModelDispatchMetrics
172
+ from omnibase_infra.models.dispatch.model_dispatch_outcome import ModelDispatchOutcome
173
+ from omnibase_infra.models.dispatch.model_dispatch_outputs import ModelDispatchOutputs
174
+ from omnibase_infra.models.dispatch.model_dispatch_result import ModelDispatchResult
175
+ from omnibase_infra.models.dispatch.model_dispatch_route import ModelDispatchRoute
176
+ from omnibase_infra.models.dispatch.model_dispatcher_metrics import (
177
+ ModelDispatcherMetrics,
178
+ )
179
+ from omnibase_infra.models.dispatch.model_materialized_dispatch import (
180
+ ModelMaterializedDispatch,
181
+ )
182
+ from omnibase_infra.runtime.binding_resolver import OperationBindingResolver
183
+ from omnibase_infra.runtime.dispatch_context_enforcer import DispatchContextEnforcer
184
+ from omnibase_infra.utils import sanitize_error_message
185
+
186
+ if TYPE_CHECKING:
187
+ from omnibase_core.enums.enum_node_kind import EnumNodeKind
188
+
189
+
190
+ class ModelLogContextKwargs(TypedDict, total=False):
191
+ """TypedDict for _build_log_context kwargs to ensure type safety.
192
+
193
+ All fields are optional (total=False) since callers pass only the
194
+ relevant subset. ModelDispatchLogContext validators handle None-to-sentinel
195
+ conversion.
196
+
197
+ .. versionadded:: 0.6.3
198
+ Created as part of Union Reduction Phase 2 (OMN-1002) to eliminate
199
+ type: ignore comment in _build_log_context.
200
+ """
201
+
202
+ topic: str | None
203
+ category: EnumMessageCategory | None
204
+ message_type: str | None
205
+ dispatcher_id: str | None
206
+ dispatcher_count: int | None
207
+ duration_ms: float | None
208
+ correlation_id: UUID | None
209
+ trace_id: UUID | None
210
+ error_code: EnumCoreErrorCode | None
211
+
212
+
213
+ # Type alias for dispatcher output topics
214
+ #
215
+ # Dispatchers can return:
216
+ # - str: A single output topic
217
+ # - list[str]: Multiple output topics
218
+ # - None: No output topics to publish
219
+ # - ModelDispatchResult: Protocol-based dispatchers return this for structured output
220
+ DispatcherOutput = str | list[str] | None | ModelDispatchResult
221
+
222
+ # Module-level logger for fallback when no custom logger is provided
223
+ _module_logger = logging.getLogger(__name__)
224
+
225
+ # Minimum number of parameters for a dispatcher to be considered context-aware.
226
+ # Context-aware dispatchers have signature: (envelope, context, ...)
227
+ # Non-context-aware dispatchers have signature: (envelope)
228
+ # We use >= MIN_PARAMS_FOR_CONTEXT (not ==) to support dispatchers with additional
229
+ # optional parameters (e.g., for testing, logging, or future extensibility).
230
+ MIN_PARAMS_FOR_CONTEXT = 2
231
+
232
+ # Type alias for dispatcher functions
233
+ #
234
+ # Design Note (PR #61 Review):
235
+ # ModelEventEnvelope[object] is used instead of Any to satisfy ONEX "no Any types" rule.
236
+ #
237
+ # Rationale:
238
+ # - Input: ModelEventEnvelope[object] is intentionally generic because dispatchers
239
+ # must accept envelopes with any payload type. The dispatch engine routes based
240
+ # on topic/category/message_type, not payload shape. Using a TypeVar would require
241
+ # dispatchers to be generic, adding complexity without benefit since the engine
242
+ # already performs type-based routing.
243
+ # - Output: DispatcherOutput | Awaitable[DispatcherOutput] defines the valid return
244
+ # types: str (single topic), list[str] (multiple topics), or None (no output).
245
+ # Dispatchers can be sync or async.
246
+ #
247
+ # Using `object` instead of `Any` provides:
248
+ # - Explicit "any object" semantics that are more informative to type checkers
249
+ # - Compliance with ONEX coding guidelines
250
+ # - Same runtime behavior as Any but with clearer intent
251
+ #
252
+ # See also: ProtocolMessageDispatcher in dispatcher_registry.py for protocol-based
253
+ # dispatchers that return ModelDispatchResult.
254
+ DispatcherFunc = Callable[
255
+ [ModelEventEnvelope[object]], DispatcherOutput | Awaitable[DispatcherOutput]
256
+ ]
257
+
258
+ # Context-aware dispatcher type (for dispatchers registered with node_kind)
259
+ # These dispatchers receive a ModelDispatchContext with time injection based on node_kind:
260
+ # - REDUCER/COMPUTE: now=None (deterministic)
261
+ # - ORCHESTRATOR/EFFECT/RUNTIME_HOST: now=datetime.now(UTC)
262
+ #
263
+ # This type is used when register_dispatcher() is called with node_kind parameter.
264
+ # The dispatch engine inspects the callable's signature to determine if it accepts context.
265
+ ContextAwareDispatcherFunc = Callable[
266
+ [ModelEventEnvelope[object], ModelDispatchContext],
267
+ DispatcherOutput | Awaitable[DispatcherOutput],
268
+ ]
269
+
270
+ # Sync-only dispatcher type for use with run_in_executor
271
+ # Used internally after runtime type narrowing via inspect.iscoroutinefunction
272
+ # All envelopes are materialized to dict format with __bindings namespace
273
+ _SyncDispatcherFunc = Callable[[dict[str, object]], DispatcherOutput]
274
+
275
+ # Sync-only context-aware dispatcher type for use with run_in_executor
276
+ # All envelopes are materialized to dict format with __bindings namespace
277
+ _SyncContextAwareDispatcherFunc = Callable[
278
+ [dict[str, object], ModelDispatchContext],
279
+ DispatcherOutput,
280
+ ]
281
+
282
+
283
+ class DispatchEntryInternal:
284
+ """
285
+ Internal storage for dispatcher registration metadata.
286
+
287
+ This class is an implementation detail and not part of the public API.
288
+ It stores the dispatcher callable and associated metadata for the
289
+ MessageDispatchEngine's internal routing.
290
+
291
+ Attributes:
292
+ dispatcher_id: Unique identifier for this dispatcher.
293
+ dispatcher: The callable that processes messages.
294
+ category: Message category this dispatcher handles.
295
+ message_types: Specific message types to handle (None = all types).
296
+ node_kind: Optional ONEX node kind for time injection context.
297
+ When set, the dispatcher receives a ModelDispatchContext with
298
+ appropriate time injection based on ONEX rules:
299
+ - REDUCER/COMPUTE: now=None (deterministic)
300
+ - ORCHESTRATOR/EFFECT/RUNTIME_HOST: now=datetime.now(UTC)
301
+ accepts_context: Cached result of signature inspection indicating
302
+ whether the dispatcher accepts a context parameter (2+ params).
303
+ Computed once at registration time for performance.
304
+ operation_bindings: Optional declarative bindings for resolving
305
+ handler parameters from envelope/payload/context. When set,
306
+ bindings are resolved BEFORE handler execution and materialized
307
+ into a new envelope with __bindings namespace.
308
+ """
309
+
310
+ __slots__ = (
311
+ "accepts_context",
312
+ "category",
313
+ "dispatcher",
314
+ "dispatcher_id",
315
+ "message_types",
316
+ "node_kind",
317
+ "operation_bindings",
318
+ )
319
+
320
+ def __init__(
321
+ self,
322
+ dispatcher_id: str,
323
+ dispatcher: DispatcherFunc | ContextAwareDispatcherFunc,
324
+ category: EnumMessageCategory,
325
+ message_types: set[str] | None,
326
+ node_kind: EnumNodeKind | None = None,
327
+ accepts_context: bool = False,
328
+ operation_bindings: ModelOperationBindingsSubcontract | None = None,
329
+ ) -> None:
330
+ self.dispatcher_id = dispatcher_id
331
+ self.dispatcher = dispatcher
332
+ self.category = category
333
+ self.message_types = message_types # None means "all types"
334
+ self.node_kind = node_kind # None means no context injection
335
+ self.accepts_context = accepts_context # Cached: dispatcher has 2+ params
336
+ self.operation_bindings = (
337
+ operation_bindings # Declarative bindings for this dispatcher
338
+ )
339
+
340
+
341
+ class MessageDispatchEngine:
342
+ """
343
+ Runtime dispatch engine for message routing.
344
+
345
+ Routes messages based on topic category and message type to registered
346
+ dispatchers. Supports fan-out (multiple dispatchers per message type) and
347
+ collects dispatcher outputs for publishing.
348
+
349
+ Key Characteristics:
350
+ - **Pure Routing**: No workflow inference or semantic understanding
351
+ - **Deterministic**: Same input always produces same dispatcher selection
352
+ - **Fan-out**: Multiple dispatchers can process the same message type
353
+ - **Observable**: Structured logging and comprehensive metrics
354
+
355
+ Registration Semantics:
356
+ - **Routes**: Keyed by route_id, duplicates raise error
357
+ - **Dispatchers**: Keyed by dispatcher_id, duplicates raise error
358
+ - Both must complete before freeze() is called
359
+
360
+ Thread Safety:
361
+ Follows the freeze-after-init pattern. All registrations must complete
362
+ before calling freeze(). After freeze(), dispatch operations are
363
+ thread-safe for concurrent access.
364
+
365
+ **TOCTOU Prevention** (core design goal):
366
+ Structured metrics use ``_metrics_lock`` to ensure atomic read-modify-write
367
+ operations. Without this, concurrent dispatches could lose updates:
368
+
369
+ - Thread A reads metrics, computes increment
370
+ - Thread B reads (stale) metrics, computes increment
371
+ - Thread A writes → Thread B writes → Thread A's update is lost
372
+
373
+ By holding the lock during the entire read→compute→write sequence, we
374
+ guarantee no interleaving occurs. The computations within the lock are
375
+ pure and fast (~microseconds), so lock contention is minimal.
376
+
377
+ - Structured metrics: Use ``_metrics_lock`` for atomic updates
378
+ - Use ``get_structured_metrics()`` for production monitoring
379
+
380
+ **METRICS CAVEAT**: While metrics updates are protected by a lock,
381
+ get_structured_metrics() provides point-in-time snapshots. Under high
382
+ concurrent load, metrics may be approximate between snapshot reads.
383
+ For production monitoring, consider exporting metrics to a dedicated
384
+ metrics backend (Prometheus, StatsD, etc.) for accurate aggregation
385
+ across time windows.
386
+
387
+ Logging Levels:
388
+ - **INFO**: Dispatch start/complete with topic, category, dispatcher count
389
+ - **DEBUG**: Dispatcher execution details, routing decisions
390
+ - **WARNING**: No dispatchers found, category mismatches
391
+ - **ERROR**: Dispatcher exceptions, validation failures
392
+
393
+ Example:
394
+ >>> from omnibase_infra.runtime import MessageDispatchEngine
395
+ >>> from omnibase_infra.models.dispatch import ModelDispatchRoute
396
+ >>> from omnibase_infra.enums import EnumMessageCategory
397
+ >>>
398
+ >>> # Create engine with optional custom logger
399
+ >>> engine = MessageDispatchEngine(logger=my_logger)
400
+ >>> engine.register_dispatcher(
401
+ ... dispatcher_id="user-dispatcher",
402
+ ... dispatcher=process_user_event,
403
+ ... category=EnumMessageCategory.EVENT,
404
+ ... message_types={"UserCreated", "UserUpdated"},
405
+ ... )
406
+ >>> engine.register_route(ModelDispatchRoute(
407
+ ... route_id="user-route",
408
+ ... topic_pattern="*.user.events.*",
409
+ ... message_category=EnumMessageCategory.EVENT,
410
+ ... dispatcher_id="user-dispatcher",
411
+ ... ))
412
+ >>> engine.freeze()
413
+ >>>
414
+ >>> # Dispatch (thread-safe after freeze)
415
+ >>> result = await engine.dispatch("dev.user.events.v1", envelope)
416
+
417
+ Attributes:
418
+ _routes: Registry of routes by route_id
419
+ _dispatchers: Registry of dispatchers by dispatcher_id
420
+ _dispatchers_by_category: Index of dispatchers by category for fast lookup
421
+ _frozen: If True, registration methods raise ModelOnexError
422
+ _registration_lock: Lock protecting registration methods
423
+ _metrics_lock: Lock protecting structured metrics updates
424
+ _structured_metrics: Pydantic-based metrics model for observability
425
+ _logger: Optional custom logger for structured logging
426
+
427
+ See Also:
428
+ - :class:`~omnibase_infra.models.dispatch.ModelDispatchRoute`: Route model
429
+ - :class:`~omnibase_infra.models.dispatch.ModelDispatchResult`: Result model
430
+ - :class:`~omnibase_infra.models.dispatch.ModelDispatchMetrics`: Metrics model
431
+ - :class:`~omnibase_core.runtime.EnvelopeRouter`: Reference implementation
432
+
433
+ .. versionadded:: 0.4.0
434
+ """
435
+
436
+ def __init__(
437
+ self,
438
+ logger: logging.Logger | None = None,
439
+ ) -> None:
440
+ """
441
+ Initialize MessageDispatchEngine with empty registries.
442
+
443
+ Creates empty route and dispatcher registries and initializes metrics.
444
+ Call freeze() after registration to enable thread-safe dispatch.
445
+
446
+ Args:
447
+ logger: Optional custom logger for structured logging.
448
+ If not provided, uses module-level logger.
449
+ """
450
+ # Optional custom logger
451
+ self._logger: logging.Logger = logger if logger is not None else _module_logger
452
+
453
+ # Route storage: route_id -> ModelDispatchRoute
454
+ self._routes: dict[str, ModelDispatchRoute] = {}
455
+
456
+ # Dispatcher storage: dispatcher_id -> DispatchEntryInternal
457
+ self._dispatchers: dict[str, DispatchEntryInternal] = {}
458
+
459
+ # Index for fast dispatcher lookup by category
460
+ # category -> list of dispatcher_ids
461
+ # NOTE: Only routable message categories are indexed here.
462
+ # PROJECTION is NOT included because projections are reducer outputs,
463
+ # not routable messages. See CLAUDE.md "Enum Usage" section.
464
+ self._dispatchers_by_category: dict[EnumMessageCategory, list[str]] = {
465
+ EnumMessageCategory.EVENT: [],
466
+ EnumMessageCategory.COMMAND: [],
467
+ EnumMessageCategory.INTENT: [],
468
+ }
469
+
470
+ # Freeze state
471
+ self._frozen: bool = False
472
+ self._registration_lock: threading.Lock = threading.Lock()
473
+
474
+ # Metrics lock for TOCTOU-safe structured metrics updates
475
+ # This lock protects the entire read-modify-write sequence on _structured_metrics:
476
+ # 1. Read current metrics state
477
+ # 2. Compute new values (record_execution, model_copy)
478
+ # 3. Write updated metrics back
479
+ # Holding the lock during computation prevents lost updates from concurrent dispatches.
480
+ # The computations are pure and fast (~microseconds), minimizing lock contention.
481
+ self._metrics_lock: threading.Lock = threading.Lock()
482
+
483
+ # Structured metrics (Pydantic model)
484
+ self._structured_metrics: ModelDispatchMetrics = ModelDispatchMetrics()
485
+
486
+ # Context enforcer for creating dispatch contexts based on node_kind.
487
+ # Delegates time injection rule enforcement to a single source of truth.
488
+ self._context_enforcer: DispatchContextEnforcer = DispatchContextEnforcer()
489
+
490
+ # Binding resolver for declarative operation bindings.
491
+ # Resolves ${source.path} expressions from envelope/payload/context.
492
+ self._binding_resolver: OperationBindingResolver = OperationBindingResolver()
493
+
494
+ def register_route(self, route: ModelDispatchRoute) -> None:
495
+ """
496
+ Register a routing rule.
497
+
498
+ Routes define how messages are matched to dispatchers based on topic
499
+ pattern, message category, and optionally message type.
500
+
501
+ Args:
502
+ route: The routing rule to register. Must have unique route_id.
503
+
504
+ Raises:
505
+ ModelOnexError: If engine is frozen (INVALID_STATE)
506
+ ModelOnexError: If route is None (INVALID_PARAMETER)
507
+ ModelOnexError: If route with same route_id exists (DUPLICATE_REGISTRATION)
508
+ ModelOnexError: If route.dispatcher_id references non-existent dispatcher
509
+ (ITEM_NOT_REGISTERED) - only checked after freeze
510
+
511
+ Example:
512
+ >>> engine.register_route(ModelDispatchRoute(
513
+ ... route_id="order-events",
514
+ ... topic_pattern="*.order.events.*",
515
+ ... message_category=EnumMessageCategory.EVENT,
516
+ ... dispatcher_id="order-dispatcher",
517
+ ... ))
518
+
519
+ Note:
520
+ Route-to-dispatcher consistency is NOT validated during registration
521
+ to allow flexible registration order. Validation occurs at freeze()
522
+ time or during dispatch.
523
+ """
524
+ if route is None:
525
+ raise ModelOnexError(
526
+ message="Cannot register None route. ModelDispatchRoute is required.",
527
+ error_code=EnumCoreErrorCode.INVALID_PARAMETER,
528
+ )
529
+
530
+ with self._registration_lock:
531
+ if self._frozen:
532
+ raise ModelOnexError(
533
+ message="Cannot register route: MessageDispatchEngine is frozen. "
534
+ "Registration is not allowed after freeze() has been called.",
535
+ error_code=EnumCoreErrorCode.INVALID_STATE,
536
+ )
537
+
538
+ if route.route_id in self._routes:
539
+ raise ModelOnexError(
540
+ message=f"Route with ID '{route.route_id}' is already registered. "
541
+ "Cannot register duplicate route ID.",
542
+ error_code=EnumCoreErrorCode.DUPLICATE_REGISTRATION,
543
+ )
544
+
545
+ self._routes[route.route_id] = route
546
+ self._logger.debug(
547
+ "Registered route '%s' for pattern '%s' (category=%s, dispatcher=%s)",
548
+ route.route_id,
549
+ route.topic_pattern,
550
+ route.message_category,
551
+ route.dispatcher_id,
552
+ )
553
+
554
+ # --- @overload stubs for static type safety ---
555
+ #
556
+ # NOTE: These are TYPE STUBS only - they provide no runtime behavior.
557
+ # The actual implementation is in the non-overloaded register_dispatcher() below.
558
+ #
559
+ # Purpose: Enable type checkers (mypy, pyright) to validate that:
560
+ # - When node_kind=None (or omitted): dispatcher must be DispatcherFunc
561
+ # - When node_kind=EnumNodeKind: dispatcher must be ContextAwareDispatcherFunc
562
+ #
563
+ # This pattern enforces compile-time type safety for the relationship between
564
+ # node_kind presence and expected dispatcher signature.
565
+ #
566
+ # See ADR_DISPATCHER_TYPE_SAFETY.md Option 4 for design rationale.
567
+
568
+ @overload
569
+ def register_dispatcher(
570
+ self,
571
+ dispatcher_id: str,
572
+ dispatcher: DispatcherFunc,
573
+ category: EnumMessageCategory,
574
+ message_types: set[str] | None = None,
575
+ node_kind: None = None,
576
+ operation_bindings: ModelOperationBindingsSubcontract | None = None,
577
+ ) -> None: ... # Stub: no node_kind -> DispatcherFunc (no context)
578
+
579
+ @overload
580
+ def register_dispatcher(
581
+ self,
582
+ dispatcher_id: str,
583
+ dispatcher: ContextAwareDispatcherFunc,
584
+ category: EnumMessageCategory,
585
+ message_types: set[str] | None = None,
586
+ *,
587
+ node_kind: EnumNodeKind,
588
+ operation_bindings: ModelOperationBindingsSubcontract | None = None,
589
+ ) -> None: ... # Stub: with node_kind -> ContextAwareDispatcherFunc (gets context)
590
+
591
+ def register_dispatcher(
592
+ self,
593
+ dispatcher_id: str,
594
+ dispatcher: DispatcherFunc | ContextAwareDispatcherFunc,
595
+ category: EnumMessageCategory,
596
+ message_types: set[str] | None = None,
597
+ node_kind: EnumNodeKind | None = None,
598
+ operation_bindings: ModelOperationBindingsSubcontract | None = None,
599
+ ) -> None:
600
+ """
601
+ Register a message dispatcher.
602
+
603
+ Dispatchers process messages that match their category and (optionally)
604
+ message type. Multiple dispatchers can register for the same category
605
+ and message type (fan-out pattern).
606
+
607
+ Args:
608
+ dispatcher_id: Unique identifier for this dispatcher
609
+ dispatcher: Callable that processes messages. Can be sync or async.
610
+ Signature: (envelope: ModelEventEnvelope[object]) -> DispatcherOutput
611
+ Or with context:
612
+ (envelope: ModelEventEnvelope[object], context: ModelDispatchContext) -> DispatcherOutput
613
+ category: Message category this dispatcher processes
614
+ message_types: Optional set of specific message types to handle.
615
+ When None, handles all message types in the category.
616
+ node_kind: Optional ONEX node kind for time injection context.
617
+ When provided, the dispatcher receives a ModelDispatchContext
618
+ with appropriate time injection based on ONEX rules:
619
+ - REDUCER/COMPUTE: now=None (deterministic execution)
620
+ - ORCHESTRATOR/EFFECT/RUNTIME_HOST: now=datetime.now(UTC)
621
+ When None, dispatcher is called without context.
622
+ operation_bindings: Optional declarative bindings for resolving
623
+ handler parameters from envelope/payload/context. When provided,
624
+ bindings are resolved BEFORE handler execution and materialized
625
+ into a new envelope with __bindings namespace. The original
626
+ envelope is NEVER mutated.
627
+
628
+ Raises:
629
+ ModelOnexError: If engine is frozen (INVALID_STATE)
630
+ ModelOnexError: If dispatcher_id is empty (INVALID_PARAMETER)
631
+ ModelOnexError: If dispatcher is not callable (INVALID_PARAMETER)
632
+ ModelOnexError: If dispatcher with same ID exists (DUPLICATE_REGISTRATION)
633
+
634
+ Example:
635
+ >>> async def process_user_event(envelope):
636
+ ... user_data = envelope.payload
637
+ ... # Process the event
638
+ ... return {"processed": True}
639
+ >>>
640
+ >>> engine.register_dispatcher(
641
+ ... dispatcher_id="user-event-dispatcher",
642
+ ... dispatcher=process_user_event,
643
+ ... category=EnumMessageCategory.EVENT,
644
+ ... message_types={"UserCreated", "UserUpdated"},
645
+ ... )
646
+ >>>
647
+ >>> # With time injection context for orchestrator
648
+ >>> async def process_with_context(envelope, context):
649
+ ... current_time = context.now # Injected time
650
+ ... return "processed"
651
+ >>>
652
+ >>> engine.register_dispatcher(
653
+ ... dispatcher_id="orchestrator-dispatcher",
654
+ ... dispatcher=process_with_context,
655
+ ... category=EnumMessageCategory.COMMAND,
656
+ ... node_kind=EnumNodeKind.ORCHESTRATOR,
657
+ ... )
658
+
659
+ Note:
660
+ Dispatchers are NOT automatically linked to routes. You must register
661
+ routes separately that reference the dispatcher_id.
662
+
663
+ .. versionchanged:: 0.5.0
664
+ Added ``node_kind`` parameter for time injection context support.
665
+ """
666
+ # Validate inputs before acquiring lock
667
+ if not dispatcher_id or not dispatcher_id.strip():
668
+ raise ModelOnexError(
669
+ message="Dispatcher ID cannot be empty or whitespace.",
670
+ error_code=EnumCoreErrorCode.INVALID_PARAMETER,
671
+ )
672
+
673
+ if dispatcher is None or not callable(dispatcher):
674
+ raise ModelOnexError(
675
+ message=f"Dispatcher for '{dispatcher_id}' must be callable. "
676
+ f"Got {type(dispatcher).__name__}.",
677
+ error_code=EnumCoreErrorCode.INVALID_PARAMETER,
678
+ )
679
+
680
+ if not isinstance(category, EnumMessageCategory):
681
+ raise ModelOnexError(
682
+ message=f"Category must be EnumMessageCategory, got {type(category).__name__}.",
683
+ error_code=EnumCoreErrorCode.INVALID_PARAMETER,
684
+ )
685
+
686
+ # Runtime validation for node_kind to catch dynamic dispatch issues
687
+ # where type checkers can't help (e.g., dynamically constructed arguments)
688
+ if node_kind is not None:
689
+ # Import here to avoid circular import at module level
690
+ # EnumNodeKind is only in TYPE_CHECKING block at top of file
691
+ from omnibase_core.enums.enum_node_kind import EnumNodeKind
692
+
693
+ if not isinstance(node_kind, EnumNodeKind):
694
+ context = ModelInfraErrorContext.with_correlation(
695
+ transport_type=EnumInfraTransportType.RUNTIME,
696
+ operation="register_dispatcher",
697
+ )
698
+ raise ProtocolConfigurationError(
699
+ f"node_kind must be EnumNodeKind or None, got {type(node_kind).__name__}",
700
+ context=context,
701
+ )
702
+
703
+ with self._registration_lock:
704
+ if self._frozen:
705
+ raise ModelOnexError(
706
+ message="Cannot register dispatcher: MessageDispatchEngine is frozen. "
707
+ "Registration is not allowed after freeze() has been called.",
708
+ error_code=EnumCoreErrorCode.INVALID_STATE,
709
+ )
710
+
711
+ if dispatcher_id in self._dispatchers:
712
+ raise ModelOnexError(
713
+ message=f"Dispatcher with ID '{dispatcher_id}' is already registered. "
714
+ "Cannot register duplicate dispatcher ID.",
715
+ error_code=EnumCoreErrorCode.DUPLICATE_REGISTRATION,
716
+ )
717
+
718
+ # Compute accepts_context once at registration time (cached)
719
+ # This avoids expensive inspect.signature() calls on every dispatch
720
+ accepts_context = self._dispatcher_accepts_context(dispatcher)
721
+
722
+ # Store dispatcher entry
723
+ entry = DispatchEntryInternal(
724
+ dispatcher_id=dispatcher_id,
725
+ dispatcher=dispatcher,
726
+ category=category,
727
+ message_types=message_types,
728
+ node_kind=node_kind,
729
+ accepts_context=accepts_context,
730
+ operation_bindings=operation_bindings,
731
+ )
732
+ self._dispatchers[dispatcher_id] = entry
733
+
734
+ # Log requirement for operation_bindings users
735
+ # NOTE: When operation_bindings is provided, envelopes dispatched to this
736
+ # handler MUST have an 'operation' attribute/key. The operation field is
737
+ # extracted at dispatch time via _extract_operation() and used to select
738
+ # the appropriate binding configuration. Missing operation fields will
739
+ # result in binding resolution failures at dispatch time.
740
+ if operation_bindings is not None:
741
+ self._logger.debug(
742
+ "Dispatcher '%s' registered with operation_bindings. "
743
+ "Envelopes MUST have an 'operation' attribute/key for binding resolution.",
744
+ dispatcher_id,
745
+ )
746
+
747
+ # Update category index
748
+ self._dispatchers_by_category[category].append(dispatcher_id)
749
+
750
+ self._logger.debug(
751
+ "Registered dispatcher '%s' for category %s (message_types=%s, node_kind=%s)",
752
+ dispatcher_id,
753
+ category,
754
+ message_types if message_types else "all",
755
+ node_kind.value if node_kind else "none",
756
+ )
757
+
758
+ def freeze(self) -> None:
759
+ """
760
+ Freeze the engine to prevent further registration.
761
+
762
+ Once frozen, any calls to register_route() or register_dispatcher()
763
+ will raise ModelOnexError with INVALID_STATE. This enforces the
764
+ read-only-after-init pattern for thread safety.
765
+
766
+ The freeze operation validates route-to-dispatcher consistency:
767
+ all routes must reference existing dispatchers.
768
+
769
+ Raises:
770
+ ModelOnexError: If any route references a non-existent dispatcher
771
+ (ITEM_NOT_REGISTERED)
772
+
773
+ Example:
774
+ >>> engine = MessageDispatchEngine()
775
+ >>> engine.register_dispatcher("d1", dispatcher, EnumMessageCategory.EVENT)
776
+ >>> engine.register_route(route)
777
+ >>> engine.freeze() # Validates and freezes
778
+ >>> assert engine.is_frozen
779
+
780
+ Note:
781
+ This is a one-way operation. There is no unfreeze() method
782
+ by design, as unfreezing would defeat thread-safety guarantees.
783
+
784
+ .. versionadded:: 0.4.0
785
+ """
786
+ with self._registration_lock:
787
+ if self._frozen:
788
+ # Idempotent - already frozen
789
+ return
790
+
791
+ # Validate all routes reference existing dispatchers
792
+ for route in self._routes.values():
793
+ if route.dispatcher_id not in self._dispatchers:
794
+ raise ModelOnexError(
795
+ message=f"Route '{route.route_id}' references dispatcher "
796
+ f"'{route.dispatcher_id}' which is not registered. "
797
+ "Register the dispatcher before freezing.",
798
+ error_code=EnumCoreErrorCode.ITEM_NOT_REGISTERED,
799
+ )
800
+
801
+ self._frozen = True
802
+ self._logger.info(
803
+ "MessageDispatchEngine frozen with %d routes and %d dispatchers",
804
+ len(self._routes),
805
+ len(self._dispatchers),
806
+ )
807
+
808
+ @property
809
+ def is_frozen(self) -> bool:
810
+ """
811
+ Check if the engine is frozen.
812
+
813
+ Returns:
814
+ True if frozen and registration is disabled, False otherwise
815
+
816
+ .. versionadded:: 0.4.0
817
+ """
818
+ return self._frozen
819
+
820
+ def _build_log_context(
821
+ self, **kwargs: Unpack[ModelLogContextKwargs]
822
+ ) -> dict[str, PrimitiveValue]:
823
+ """
824
+ Build structured log context dictionary.
825
+
826
+ .. versionchanged:: 0.6.0
827
+ Now delegates to ModelDispatchLogContext.to_dict() for type-safe
828
+ context construction.
829
+
830
+ .. versionchanged:: 0.6.2
831
+ Refactored to use ``**kwargs`` forwarding to eliminate 9 union
832
+ parameters from method signature (OMN-1002 Union Reduction Phase 2).
833
+ ModelDispatchLogContext validators handle None-to-sentinel conversion.
834
+
835
+ .. versionchanged:: 0.6.3
836
+ Updated to use ``Unpack[ModelLogContextKwargs]`` TypedDict for type-safe
837
+ kwargs (OMN-1002). Eliminates need for ``type: ignore`` comment.
838
+
839
+ Design Note (Union Reduction - OMN-1002):
840
+ This private method uses typed ``**kwargs`` via ``ModelLogContextKwargs``
841
+ TypedDict to forward parameters to ModelDispatchLogContext. The
842
+ TypedDict provides compile-time type checking while the model's
843
+ field validators handle None-to-sentinel conversion at runtime.
844
+
845
+ Args:
846
+ **kwargs: Keyword arguments forwarded to ModelDispatchLogContext.
847
+ Typed via ``ModelLogContextKwargs`` TypedDict with supported keys:
848
+ topic, category, message_type, dispatcher_id, dispatcher_count,
849
+ duration_ms, correlation_id, trace_id, error_code.
850
+ None values are automatically converted to sentinel values by
851
+ the model's field validators.
852
+
853
+ Returns:
854
+ Dictionary with non-sentinel values for structured logging.
855
+ UUID values are converted to strings at serialization time.
856
+ """
857
+ # Forward all kwargs to ModelDispatchLogContext which handles
858
+ # None-to-sentinel conversion via field validators.
859
+ # Use model_validate() to properly invoke "before" validators that
860
+ # accept None via object type annotation.
861
+ ctx = ModelDispatchLogContext.model_validate(kwargs)
862
+ return ctx.to_dict()
863
+
864
+ async def dispatch(
865
+ self,
866
+ topic: str,
867
+ envelope: ModelEventEnvelope[object],
868
+ ) -> ModelDispatchResult:
869
+ """
870
+ Dispatch a message to matching dispatchers.
871
+
872
+ Routes the message based on topic category and message type, executes
873
+ all matching dispatchers, and collects their outputs.
874
+
875
+ Dispatch Process:
876
+ 1. Parse topic to extract message category
877
+ 2. Validate envelope category matches topic category
878
+ 3. Get message type from envelope payload
879
+ 4. Find all matching dispatchers (by category + message type)
880
+ 5. Execute dispatchers (fan-out)
881
+ 6. Collect outputs and return result
882
+
883
+ Args:
884
+ topic: The topic the message was received on (e.g., "dev.user.events.v1")
885
+ envelope: The message envelope to dispatch
886
+
887
+ Returns:
888
+ ModelDispatchResult with dispatch status, metrics, and dispatcher outputs
889
+
890
+ Raises:
891
+ ModelOnexError: If engine is not frozen (INVALID_STATE)
892
+ ModelOnexError: If topic is empty (INVALID_PARAMETER)
893
+ ModelOnexError: If envelope is None (INVALID_PARAMETER)
894
+
895
+ Example:
896
+ >>> result = await engine.dispatch(
897
+ ... topic="dev.user.events.v1",
898
+ ... envelope=ModelEventEnvelope(payload=UserCreatedEvent(...)),
899
+ ... )
900
+ >>> if result.is_successful():
901
+ ... print(f"Dispatched to {result.output_count} dispatchers")
902
+
903
+ Note:
904
+ Dispatcher exceptions are caught and reported in the result.
905
+ The dispatch continues to other dispatchers even if one fails.
906
+
907
+ .. versionadded:: 0.4.0
908
+ """
909
+ # Enforce freeze contract
910
+ if not self._frozen:
911
+ raise ModelOnexError(
912
+ message="dispatch() called before freeze(). "
913
+ "Registration MUST complete and freeze() MUST be called before dispatch. "
914
+ "This is required for thread safety.",
915
+ error_code=EnumCoreErrorCode.INVALID_STATE,
916
+ )
917
+
918
+ # Validate inputs
919
+ if not topic or not topic.strip():
920
+ raise ModelOnexError(
921
+ message="Topic cannot be empty or whitespace.",
922
+ error_code=EnumCoreErrorCode.INVALID_PARAMETER,
923
+ )
924
+
925
+ if envelope is None:
926
+ raise ModelOnexError(
927
+ message="Cannot dispatch None envelope. ModelEventEnvelope is required.",
928
+ error_code=EnumCoreErrorCode.INVALID_PARAMETER,
929
+ )
930
+
931
+ # Start timing
932
+ start_time = time.perf_counter()
933
+ dispatch_id = uuid4()
934
+ started_at = datetime.now(UTC)
935
+
936
+ # Extract correlation/trace IDs for logging (kept as UUID, converted to string at serialization)
937
+ # Per ONEX guidelines: auto-generate correlation_id if not provided (uuid4())
938
+ correlation_id = envelope.correlation_id or uuid4()
939
+ trace_id = envelope.trace_id
940
+
941
+ # Step 1: Parse topic to get category
942
+ topic_category = EnumMessageCategory.from_topic(topic)
943
+ if topic_category is None:
944
+ # Capture duration and completed_at together for consistency
945
+ duration_ms = (time.perf_counter() - start_time) * 1000
946
+ completed_at = datetime.now(UTC)
947
+
948
+ # Update metrics (protected by lock for thread safety)
949
+ with self._metrics_lock:
950
+ self._structured_metrics = self._structured_metrics.record_dispatch(
951
+ duration_ms=duration_ms,
952
+ success=False,
953
+ category=None,
954
+ no_dispatcher=False,
955
+ category_mismatch=False,
956
+ topic=topic,
957
+ )
958
+
959
+ # Log error
960
+ self._logger.error(
961
+ "Dispatch failed: invalid topic category",
962
+ extra=self._build_log_context(
963
+ topic=topic,
964
+ duration_ms=duration_ms,
965
+ correlation_id=correlation_id,
966
+ trace_id=trace_id,
967
+ error_code=EnumCoreErrorCode.VALIDATION_ERROR,
968
+ ),
969
+ )
970
+
971
+ return ModelDispatchResult(
972
+ dispatch_id=dispatch_id,
973
+ status=EnumDispatchStatus.INVALID_MESSAGE,
974
+ topic=topic,
975
+ started_at=started_at,
976
+ completed_at=completed_at,
977
+ duration_ms=duration_ms,
978
+ error_message=f"Cannot infer message category from topic '{topic}'. "
979
+ "Topic must contain .events, .commands, .intents, or .projections segment.",
980
+ error_code=EnumCoreErrorCode.VALIDATION_ERROR,
981
+ correlation_id=correlation_id,
982
+ output_events=[],
983
+ )
984
+
985
+ # Log dispatch start at INFO level
986
+ self._logger.info(
987
+ "Dispatch started",
988
+ extra=self._build_log_context(
989
+ topic=topic,
990
+ category=topic_category,
991
+ correlation_id=correlation_id,
992
+ trace_id=trace_id,
993
+ ),
994
+ )
995
+
996
+ # Step 2: Validate envelope category matches topic category
997
+ # NOTE: ModelEventEnvelope.infer_category() is not yet implemented in omnibase_core.
998
+ # Until it is, we trust the topic category as the source of truth for routing.
999
+ # This is safe because the topic defines the message category, and handlers
1000
+ # are registered for specific categories - any mismatch would be a caller error.
1001
+ # TODO(OMN-934): Re-enable envelope category validation when infer_category() is available
1002
+ #
1003
+ # The code below is disabled until infer_category() is available:
1004
+ # envelope_category = envelope.infer_category()
1005
+ # if envelope_category != topic_category:
1006
+ # ... (category mismatch handling with structured metrics)
1007
+
1008
+ # Step 3: Get message type from payload
1009
+ message_type = type(envelope.payload).__name__
1010
+
1011
+ # Step 4: Find matching dispatchers
1012
+ matching_dispatchers = self._find_matching_dispatchers(
1013
+ topic=topic,
1014
+ category=topic_category,
1015
+ message_type=message_type,
1016
+ )
1017
+
1018
+ # Log routing decision at DEBUG level
1019
+ self._logger.debug(
1020
+ "Routing decision: %d dispatchers matched for message_type '%s'",
1021
+ len(matching_dispatchers),
1022
+ message_type,
1023
+ extra=self._build_log_context(
1024
+ topic=topic,
1025
+ category=topic_category,
1026
+ message_type=message_type,
1027
+ dispatcher_count=len(matching_dispatchers),
1028
+ correlation_id=correlation_id,
1029
+ trace_id=trace_id,
1030
+ ),
1031
+ )
1032
+
1033
+ if not matching_dispatchers:
1034
+ # Capture duration and completed_at together for consistency
1035
+ duration_ms = (time.perf_counter() - start_time) * 1000
1036
+ completed_at = datetime.now(UTC)
1037
+
1038
+ # Update metrics (protected by lock for thread safety)
1039
+ with self._metrics_lock:
1040
+ self._structured_metrics = self._structured_metrics.record_dispatch(
1041
+ duration_ms=duration_ms,
1042
+ success=False,
1043
+ category=topic_category,
1044
+ no_dispatcher=True,
1045
+ topic=topic,
1046
+ )
1047
+
1048
+ # Log warning
1049
+ self._logger.warning(
1050
+ "No dispatcher found for category '%s' and message type '%s'",
1051
+ topic_category,
1052
+ message_type,
1053
+ extra=self._build_log_context(
1054
+ topic=topic,
1055
+ category=topic_category,
1056
+ message_type=message_type,
1057
+ dispatcher_count=0,
1058
+ duration_ms=duration_ms,
1059
+ correlation_id=correlation_id,
1060
+ trace_id=trace_id,
1061
+ error_code=EnumCoreErrorCode.ITEM_NOT_REGISTERED,
1062
+ ),
1063
+ )
1064
+
1065
+ return ModelDispatchResult(
1066
+ dispatch_id=dispatch_id,
1067
+ status=EnumDispatchStatus.NO_DISPATCHER,
1068
+ topic=topic,
1069
+ message_category=topic_category,
1070
+ message_type=message_type,
1071
+ started_at=started_at,
1072
+ completed_at=completed_at,
1073
+ duration_ms=duration_ms,
1074
+ error_message=f"No dispatcher registered for category '{topic_category}' "
1075
+ f"and message type '{message_type}' matching topic '{topic}'.",
1076
+ error_code=EnumCoreErrorCode.ITEM_NOT_REGISTERED,
1077
+ correlation_id=correlation_id,
1078
+ output_events=[],
1079
+ )
1080
+
1081
+ # Step 5: Execute dispatchers and collect outputs
1082
+ outputs: list[str] = []
1083
+ dispatcher_errors: list[str] = []
1084
+ executed_dispatcher_ids: list[str] = []
1085
+
1086
+ for dispatcher_entry in matching_dispatchers:
1087
+ dispatcher_start_time = time.perf_counter()
1088
+
1089
+ # Log dispatcher execution at DEBUG level
1090
+ self._logger.debug(
1091
+ "Executing dispatcher '%s'",
1092
+ dispatcher_entry.dispatcher_id,
1093
+ extra=self._build_log_context(
1094
+ topic=topic,
1095
+ category=topic_category,
1096
+ message_type=message_type,
1097
+ dispatcher_id=dispatcher_entry.dispatcher_id,
1098
+ correlation_id=correlation_id,
1099
+ trace_id=trace_id,
1100
+ ),
1101
+ )
1102
+
1103
+ try:
1104
+ result = await self._execute_dispatcher(
1105
+ dispatcher_entry, envelope, topic
1106
+ )
1107
+ dispatcher_duration_ms = (
1108
+ time.perf_counter() - dispatcher_start_time
1109
+ ) * 1000
1110
+ executed_dispatcher_ids.append(dispatcher_entry.dispatcher_id)
1111
+
1112
+ # TOCTOU Prevention: Update per-dispatcher metrics atomically
1113
+ # ---------------------------------------------------------
1114
+ # The entire read-modify-write sequence below MUST execute within
1115
+ # a single lock acquisition to prevent race conditions:
1116
+ # 1. Read: Get existing dispatcher metrics (or create default)
1117
+ # 2. Modify: Call record_execution() to compute new values
1118
+ # 3. Write: Update _structured_metrics with new dispatcher entry
1119
+ #
1120
+ # These operations are pure (no I/O) and fast (~microseconds),
1121
+ # so holding the lock during computation is acceptable.
1122
+ with self._metrics_lock:
1123
+ existing_dispatcher_metrics = (
1124
+ self._structured_metrics.dispatcher_metrics.get(
1125
+ dispatcher_entry.dispatcher_id
1126
+ )
1127
+ )
1128
+ if existing_dispatcher_metrics is None:
1129
+ existing_dispatcher_metrics = ModelDispatcherMetrics(
1130
+ dispatcher_id=dispatcher_entry.dispatcher_id
1131
+ )
1132
+ new_dispatcher_metrics = (
1133
+ existing_dispatcher_metrics.record_execution(
1134
+ duration_ms=dispatcher_duration_ms,
1135
+ success=True,
1136
+ topic=topic,
1137
+ )
1138
+ )
1139
+ new_dispatcher_metrics_dict = {
1140
+ **self._structured_metrics.dispatcher_metrics,
1141
+ dispatcher_entry.dispatcher_id: new_dispatcher_metrics,
1142
+ }
1143
+ self._structured_metrics = self._structured_metrics.model_copy(
1144
+ update={
1145
+ "dispatcher_execution_count": (
1146
+ self._structured_metrics.dispatcher_execution_count + 1
1147
+ ),
1148
+ "dispatcher_metrics": new_dispatcher_metrics_dict,
1149
+ }
1150
+ )
1151
+
1152
+ # Log dispatcher completion at DEBUG level
1153
+ self._logger.debug(
1154
+ "Dispatcher '%s' completed successfully in %.2f ms",
1155
+ dispatcher_entry.dispatcher_id,
1156
+ dispatcher_duration_ms,
1157
+ extra=self._build_log_context(
1158
+ topic=topic,
1159
+ category=topic_category,
1160
+ message_type=message_type,
1161
+ dispatcher_id=dispatcher_entry.dispatcher_id,
1162
+ duration_ms=dispatcher_duration_ms,
1163
+ correlation_id=correlation_id,
1164
+ trace_id=trace_id,
1165
+ ),
1166
+ )
1167
+
1168
+ # Normalize dispatcher output using ModelDispatchOutcome to avoid
1169
+ # manual isinstance checks on the 3-way union (str | list[str] | None).
1170
+ # This centralizes the union handling in the model's from_legacy_output().
1171
+ outcome = ModelDispatchOutcome.from_legacy_output(result)
1172
+ outputs.extend(outcome.topics)
1173
+ except (SystemExit, KeyboardInterrupt, GeneratorExit):
1174
+ # Never catch cancellation/exit signals
1175
+ raise
1176
+ except asyncio.CancelledError:
1177
+ # Never suppress async cancellation
1178
+ raise
1179
+ except Exception as e:
1180
+ dispatcher_duration_ms = (
1181
+ time.perf_counter() - dispatcher_start_time
1182
+ ) * 1000
1183
+ # Sanitize exception message to prevent credential leakage
1184
+ # (e.g., connection strings with passwords, API keys in URLs)
1185
+ sanitized_error = sanitize_error_message(e)
1186
+ error_msg = (
1187
+ f"Dispatcher '{dispatcher_entry.dispatcher_id}' "
1188
+ f"failed: {sanitized_error}"
1189
+ )
1190
+ dispatcher_errors.append(error_msg)
1191
+
1192
+ # TOCTOU Prevention: Update per-dispatcher error metrics atomically
1193
+ # ----------------------------------------------------------------
1194
+ # The entire read-modify-write sequence below MUST execute within
1195
+ # a single lock acquisition to prevent race conditions:
1196
+ # 1. Read: Get existing dispatcher metrics (or create default)
1197
+ # 2. Modify: Call record_execution() to compute new error values
1198
+ # 3. Write: Update _structured_metrics with new dispatcher entry
1199
+ #
1200
+ # These operations are pure (no I/O) and fast (~microseconds),
1201
+ # so holding the lock during computation is acceptable.
1202
+ with self._metrics_lock:
1203
+ existing_dispatcher_metrics = (
1204
+ self._structured_metrics.dispatcher_metrics.get(
1205
+ dispatcher_entry.dispatcher_id
1206
+ )
1207
+ )
1208
+ if existing_dispatcher_metrics is None:
1209
+ existing_dispatcher_metrics = ModelDispatcherMetrics(
1210
+ dispatcher_id=dispatcher_entry.dispatcher_id
1211
+ )
1212
+ new_dispatcher_metrics = (
1213
+ existing_dispatcher_metrics.record_execution(
1214
+ duration_ms=dispatcher_duration_ms,
1215
+ success=False,
1216
+ topic=topic,
1217
+ # Use sanitized error message for metrics as well
1218
+ error_message=sanitized_error,
1219
+ )
1220
+ )
1221
+ new_dispatcher_metrics_dict = {
1222
+ **self._structured_metrics.dispatcher_metrics,
1223
+ dispatcher_entry.dispatcher_id: new_dispatcher_metrics,
1224
+ }
1225
+ self._structured_metrics = self._structured_metrics.model_copy(
1226
+ update={
1227
+ "dispatcher_execution_count": (
1228
+ self._structured_metrics.dispatcher_execution_count + 1
1229
+ ),
1230
+ "dispatcher_error_count": (
1231
+ self._structured_metrics.dispatcher_error_count + 1
1232
+ ),
1233
+ "dispatcher_metrics": new_dispatcher_metrics_dict,
1234
+ }
1235
+ )
1236
+
1237
+ # Log error with sanitized message
1238
+ # Note: Using logger.error() with sanitized message instead of
1239
+ # logger.exception() to avoid leaking sensitive data in stack traces.
1240
+ # The sanitized_error variable already contains safe error details.
1241
+ # TRY400: Intentionally using error() instead of exception() for security
1242
+ self._logger.error(
1243
+ "Dispatcher '%s' failed: %s",
1244
+ dispatcher_entry.dispatcher_id,
1245
+ sanitized_error,
1246
+ extra=self._build_log_context(
1247
+ topic=topic,
1248
+ category=topic_category,
1249
+ message_type=message_type,
1250
+ dispatcher_id=dispatcher_entry.dispatcher_id,
1251
+ duration_ms=dispatcher_duration_ms,
1252
+ correlation_id=correlation_id,
1253
+ trace_id=trace_id,
1254
+ error_code=EnumCoreErrorCode.HANDLER_EXECUTION_ERROR,
1255
+ ),
1256
+ )
1257
+
1258
+ # Step 6: Build result
1259
+ # Capture duration and completed_at together for consistency
1260
+ duration_ms = (time.perf_counter() - start_time) * 1000
1261
+ completed_at = datetime.now(UTC)
1262
+
1263
+ # Determine final status
1264
+ if dispatcher_errors:
1265
+ # Either partial or total failure
1266
+ status = EnumDispatchStatus.HANDLER_ERROR
1267
+ else:
1268
+ status = EnumDispatchStatus.SUCCESS
1269
+
1270
+ # Update all metrics atomically (protected by lock)
1271
+ with self._metrics_lock:
1272
+ # NOTE: dispatcher_id and handler_error are NOT passed here because
1273
+ # per-dispatcher metrics (including dispatcher_execution_count and
1274
+ # dispatcher_error_count) are already updated in the dispatcher loop
1275
+ # above. Passing them here would cause double-counting.
1276
+ self._structured_metrics = self._structured_metrics.record_dispatch(
1277
+ duration_ms=duration_ms,
1278
+ success=status == EnumDispatchStatus.SUCCESS,
1279
+ category=topic_category,
1280
+ dispatcher_id=None, # Already tracked in dispatcher loop
1281
+ handler_error=False, # Already tracked in dispatcher loop
1282
+ routes_matched=len(matching_dispatchers),
1283
+ topic=topic,
1284
+ error_message=dispatcher_errors[0] if dispatcher_errors else None,
1285
+ )
1286
+
1287
+ # Find route ID that matched (first matching route for logging)
1288
+ # Use empty string sentinel internally to avoid str | None union
1289
+ matched_route_id: str = ""
1290
+ for route in self._routes.values():
1291
+ if route.matches(topic, topic_category, message_type):
1292
+ matched_route_id = route.route_id
1293
+ break
1294
+
1295
+ # Log dispatch completion at INFO level
1296
+ # Use empty string sentinel to avoid str | None union in local scope
1297
+ dispatcher_ids_str: str = (
1298
+ ", ".join(executed_dispatcher_ids) if executed_dispatcher_ids else ""
1299
+ )
1300
+ if status == EnumDispatchStatus.SUCCESS:
1301
+ self._logger.info(
1302
+ "Dispatch completed successfully",
1303
+ extra=self._build_log_context(
1304
+ topic=topic,
1305
+ category=topic_category,
1306
+ message_type=message_type,
1307
+ dispatcher_id=dispatcher_ids_str,
1308
+ dispatcher_count=len(executed_dispatcher_ids),
1309
+ duration_ms=duration_ms,
1310
+ correlation_id=correlation_id,
1311
+ trace_id=trace_id,
1312
+ ),
1313
+ )
1314
+ else:
1315
+ self._logger.error(
1316
+ "Dispatch completed with errors",
1317
+ extra=self._build_log_context(
1318
+ topic=topic,
1319
+ category=topic_category,
1320
+ message_type=message_type,
1321
+ dispatcher_id=dispatcher_ids_str,
1322
+ dispatcher_count=len(matching_dispatchers),
1323
+ duration_ms=duration_ms,
1324
+ correlation_id=correlation_id,
1325
+ trace_id=trace_id,
1326
+ error_code=EnumCoreErrorCode.HANDLER_EXECUTION_ERROR,
1327
+ ),
1328
+ )
1329
+
1330
+ # Convert list of output topics to ModelDispatchOutputs
1331
+ # Handle Pydantic validation errors (e.g., invalid topic format)
1332
+ dispatch_outputs: ModelDispatchOutputs | None = None
1333
+ if outputs:
1334
+ try:
1335
+ dispatch_outputs = ModelDispatchOutputs(topics=outputs)
1336
+ except (ValueError, ValidationError) as validation_error:
1337
+ # Log validation failure with context (no secrets in topic names)
1338
+ # Note: Using sanitize_error_message for consistency, though topic
1339
+ # validation errors typically don't contain sensitive data
1340
+ sanitized_validation_error = sanitize_error_message(validation_error)
1341
+ # TRY400: Intentionally using error() instead of exception() for security
1342
+ # - exception() would log stack trace which may expose internal paths
1343
+ # - sanitized_validation_error already contains safe error details
1344
+ self._logger.error(
1345
+ "Failed to validate dispatch outputs (%d topics): %s",
1346
+ len(outputs),
1347
+ sanitized_validation_error,
1348
+ extra=self._build_log_context(
1349
+ topic=topic,
1350
+ category=topic_category,
1351
+ message_type=message_type,
1352
+ correlation_id=correlation_id,
1353
+ trace_id=trace_id,
1354
+ error_code=EnumCoreErrorCode.VALIDATION_ERROR,
1355
+ ),
1356
+ )
1357
+ # Add validation error to dispatcher_errors for result
1358
+ validation_error_msg = (
1359
+ f"Output validation failed: {sanitized_validation_error}"
1360
+ )
1361
+ dispatcher_errors.append(validation_error_msg)
1362
+ # Update status to reflect validation error
1363
+ status = EnumDispatchStatus.HANDLER_ERROR
1364
+
1365
+ # Construct final dispatch result with ValidationError protection
1366
+ # This ensures any Pydantic validation failure in ModelDispatchResult
1367
+ # is handled gracefully rather than propagating as an unhandled exception
1368
+ try:
1369
+ return ModelDispatchResult(
1370
+ dispatch_id=dispatch_id,
1371
+ status=status,
1372
+ route_id=matched_route_id,
1373
+ dispatcher_id=dispatcher_ids_str,
1374
+ topic=topic,
1375
+ message_category=topic_category,
1376
+ message_type=message_type,
1377
+ duration_ms=duration_ms,
1378
+ started_at=started_at,
1379
+ completed_at=completed_at,
1380
+ outputs=dispatch_outputs,
1381
+ output_count=len(outputs),
1382
+ error_message="; ".join(dispatcher_errors)
1383
+ if dispatcher_errors
1384
+ else None,
1385
+ error_code=EnumCoreErrorCode.HANDLER_EXECUTION_ERROR
1386
+ if dispatcher_errors
1387
+ else None,
1388
+ correlation_id=correlation_id,
1389
+ trace_id=trace_id,
1390
+ span_id=envelope.span_id,
1391
+ )
1392
+ except ValidationError as result_validation_error:
1393
+ # Pydantic validation failed during result construction
1394
+ # This is a critical internal error - log and return a minimal error result
1395
+ sanitized_result_error = sanitize_error_message(result_validation_error)
1396
+ # TRY400: Intentionally using error() instead of exception() for security
1397
+ self._logger.error(
1398
+ "Failed to construct ModelDispatchResult: %s",
1399
+ sanitized_result_error,
1400
+ extra=self._build_log_context(
1401
+ topic=topic,
1402
+ category=topic_category,
1403
+ message_type=message_type,
1404
+ correlation_id=correlation_id,
1405
+ trace_id=trace_id,
1406
+ error_code=EnumCoreErrorCode.INTERNAL_ERROR,
1407
+ ),
1408
+ )
1409
+ # Return a minimal fallback result that should always succeed
1410
+ return ModelDispatchResult(
1411
+ dispatch_id=dispatch_id,
1412
+ status=EnumDispatchStatus.INTERNAL_ERROR,
1413
+ topic=topic,
1414
+ started_at=started_at,
1415
+ completed_at=datetime.now(UTC),
1416
+ duration_ms=duration_ms,
1417
+ error_message=f"Internal error constructing dispatch result: {sanitized_result_error}",
1418
+ error_code=EnumCoreErrorCode.INTERNAL_ERROR,
1419
+ correlation_id=correlation_id,
1420
+ output_events=[],
1421
+ )
1422
+
1423
+ def _find_matching_dispatchers(
1424
+ self,
1425
+ topic: str,
1426
+ category: EnumMessageCategory,
1427
+ message_type: str,
1428
+ ) -> list[DispatchEntryInternal]:
1429
+ """
1430
+ Find all dispatchers that match the given criteria.
1431
+
1432
+ Matching is done in two phases:
1433
+ 1. Find routes that match topic pattern and category
1434
+ 2. Find dispatchers for those routes that accept the message type
1435
+
1436
+ Args:
1437
+ topic: The topic to match
1438
+ category: The message category
1439
+ message_type: The specific message type
1440
+
1441
+ Returns:
1442
+ List of matching dispatcher entries (may be empty)
1443
+ """
1444
+ matching_dispatchers: list[DispatchEntryInternal] = []
1445
+ seen_dispatcher_ids: set[str] = set()
1446
+
1447
+ # Find all routes that match this topic and category
1448
+ for route in self._routes.values():
1449
+ if not route.enabled:
1450
+ continue
1451
+ if not route.matches_topic(topic):
1452
+ continue
1453
+ if route.message_category != category:
1454
+ continue
1455
+ # Route-level message type filter (if specified)
1456
+ if route.message_type is not None and route.message_type != message_type:
1457
+ continue
1458
+
1459
+ # Get the dispatcher for this route
1460
+ dispatcher_id = route.dispatcher_id
1461
+ if dispatcher_id in seen_dispatcher_ids:
1462
+ # Avoid duplicate dispatcher execution
1463
+ continue
1464
+
1465
+ entry = self._dispatchers.get(dispatcher_id)
1466
+ if entry is None:
1467
+ # Dispatcher not found (should have been caught at freeze)
1468
+ self._logger.warning(
1469
+ "Route '%s' references missing dispatcher '%s'",
1470
+ route.route_id,
1471
+ dispatcher_id,
1472
+ )
1473
+ continue
1474
+
1475
+ # Check dispatcher-level message type filter
1476
+ if (
1477
+ entry.message_types is not None
1478
+ and message_type not in entry.message_types
1479
+ ):
1480
+ continue
1481
+
1482
+ matching_dispatchers.append(entry)
1483
+ seen_dispatcher_ids.add(dispatcher_id)
1484
+
1485
+ return matching_dispatchers
1486
+
1487
+ async def _execute_dispatcher(
1488
+ self,
1489
+ entry: DispatchEntryInternal,
1490
+ envelope: ModelEventEnvelope[object],
1491
+ topic: str,
1492
+ ) -> DispatcherOutput:
1493
+ """
1494
+ Execute a dispatcher (sync or async).
1495
+
1496
+ Sync dispatchers are executed via ``loop.run_in_executor()`` using the
1497
+ default ``ThreadPoolExecutor``. This allows sync code to run without
1498
+ blocking the event loop, but has important implications:
1499
+
1500
+ Thread Pool Considerations:
1501
+ - The default executor uses a limited thread pool (typically
1502
+ ``min(32, os.cpu_count() + 4)`` threads in Python 3.8+)
1503
+ - Each sync dispatcher execution consumes one thread until completion
1504
+ - Blocking dispatchers can exhaust the thread pool, causing:
1505
+ - Starvation of other sync dispatchers waiting for threads
1506
+ - Delayed scheduling of new async tasks
1507
+ - Potential deadlocks under high concurrent load
1508
+ - Increased latency for all executor-based operations
1509
+
1510
+ Best Practices:
1511
+ - Sync dispatchers SHOULD complete quickly (< 100ms recommended)
1512
+ - For blocking I/O (network, database, file), use async dispatchers
1513
+ - For CPU-bound work, consider using a dedicated ProcessPoolExecutor
1514
+ - Monitor ``dispatcher_execution_count`` metrics for bottlenecks
1515
+
1516
+ Args:
1517
+ entry: The dispatcher entry containing the callable
1518
+ envelope: The message envelope to process
1519
+
1520
+ Returns:
1521
+ DispatcherOutput: str (single topic), list[str] (multiple topics),
1522
+ or None (no output topics)
1523
+
1524
+ Raises:
1525
+ Any exception raised by the dispatcher
1526
+
1527
+ Warning:
1528
+ Sync dispatchers that block for extended periods (> 100ms) can
1529
+ severely degrade dispatch engine throughput. Prefer async dispatchers
1530
+ for any operation involving I/O or external service calls.
1531
+
1532
+ .. versionchanged:: 0.5.0
1533
+ Added support for context-aware dispatchers via ``node_kind``.
1534
+
1535
+ .. versionchanged:: 0.2.6
1536
+ Added binding resolution before handler execution (OMN-1518).
1537
+ """
1538
+ dispatcher = entry.dispatcher
1539
+
1540
+ # =================================================================
1541
+ # Binding Resolution Phase (OMN-1518)
1542
+ # =================================================================
1543
+ # ALWAYS materialize envelope to dict format for consistent dispatcher API.
1544
+ # INVARIANT: Original envelope is NEVER mutated.
1545
+ resolved_bindings: dict[str, JsonType] = {}
1546
+
1547
+ if entry.operation_bindings is not None:
1548
+ # Extract correlation_id FIRST for error context and tracing
1549
+ correlation_id = self._extract_correlation_id(envelope)
1550
+
1551
+ # Extract operation name from envelope for binding lookup (fail-fast)
1552
+ operation = self._extract_operation_from_envelope(envelope, correlation_id)
1553
+
1554
+ # Create context for binding resolution (see constants.VALID_CONTEXT_PATHS)
1555
+ dispatch_context: dict[str, object] = {
1556
+ "now_iso": datetime.now(UTC).isoformat(),
1557
+ "dispatcher_id": entry.dispatcher_id,
1558
+ "correlation_id": correlation_id,
1559
+ }
1560
+
1561
+ # Check for declared additional_context_paths that are not provided
1562
+ # This is a CONTRACT: if handler declares additional_context_paths,
1563
+ # the dispatch engine should provide them. Log warning if missing.
1564
+ declared_additional_paths = (
1565
+ entry.operation_bindings.additional_context_paths
1566
+ )
1567
+ if declared_additional_paths:
1568
+ missing_paths = [
1569
+ path
1570
+ for path in declared_additional_paths
1571
+ if path not in dispatch_context
1572
+ ]
1573
+ if missing_paths:
1574
+ self._logger.warning(
1575
+ "Dispatcher '%s' declares additional_context_paths %s "
1576
+ "but these are not provided in dispatch context. "
1577
+ "Bindings using these paths will resolve to None unless "
1578
+ "they have defaults. correlation_id=%s",
1579
+ entry.dispatcher_id,
1580
+ missing_paths,
1581
+ correlation_id,
1582
+ )
1583
+
1584
+ # Resolve all bindings for this operation
1585
+ resolution = self._binding_resolver.resolve(
1586
+ operation=operation,
1587
+ bindings_subcontract=entry.operation_bindings,
1588
+ envelope=envelope,
1589
+ context=dispatch_context,
1590
+ correlation_id=correlation_id,
1591
+ )
1592
+
1593
+ if not resolution.success:
1594
+ # Fail fast on binding resolution failure
1595
+ raise BindingResolutionError(
1596
+ f"Binding resolution failed: {resolution.error}",
1597
+ operation_name=resolution.operation_name,
1598
+ parameter_name="unknown",
1599
+ expression="unknown",
1600
+ correlation_id=correlation_id,
1601
+ )
1602
+
1603
+ # dict() creates a shallow copy to avoid mutating the resolution result
1604
+ resolved_bindings = dict(resolution.resolved_parameters)
1605
+
1606
+ # ALWAYS materialize envelope with bindings (never mutate original)
1607
+ # Empty dict for __bindings if no bindings configured
1608
+ envelope_for_handler: dict[str, JsonType] = (
1609
+ self._materialize_envelope_with_bindings(envelope, resolved_bindings, topic)
1610
+ )
1611
+
1612
+ # =================================================================
1613
+ # Context Creation Phase
1614
+ # =================================================================
1615
+ # Create context ONLY if both conditions are met:
1616
+ # 1. node_kind is set (time injection rules apply)
1617
+ # 2. dispatcher accepts context (will actually use it)
1618
+ # This avoids unnecessary object creation on the dispatch hot path when
1619
+ # a dispatcher has node_kind set but doesn't accept a context parameter.
1620
+ context: ModelDispatchContext | None = None
1621
+ if entry.node_kind is not None and entry.accepts_context:
1622
+ context = self._create_context_for_entry(entry, envelope)
1623
+
1624
+ # =================================================================
1625
+ # Dispatcher Execution Phase
1626
+ # =================================================================
1627
+ # Check if dispatcher is async
1628
+ # Note: context is only non-None when entry.accepts_context is True,
1629
+ # so checking `context is not None` is sufficient to determine whether
1630
+ # to pass context to the dispatcher.
1631
+ if inspect.iscoroutinefunction(dispatcher):
1632
+ if context is not None:
1633
+ # NOTE: Dispatcher signature varies - context param may be optional.
1634
+ # Return type depends on dispatcher implementation (dict or model).
1635
+ return await dispatcher(envelope_for_handler, context) # type: ignore[call-arg,no-any-return] # NOTE: dispatcher signature varies
1636
+ # NOTE: Return type depends on dispatcher implementation (dict or model).
1637
+ return await dispatcher(envelope_for_handler) # type: ignore[no-any-return] # NOTE: dispatcher return type varies
1638
+ else:
1639
+ # Sync dispatcher execution via ThreadPoolExecutor
1640
+ # -----------------------------------------------
1641
+ # WARNING: Sync dispatchers MUST be non-blocking (< 100ms execution).
1642
+ # Blocking dispatchers can exhaust the thread pool, causing:
1643
+ # - Starvation of other sync dispatchers
1644
+ # - Delayed async dispatcher scheduling
1645
+ # - Potential deadlocks under high load
1646
+ #
1647
+ # For blocking I/O operations, use async dispatchers instead.
1648
+ loop = asyncio.get_running_loop()
1649
+
1650
+ if context is not None:
1651
+ # Context-aware sync dispatcher
1652
+ sync_ctx_dispatcher = cast(
1653
+ "_SyncContextAwareDispatcherFunc", dispatcher
1654
+ )
1655
+ # NOTE: run_in_executor arg-type check fails because envelope_for_handler
1656
+ # is dict[str, JsonType] but dispatcher expects dict[str, object].
1657
+ # JsonType is a subset of object, so this is safe at runtime.
1658
+ return await loop.run_in_executor(
1659
+ None,
1660
+ sync_ctx_dispatcher, # type: ignore[arg-type]
1661
+ envelope_for_handler,
1662
+ context,
1663
+ )
1664
+ else:
1665
+ # Cast to sync-only type - safe because iscoroutinefunction check above
1666
+ # guarantees this branch only executes for non-async callables
1667
+ sync_dispatcher = cast("_SyncDispatcherFunc", dispatcher)
1668
+ # NOTE: run_in_executor arg-type check fails because envelope_for_handler
1669
+ # is dict[str, JsonType] but dispatcher expects dict[str, object].
1670
+ # JsonType is a subset of object, so this is safe at runtime.
1671
+ return await loop.run_in_executor(
1672
+ None,
1673
+ sync_dispatcher, # type: ignore[arg-type]
1674
+ envelope_for_handler,
1675
+ )
1676
+
1677
+ def _create_context_for_entry(
1678
+ self,
1679
+ entry: DispatchEntryInternal,
1680
+ envelope: ModelEventEnvelope[object],
1681
+ ) -> ModelDispatchContext:
1682
+ """
1683
+ Create dispatch context based on entry's node_kind.
1684
+
1685
+ Delegates to DispatchContextEnforcer.create_context_for_node_kind() to
1686
+ ensure a single source of truth for time injection rules. This method
1687
+ is a thin wrapper that validates node_kind is not None before delegation.
1688
+
1689
+ Creates a ModelDispatchContext with appropriate time injection based on
1690
+ the ONEX node kind:
1691
+ - REDUCER: now=None (deterministic state aggregation)
1692
+ - COMPUTE: now=None (pure transformation)
1693
+ - ORCHESTRATOR: now=datetime.now(UTC) (coordination)
1694
+ - EFFECT: now=datetime.now(UTC) (I/O operations)
1695
+ - RUNTIME_HOST: now=datetime.now(UTC) (infrastructure)
1696
+
1697
+ Args:
1698
+ entry: The dispatcher entry containing node_kind.
1699
+ envelope: The event envelope containing correlation metadata.
1700
+
1701
+ Returns:
1702
+ ModelDispatchContext configured appropriately for the node kind.
1703
+
1704
+ Raises:
1705
+ ModelOnexError: If node_kind is None or unrecognized.
1706
+
1707
+ Note:
1708
+ This is an internal method. Callers should ensure entry.node_kind
1709
+ is not None before calling.
1710
+
1711
+ Time Semantics:
1712
+ The ``now`` field is captured at context creation time (dispatch time),
1713
+ NOT at handler execution time. For ORCHESTRATOR, EFFECT, and RUNTIME_HOST
1714
+ nodes, this means:
1715
+
1716
+ - ``now`` represents when MessageDispatchEngine created the context
1717
+ - Handler execution may occur microseconds to milliseconds later
1718
+ - For most use cases, this drift is negligible
1719
+ - If sub-millisecond precision is required, handlers should capture
1720
+ their own time at the start of execution
1721
+
1722
+ .. versionadded:: 0.5.0
1723
+ .. versionchanged:: 0.5.1
1724
+ Now delegates to DispatchContextEnforcer.create_context_for_node_kind()
1725
+ to eliminate code duplication.
1726
+ """
1727
+ node_kind = entry.node_kind
1728
+ if node_kind is None:
1729
+ raise ModelOnexError(
1730
+ message=f"Cannot create context for dispatcher '{entry.dispatcher_id}': "
1731
+ "node_kind is None. This is an internal error.",
1732
+ error_code=EnumCoreErrorCode.INTERNAL_ERROR,
1733
+ )
1734
+
1735
+ # Delegate to the shared context enforcer for time injection rules.
1736
+ # This eliminates duplication between MessageDispatchEngine and any
1737
+ # other components that need to create contexts based on node_kind.
1738
+ return self._context_enforcer.create_context_for_node_kind(
1739
+ node_kind=node_kind,
1740
+ envelope=envelope,
1741
+ dispatcher_id=entry.dispatcher_id,
1742
+ )
1743
+
1744
+ def _dispatcher_accepts_context(
1745
+ self,
1746
+ dispatcher: DispatcherFunc | ContextAwareDispatcherFunc,
1747
+ ) -> bool:
1748
+ """
1749
+ Check if a dispatcher callable accepts a context parameter.
1750
+
1751
+ Uses inspect.signature to determine if the dispatcher has a second
1752
+ parameter for ModelDispatchContext. This enables backwards-compatible
1753
+ context injection - dispatchers without a context parameter will be
1754
+ called with just the envelope.
1755
+
1756
+ This method is called once at registration time and the result is
1757
+ cached in DispatchEntryInternal.accepts_context for performance.
1758
+ No signature inspection occurs during dispatch execution.
1759
+
1760
+ Type Safety Warnings:
1761
+ When a dispatcher has 2+ parameters but the second parameter doesn't
1762
+ follow conventional naming (containing 'context' or 'ctx'), a warning
1763
+ is logged to help developers identify potential signature mismatches.
1764
+ This is non-blocking - the method still returns True for backwards
1765
+ compatibility with existing dispatchers.
1766
+
1767
+ Args:
1768
+ dispatcher: The dispatcher callable to inspect.
1769
+
1770
+ Returns:
1771
+ True if dispatcher accepts a context parameter, False otherwise.
1772
+
1773
+ .. versionadded:: 0.5.0
1774
+ .. versionchanged:: 0.5.1
1775
+ Added warning logging for unconventional parameter naming.
1776
+ """
1777
+ try:
1778
+ sig = inspect.signature(dispatcher)
1779
+ params = list(sig.parameters.values())
1780
+ # Dispatcher with context has 2+ parameters: (envelope, context, ...)
1781
+ # Dispatcher without context has 1 parameter: (envelope)
1782
+ #
1783
+ # Design Decision: We use >= MIN_PARAMS_FOR_CONTEXT (not ==) intentionally
1784
+ # to support:
1785
+ # - Future extensibility (e.g., envelope, context, **kwargs)
1786
+ # - Dispatchers with additional optional parameters for testing/logging
1787
+ # - Protocol compliance without strict arity enforcement
1788
+ #
1789
+ # Strict == MIN_PARAMS_FOR_CONTEXT would reject valid dispatchers that
1790
+ # happen to have extra optional parameters, which is unnecessarily restrictive.
1791
+ if len(params) < MIN_PARAMS_FOR_CONTEXT:
1792
+ return False
1793
+
1794
+ # Type safety enhancement: Warn if second parameter doesn't follow
1795
+ # context naming convention. This helps developers identify potential
1796
+ # signature mismatches where a 2+ parameter dispatcher might not
1797
+ # actually expect a ModelDispatchContext.
1798
+ #
1799
+ # This is NON-BLOCKING - we still return True.
1800
+ # The warning is informational to help improve code quality.
1801
+ second_param = params[1]
1802
+ second_name = second_param.name.lower()
1803
+ if "context" not in second_name and "ctx" not in second_name:
1804
+ dispatcher_name = getattr(dispatcher, "__name__", str(dispatcher))
1805
+ self._logger.warning(
1806
+ "Dispatcher '%s' has 2+ parameters but second parameter '%s' "
1807
+ "doesn't follow context naming convention. "
1808
+ "Expected parameter name containing 'context' or 'ctx'. "
1809
+ "If this dispatcher expects a ModelDispatchContext, consider "
1810
+ "renaming the parameter for clarity.",
1811
+ dispatcher_name,
1812
+ second_param.name,
1813
+ )
1814
+
1815
+ return True
1816
+ except (ValueError, TypeError) as e:
1817
+ # If we can't inspect the signature, assume no context and log warning
1818
+ self._logger.warning(
1819
+ "Failed to inspect dispatcher signature: %s. "
1820
+ "Assuming no context parameter. Uninspectable dispatchers "
1821
+ "(C extensions, certain decorators) will receive envelope only.",
1822
+ e,
1823
+ )
1824
+ return False
1825
+
1826
+ def _extract_operation_from_envelope(
1827
+ self, envelope: object, correlation_id: UUID | None = None
1828
+ ) -> str:
1829
+ """Extract operation name from envelope.
1830
+
1831
+ Supports both dict-based envelopes and Pydantic model envelopes
1832
+ with an ``operation`` attribute.
1833
+
1834
+ Args:
1835
+ envelope: Event envelope (dict or Pydantic model).
1836
+ correlation_id: Optional correlation ID for error context.
1837
+
1838
+ Returns:
1839
+ Operation name string.
1840
+
1841
+ Raises:
1842
+ BindingResolutionError: When operation cannot be extracted. This
1843
+ error is raised in the following cases:
1844
+
1845
+ 1. **Dict envelope without ``operation`` key**: The envelope is a dict
1846
+ but does not contain an ``"operation"`` key.
1847
+ 2. **Model envelope without ``operation`` attribute**: The envelope is
1848
+ a Pydantic model or object but lacks an ``operation`` attribute.
1849
+ 3. **Empty/None operation value**: The operation key/attribute exists
1850
+ but has a None or falsy value.
1851
+
1852
+ Fail-Fast Behavior:
1853
+ This method implements fail-fast semantics. If an operation cannot be
1854
+ extracted, it raises ``BindingResolutionError`` immediately rather than
1855
+ returning a fallback value. This ensures:
1856
+
1857
+ - Early detection of misconfigured envelopes
1858
+ - Clear error messages with diagnostic context
1859
+ - No silent failures from attempting to resolve bindings for "unknown" operation
1860
+
1861
+ Note:
1862
+ If ``entry.operation_bindings`` is ``None`` (no bindings configured),
1863
+ this method is not called at all, avoiding unnecessary extraction.
1864
+
1865
+ .. versionadded:: 0.2.6
1866
+ Added as part of OMN-1518 - Declarative operation bindings.
1867
+
1868
+ .. versionchanged:: 0.2.7
1869
+ Changed from fallback-to-unknown to fail-fast behavior.
1870
+ """
1871
+ # Dict-based envelopes (common for Kafka/JSON payloads)
1872
+ if isinstance(envelope, dict):
1873
+ operation = envelope.get("operation")
1874
+ if operation is not None:
1875
+ return str(operation)
1876
+ # NOTE: Do not log envelope.keys() - may contain sensitive field names
1877
+ raise BindingResolutionError(
1878
+ "Operation extraction failed: dict envelope missing 'operation' key. "
1879
+ f"Ensure envelope contains 'operation' key (found {len(envelope)} keys).",
1880
+ operation_name="extraction_failed",
1881
+ parameter_name="operation",
1882
+ expression="envelope['operation']",
1883
+ missing_segment="operation",
1884
+ correlation_id=correlation_id,
1885
+ )
1886
+
1887
+ # Pydantic model or object with operation attribute
1888
+ if hasattr(envelope, "operation"):
1889
+ operation = getattr(envelope, "operation", None)
1890
+ if operation is not None:
1891
+ return str(operation)
1892
+ raise BindingResolutionError(
1893
+ "Operation extraction failed: envelope has 'operation' attribute "
1894
+ f"but value is None or falsy. Envelope type: {type(envelope).__name__}",
1895
+ operation_name="extraction_failed",
1896
+ parameter_name="operation",
1897
+ expression="envelope.operation",
1898
+ missing_segment="operation",
1899
+ correlation_id=correlation_id,
1900
+ )
1901
+
1902
+ # No operation attribute at all
1903
+ raise BindingResolutionError(
1904
+ "Operation extraction failed: envelope has no 'operation' attribute. "
1905
+ f"Envelope type: {type(envelope).__name__}",
1906
+ operation_name="extraction_failed",
1907
+ parameter_name="operation",
1908
+ expression="envelope.operation or envelope['operation']",
1909
+ missing_segment="operation",
1910
+ correlation_id=correlation_id,
1911
+ )
1912
+
1913
+ def _extract_correlation_id(self, envelope: object) -> UUID | None:
1914
+ """Extract correlation_id from envelope.
1915
+
1916
+ Supports both dict-based envelopes and Pydantic model envelopes
1917
+ with a ``correlation_id`` attribute. Handles both UUID and string values.
1918
+
1919
+ Args:
1920
+ envelope: Event envelope (dict or Pydantic model).
1921
+
1922
+ Returns:
1923
+ UUID correlation_id if found and valid, None otherwise.
1924
+
1925
+ .. versionadded:: 0.2.6
1926
+ Added as part of OMN-1518 - Declarative operation bindings.
1927
+ """
1928
+ cid: object = None
1929
+ if isinstance(envelope, dict):
1930
+ cid = envelope.get("correlation_id")
1931
+ elif hasattr(envelope, "correlation_id"):
1932
+ cid = getattr(envelope, "correlation_id", None)
1933
+
1934
+ if isinstance(cid, UUID):
1935
+ return cid
1936
+ if isinstance(cid, str):
1937
+ try:
1938
+ return UUID(cid)
1939
+ except ValueError:
1940
+ return None
1941
+ return None
1942
+
1943
+ def _materialize_envelope_with_bindings(
1944
+ self,
1945
+ original_envelope: object,
1946
+ resolved_bindings: dict[str, JsonType],
1947
+ topic: str,
1948
+ ) -> dict[str, JsonType]:
1949
+ """Create new JSON-safe envelope with __bindings namespace.
1950
+
1951
+ INVARIANT: original_envelope is NEVER mutated.
1952
+ INVARIANT: All output values are JSON-serializable.
1953
+
1954
+ This method ALWAYS creates a new dict containing:
1955
+ - ``payload``: Event payload as JSON-safe dict
1956
+ - ``__bindings``: Resolved binding parameters (empty dict if no bindings)
1957
+ - ``__debug_trace``: Serialized trace metadata snapshot (debug only)
1958
+
1959
+ The dispatch boundary is a **serialization boundary**. All data crossing
1960
+ this layer must be transport-safe (JSON-serializable) to enable:
1961
+ - Event replay from logs or Kafka
1962
+ - Distributed dispatch across processes
1963
+ - Observability tooling (logging, tracing, dashboards)
1964
+
1965
+ Warning:
1966
+ ``__debug_trace`` is provided ONLY for debugging and observability.
1967
+ It is a serialized snapshot of trace metadata, NOT the live envelope.
1968
+
1969
+ **DO NOT**:
1970
+ - Use ``__debug_trace`` for business logic
1971
+ - Assume ``__debug_trace`` reflects complete envelope state
1972
+ - Depend on specific fields being present
1973
+
1974
+ Args:
1975
+ original_envelope: Original event envelope (dict or model).
1976
+ resolved_bindings: JSON-safe resolved binding parameters (may be empty).
1977
+ topic: The topic this message was received on.
1978
+
1979
+ Returns:
1980
+ New dict conforming to ModelMaterializedDispatch schema.
1981
+ All values are JSON-serializable (transport-safe).
1982
+
1983
+ Example:
1984
+ >>> materialized = engine._materialize_envelope_with_bindings(
1985
+ ... original_envelope={"payload": {"sql": "SELECT 1"}},
1986
+ ... resolved_bindings={"sql": "SELECT 1", "limit": 100},
1987
+ ... topic="dev.db.commands.v1",
1988
+ ... )
1989
+ >>> materialized["__bindings"]
1990
+ {'sql': 'SELECT 1', 'limit': 100}
1991
+
1992
+ .. versionadded:: 0.2.6
1993
+ Added as part of OMN-1518 - Declarative operation bindings.
1994
+
1995
+ .. versionchanged:: 0.2.8
1996
+ Changed to strict JSON-safe contract:
1997
+ - Payload is serialized to dict (Pydantic models call model_dump)
1998
+ - Bindings values are serialized (UUIDs/datetimes → strings)
1999
+ - __debug_original_envelope replaced with __debug_trace snapshot
2000
+ - Return type changed to dict[str, JsonType]
2001
+ """
2002
+ # Extract and serialize payload to JSON-safe dict
2003
+ payload_json = self._serialize_payload(original_envelope)
2004
+
2005
+ # Serialize bindings to JSON-safe values
2006
+ bindings_json = self._serialize_bindings(resolved_bindings)
2007
+
2008
+ # Create debug trace snapshot (serialized, non-authoritative)
2009
+ debug_trace = self._create_debug_trace_snapshot(original_envelope, topic)
2010
+
2011
+ # Build materialized dict conforming to ModelMaterializedDispatch schema
2012
+ # NOTE: debug_trace is dict[str, str | None] which is a subset of JsonType
2013
+ materialized: dict[str, JsonType] = {
2014
+ "payload": payload_json,
2015
+ "__bindings": bindings_json,
2016
+ "__debug_trace": cast("JsonType", debug_trace),
2017
+ }
2018
+
2019
+ # Validate against schema to enforce contract invariants
2020
+ # This catches shape drift and provides clear error messages
2021
+ ModelMaterializedDispatch.model_validate(materialized)
2022
+
2023
+ return materialized
2024
+
2025
+ def _serialize_payload(self, original_envelope: object) -> JsonType:
2026
+ """Extract and serialize payload to JSON-safe dict.
2027
+
2028
+ Handlers that need typed Pydantic models should hydrate locally:
2029
+ ``ModelFoo.model_validate(dispatch["payload"])``
2030
+
2031
+ Args:
2032
+ original_envelope: Original event envelope (dict or model).
2033
+
2034
+ Returns:
2035
+ JSON-safe payload (dict for complex types, wrapped for primitives).
2036
+ """
2037
+ # Extract original payload
2038
+ original_payload: object
2039
+ if isinstance(original_envelope, dict):
2040
+ original_payload = original_envelope.get("payload", original_envelope)
2041
+ elif hasattr(original_envelope, "payload"):
2042
+ original_payload = getattr(original_envelope, "payload", original_envelope)
2043
+ else:
2044
+ original_payload = original_envelope
2045
+
2046
+ # Serialize to JSON-safe format
2047
+ if hasattr(original_payload, "model_dump"):
2048
+ # Pydantic model - serialize to dict with JSON mode
2049
+ # NOTE: model_dump returns Any, but we know it's JSON-compatible
2050
+ return original_payload.model_dump(mode="json") # type: ignore[union-attr, no-any-return]
2051
+ elif isinstance(original_payload, dict):
2052
+ # Dict - recursively serialize values
2053
+ return self._serialize_dict_values(original_payload)
2054
+ elif isinstance(original_payload, (str, int, float, bool, type(None))):
2055
+ # JSON primitive - wrap to maintain dict structure
2056
+ # NOTE: This is a last-resort escape hatch. Prefer dict payloads.
2057
+ return {"_raw": original_payload}
2058
+ elif isinstance(original_payload, list):
2059
+ # List - recursively serialize elements
2060
+ return [self._serialize_value(item) for item in original_payload]
2061
+ elif hasattr(original_payload, "__dict__"):
2062
+ # Plain Python object with attributes - serialize its __dict__
2063
+ # This handles domain objects that aren't Pydantic models
2064
+ self._logger.warning(
2065
+ "Serializing payload via __dict__ fallback for type %s. "
2066
+ "Consider using a Pydantic model for explicit serialization control.",
2067
+ type(original_payload).__name__,
2068
+ )
2069
+ return self._serialize_dict_values(vars(original_payload))
2070
+ else:
2071
+ # Unknown type - attempt string conversion
2072
+ self._logger.warning(
2073
+ "Serializing payload via string fallback for type %s. "
2074
+ "This may indicate a configuration error - payload types should be "
2075
+ "Pydantic models, dicts, or JSON primitives.",
2076
+ type(original_payload).__name__,
2077
+ )
2078
+ return {"_raw": str(original_payload)}
2079
+
2080
+ def _serialize_bindings(self, bindings: dict[str, JsonType]) -> dict[str, JsonType]:
2081
+ """Ensure binding values are JSON-safe (idempotent serialization).
2082
+
2083
+ When bindings are already JSON-safe (from OperationBindingResolver),
2084
+ this method is effectively a no-op but provides defensive serialization.
2085
+
2086
+ Args:
2087
+ bindings: Dict of resolved binding parameters (already JSON-safe).
2088
+
2089
+ Returns:
2090
+ Dict with JSON-safe values.
2091
+ """
2092
+ return {key: self._serialize_value(value) for key, value in bindings.items()}
2093
+
2094
+ def _serialize_value(self, value: object) -> JsonType:
2095
+ """Serialize a single value to JSON-safe type.
2096
+
2097
+ Args:
2098
+ value: Any value to serialize.
2099
+
2100
+ Returns:
2101
+ JSON-safe representation.
2102
+ """
2103
+ if value is None or isinstance(value, (str, int, float, bool)):
2104
+ return value # type: ignore[return-value]
2105
+ elif isinstance(value, UUID):
2106
+ return str(value)
2107
+ elif isinstance(value, datetime):
2108
+ return value.isoformat()
2109
+ elif hasattr(value, "model_dump"):
2110
+ # Pydantic model - model_dump returns Any, but we know it's JSON-compatible
2111
+ return value.model_dump(mode="json") # type: ignore[union-attr, no-any-return]
2112
+ elif isinstance(value, dict):
2113
+ return self._serialize_dict_values(value)
2114
+ elif isinstance(value, list):
2115
+ return [self._serialize_value(item) for item in value]
2116
+ else:
2117
+ # Unknown type - string conversion
2118
+ return str(value)
2119
+
2120
+ def _serialize_dict_values(self, d: dict[str, object]) -> dict[str, JsonType]:
2121
+ """Recursively serialize dict values."""
2122
+ return {key: self._serialize_value(value) for key, value in d.items()}
2123
+
2124
+ def _create_debug_trace_snapshot(
2125
+ self, original_envelope: object, topic: str
2126
+ ) -> dict[str, str | None]:
2127
+ """Create serialized trace metadata snapshot.
2128
+
2129
+ This snapshot is for debugging and observability ONLY.
2130
+ It is NOT authoritative and should NOT be used for business logic.
2131
+
2132
+ Args:
2133
+ original_envelope: Original event envelope.
2134
+ topic: The topic this message was received on.
2135
+
2136
+ Returns:
2137
+ Dict with serialized trace metadata (all strings or None).
2138
+ """
2139
+ # Extract trace fields safely (any missing field → None)
2140
+ event_type: str | None = None
2141
+ correlation_id: str | None = None
2142
+ trace_id: str | None = None
2143
+ causation_id: str | None = None
2144
+ timestamp: str | None = None
2145
+ partition_key: str | None = None
2146
+
2147
+ if isinstance(original_envelope, dict):
2148
+ event_type = self._safe_str(original_envelope.get("event_type"))
2149
+ correlation_id = self._safe_str(original_envelope.get("correlation_id"))
2150
+ trace_id = self._safe_str(original_envelope.get("trace_id"))
2151
+ causation_id = self._safe_str(original_envelope.get("causation_id"))
2152
+ timestamp = self._safe_str(original_envelope.get("timestamp"))
2153
+ partition_key = self._safe_str(original_envelope.get("partition_key"))
2154
+ else:
2155
+ # Pydantic model or other object - use getattr
2156
+ # All fields go through _safe_str to handle non-string types gracefully
2157
+ event_type = self._safe_str(getattr(original_envelope, "event_type", None))
2158
+ correlation_id = self._safe_str(
2159
+ getattr(original_envelope, "correlation_id", None)
2160
+ )
2161
+ trace_id = self._safe_str(getattr(original_envelope, "trace_id", None))
2162
+ causation_id = self._safe_str(
2163
+ getattr(original_envelope, "causation_id", None)
2164
+ )
2165
+ timestamp = self._safe_str(getattr(original_envelope, "timestamp", None))
2166
+ partition_key = self._safe_str(
2167
+ getattr(original_envelope, "partition_key", None)
2168
+ )
2169
+
2170
+ # Build snapshot using the model for validation
2171
+ snapshot = ModelDebugTraceSnapshot(
2172
+ event_type=event_type,
2173
+ correlation_id=correlation_id,
2174
+ trace_id=trace_id,
2175
+ causation_id=causation_id,
2176
+ topic=topic,
2177
+ timestamp=timestamp,
2178
+ partition_key=partition_key,
2179
+ )
2180
+
2181
+ return snapshot.model_dump()
2182
+
2183
+ def _safe_str(self, value: object) -> str | None:
2184
+ """Safely convert value to string or None.
2185
+
2186
+ This method is defensive - it only returns a string if the value
2187
+ can be meaningfully converted. Mock objects and other test artifacts
2188
+ are filtered out to avoid polluting trace snapshots.
2189
+ """
2190
+ if value is None:
2191
+ return None
2192
+ if isinstance(value, str):
2193
+ return value
2194
+ if isinstance(value, UUID):
2195
+ return str(value)
2196
+ if isinstance(value, datetime):
2197
+ return value.isoformat()
2198
+ # For other types, only convert if it results in a meaningful string
2199
+ # Filter out mock objects and other test artifacts
2200
+ str_value = str(value)
2201
+
2202
+ # Check for mock objects by module (most reliable)
2203
+ value_module = type(value).__module__
2204
+ if value_module.startswith("unittest.mock"):
2205
+ return None
2206
+
2207
+ # Check for common mock patterns in string representation
2208
+ mock_patterns = ("Mock", "MagicMock", "AsyncMock")
2209
+ if any(pattern in str_value for pattern in mock_patterns):
2210
+ return None
2211
+
2212
+ # Check for Python internal repr patterns (more specific than just <...>)
2213
+ # These patterns indicate non-serializable internal objects
2214
+ if str_value.startswith("<") and str_value.endswith(">"):
2215
+ internal_prefixes = (
2216
+ "<class ",
2217
+ "<function ",
2218
+ "<module ",
2219
+ "<bound method ",
2220
+ "<built-in ",
2221
+ "<coroutine ",
2222
+ "<generator ",
2223
+ "<async_generator ",
2224
+ )
2225
+ if any(str_value.startswith(prefix) for prefix in internal_prefixes):
2226
+ return None
2227
+
2228
+ return str_value
2229
+
2230
+ def get_structured_metrics(self) -> ModelDispatchMetrics:
2231
+ """
2232
+ Get structured dispatch metrics using Pydantic model.
2233
+
2234
+ Returns a comprehensive metrics model including:
2235
+ - Dispatch counts and success/error rates
2236
+ - Latency statistics (average, min, max)
2237
+ - Latency histogram for distribution analysis
2238
+ - Per-dispatcher metrics breakdown
2239
+ - Per-category metrics breakdown
2240
+
2241
+ Thread Safety:
2242
+ This method acquires ``_metrics_lock`` to return a consistent snapshot.
2243
+ The same lock protects all metrics updates, ensuring TOCTOU-safe
2244
+ read-modify-write operations during dispatch. The returned Pydantic
2245
+ model is immutable and safe to use after the lock is released.
2246
+
2247
+ Returns:
2248
+ ModelDispatchMetrics with all observability data
2249
+
2250
+ Example:
2251
+ >>> metrics = engine.get_structured_metrics()
2252
+ >>> print(f"Success rate: {metrics.success_rate:.1%}")
2253
+ >>> print(f"Avg latency: {metrics.avg_latency_ms:.2f} ms")
2254
+ >>> for dispatcher_id, dispatcher_metrics in metrics.dispatcher_metrics.items():
2255
+ ... print(f"Dispatcher {dispatcher_id}: {dispatcher_metrics.execution_count} executions")
2256
+
2257
+ .. versionadded:: 0.4.0
2258
+ """
2259
+ # Return under lock to ensure consistent snapshot
2260
+ with self._metrics_lock:
2261
+ return self._structured_metrics
2262
+
2263
+ def reset_metrics(self) -> None:
2264
+ """
2265
+ Reset all metrics to initial state.
2266
+
2267
+ Useful for testing or when starting a new monitoring period.
2268
+
2269
+ Thread Safety:
2270
+ This method acquires ``_metrics_lock`` to ensure atomic reset
2271
+ of all metrics. Safe to call during concurrent dispatch operations,
2272
+ though the reset will briefly block in-flight metric updates.
2273
+
2274
+ Example:
2275
+ >>> engine.reset_metrics()
2276
+ >>> assert engine.get_structured_metrics().total_dispatches == 0
2277
+
2278
+ .. versionadded:: 0.4.0
2279
+ """
2280
+ with self._metrics_lock:
2281
+ self._structured_metrics = ModelDispatchMetrics()
2282
+ self._logger.debug("Metrics reset to initial state")
2283
+
2284
+ def get_dispatcher_metrics(
2285
+ self, dispatcher_id: str
2286
+ ) -> ModelDispatcherMetrics | None:
2287
+ """
2288
+ Get metrics for a specific dispatcher.
2289
+
2290
+ Thread Safety:
2291
+ This method acquires ``_metrics_lock`` to return a consistent snapshot.
2292
+ The returned Pydantic model is immutable and safe to use after the
2293
+ lock is released.
2294
+
2295
+ Args:
2296
+ dispatcher_id: The dispatcher's unique identifier.
2297
+
2298
+ Returns:
2299
+ ModelDispatcherMetrics for the dispatcher, or None if no metrics recorded.
2300
+
2301
+ Example:
2302
+ >>> metrics = engine.get_dispatcher_metrics("user-event-dispatcher")
2303
+ >>> if metrics:
2304
+ ... print(f"Executions: {metrics.execution_count}")
2305
+ ... print(f"Error rate: {metrics.error_rate:.1%}")
2306
+
2307
+ .. versionadded:: 0.4.0
2308
+ """
2309
+ with self._metrics_lock:
2310
+ return self._structured_metrics.dispatcher_metrics.get(dispatcher_id)
2311
+
2312
+ @property
2313
+ def route_count(self) -> int:
2314
+ """Get the number of registered routes."""
2315
+ return len(self._routes)
2316
+
2317
+ @property
2318
+ def dispatcher_count(self) -> int:
2319
+ """Get the number of registered dispatchers."""
2320
+ return len(self._dispatchers)
2321
+
2322
+ def __str__(self) -> str:
2323
+ """Human-readable string representation."""
2324
+ return (
2325
+ f"MessageDispatchEngine[routes={len(self._routes)}, "
2326
+ f"dispatchers={len(self._dispatchers)}, frozen={self._frozen}]"
2327
+ )
2328
+
2329
+ def __repr__(self) -> str:
2330
+ """Detailed representation for debugging."""
2331
+ route_ids = list(self._routes.keys())[:10]
2332
+ dispatcher_ids = list(self._dispatchers.keys())[:10]
2333
+
2334
+ route_repr = (
2335
+ repr(route_ids)
2336
+ if len(self._routes) <= 10
2337
+ else f"<{len(self._routes)} routes>"
2338
+ )
2339
+ dispatcher_repr = (
2340
+ repr(dispatcher_ids)
2341
+ if len(self._dispatchers) <= 10
2342
+ else f"<{len(self._dispatchers)} dispatchers>"
2343
+ )
2344
+
2345
+ return (
2346
+ f"MessageDispatchEngine("
2347
+ f"routes={route_repr}, "
2348
+ f"dispatchers={dispatcher_repr}, "
2349
+ f"frozen={self._frozen})"
2350
+ )