omnibase_infra 0.3.1__py3-none-any.whl → 0.4.0__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 (117) hide show
  1. omnibase_infra/__init__.py +1 -1
  2. omnibase_infra/enums/__init__.py +3 -0
  3. omnibase_infra/enums/enum_consumer_group_purpose.py +9 -0
  4. omnibase_infra/enums/enum_postgres_error_code.py +188 -0
  5. omnibase_infra/errors/__init__.py +4 -0
  6. omnibase_infra/errors/error_infra.py +60 -0
  7. omnibase_infra/handlers/__init__.py +3 -0
  8. omnibase_infra/handlers/handler_slack_webhook.py +426 -0
  9. omnibase_infra/handlers/models/__init__.py +14 -0
  10. omnibase_infra/handlers/models/enum_alert_severity.py +36 -0
  11. omnibase_infra/handlers/models/model_slack_alert.py +24 -0
  12. omnibase_infra/handlers/models/model_slack_alert_payload.py +77 -0
  13. omnibase_infra/handlers/models/model_slack_alert_result.py +73 -0
  14. omnibase_infra/handlers/registration_storage/handler_registration_storage_postgres.py +29 -20
  15. omnibase_infra/mixins/__init__.py +14 -0
  16. omnibase_infra/mixins/mixin_node_introspection.py +42 -20
  17. omnibase_infra/mixins/mixin_postgres_error_response.py +314 -0
  18. omnibase_infra/mixins/mixin_postgres_op_executor.py +298 -0
  19. omnibase_infra/models/__init__.py +3 -0
  20. omnibase_infra/models/discovery/model_dependency_spec.py +1 -0
  21. omnibase_infra/models/discovery/model_discovered_capabilities.py +1 -1
  22. omnibase_infra/models/discovery/model_introspection_config.py +28 -1
  23. omnibase_infra/models/discovery/model_introspection_performance_metrics.py +1 -0
  24. omnibase_infra/models/discovery/model_introspection_task_config.py +1 -0
  25. omnibase_infra/{nodes/effects/models → models}/model_backend_result.py +22 -6
  26. omnibase_infra/models/projection/__init__.py +11 -0
  27. omnibase_infra/models/projection/model_contract_projection.py +170 -0
  28. omnibase_infra/models/projection/model_topic_projection.py +148 -0
  29. omnibase_infra/models/runtime/__init__.py +4 -0
  30. omnibase_infra/models/runtime/model_resolved_dependencies.py +116 -0
  31. omnibase_infra/nodes/contract_registry_reducer/__init__.py +5 -0
  32. omnibase_infra/nodes/contract_registry_reducer/contract.yaml +6 -5
  33. omnibase_infra/nodes/contract_registry_reducer/contract_registration_event_router.py +689 -0
  34. omnibase_infra/nodes/contract_registry_reducer/reducer.py +9 -26
  35. omnibase_infra/nodes/effects/__init__.py +1 -1
  36. omnibase_infra/nodes/effects/models/__init__.py +6 -4
  37. omnibase_infra/nodes/effects/models/model_registry_response.py +1 -1
  38. omnibase_infra/nodes/effects/protocol_consul_client.py +1 -1
  39. omnibase_infra/nodes/effects/protocol_postgres_adapter.py +1 -1
  40. omnibase_infra/nodes/effects/registry_effect.py +1 -1
  41. omnibase_infra/nodes/node_contract_persistence_effect/__init__.py +101 -0
  42. omnibase_infra/nodes/node_contract_persistence_effect/contract.yaml +490 -0
  43. omnibase_infra/nodes/node_contract_persistence_effect/handlers/__init__.py +74 -0
  44. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_cleanup_topics.py +217 -0
  45. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_contract_upsert.py +242 -0
  46. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_deactivate.py +194 -0
  47. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_heartbeat.py +243 -0
  48. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_mark_stale.py +208 -0
  49. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_topic_update.py +298 -0
  50. omnibase_infra/nodes/node_contract_persistence_effect/models/__init__.py +15 -0
  51. omnibase_infra/nodes/node_contract_persistence_effect/models/model_persistence_result.py +52 -0
  52. omnibase_infra/nodes/node_contract_persistence_effect/node.py +131 -0
  53. omnibase_infra/nodes/node_contract_persistence_effect/registry/__init__.py +27 -0
  54. omnibase_infra/nodes/node_contract_persistence_effect/registry/registry_infra_contract_persistence_effect.py +251 -0
  55. omnibase_infra/nodes/node_registration_orchestrator/models/model_postgres_intent_payload.py +8 -12
  56. omnibase_infra/nodes/node_registry_effect/models/__init__.py +2 -2
  57. omnibase_infra/nodes/node_slack_alerter_effect/__init__.py +33 -0
  58. omnibase_infra/nodes/node_slack_alerter_effect/contract.yaml +291 -0
  59. omnibase_infra/nodes/node_slack_alerter_effect/node.py +106 -0
  60. omnibase_infra/projectors/__init__.py +6 -0
  61. omnibase_infra/projectors/projection_reader_contract.py +1301 -0
  62. omnibase_infra/runtime/__init__.py +12 -0
  63. omnibase_infra/runtime/baseline_subscriptions.py +13 -6
  64. omnibase_infra/runtime/contract_dependency_resolver.py +455 -0
  65. omnibase_infra/runtime/contract_registration_event_router.py +500 -0
  66. omnibase_infra/runtime/db/__init__.py +4 -0
  67. omnibase_infra/runtime/db/models/__init__.py +15 -10
  68. omnibase_infra/runtime/db/models/model_db_operation.py +40 -0
  69. omnibase_infra/runtime/db/models/model_db_param.py +24 -0
  70. omnibase_infra/runtime/db/models/model_db_repository_contract.py +40 -0
  71. omnibase_infra/runtime/db/models/model_db_return.py +26 -0
  72. omnibase_infra/runtime/db/models/model_db_safety_policy.py +32 -0
  73. omnibase_infra/runtime/emit_daemon/event_registry.py +34 -22
  74. omnibase_infra/runtime/event_bus_subcontract_wiring.py +63 -23
  75. omnibase_infra/runtime/intent_execution_router.py +430 -0
  76. omnibase_infra/runtime/models/__init__.py +6 -0
  77. omnibase_infra/runtime/models/model_contract_registry_config.py +41 -0
  78. omnibase_infra/runtime/models/model_intent_execution_summary.py +79 -0
  79. omnibase_infra/runtime/models/model_runtime_config.py +8 -0
  80. omnibase_infra/runtime/protocols/__init__.py +16 -0
  81. omnibase_infra/runtime/protocols/protocol_intent_executor.py +107 -0
  82. omnibase_infra/runtime/publisher_topic_scoped.py +16 -11
  83. omnibase_infra/runtime/registry_policy.py +29 -15
  84. omnibase_infra/runtime/request_response_wiring.py +793 -0
  85. omnibase_infra/runtime/service_kernel.py +295 -8
  86. omnibase_infra/runtime/service_runtime_host_process.py +149 -5
  87. omnibase_infra/runtime/util_version.py +5 -1
  88. omnibase_infra/schemas/schema_latency_baseline.sql +135 -0
  89. omnibase_infra/services/contract_publisher/config.py +4 -4
  90. omnibase_infra/services/contract_publisher/service.py +8 -5
  91. omnibase_infra/services/observability/injection_effectiveness/__init__.py +67 -0
  92. omnibase_infra/services/observability/injection_effectiveness/config.py +295 -0
  93. omnibase_infra/services/observability/injection_effectiveness/consumer.py +1461 -0
  94. omnibase_infra/services/observability/injection_effectiveness/models/__init__.py +32 -0
  95. omnibase_infra/services/observability/injection_effectiveness/models/model_agent_match.py +79 -0
  96. omnibase_infra/services/observability/injection_effectiveness/models/model_context_utilization.py +118 -0
  97. omnibase_infra/services/observability/injection_effectiveness/models/model_latency_breakdown.py +107 -0
  98. omnibase_infra/services/observability/injection_effectiveness/models/model_pattern_utilization.py +46 -0
  99. omnibase_infra/services/observability/injection_effectiveness/writer_postgres.py +596 -0
  100. omnibase_infra/services/registry_api/models/__init__.py +25 -0
  101. omnibase_infra/services/registry_api/models/model_contract_ref.py +44 -0
  102. omnibase_infra/services/registry_api/models/model_contract_view.py +81 -0
  103. omnibase_infra/services/registry_api/models/model_response_contracts.py +50 -0
  104. omnibase_infra/services/registry_api/models/model_response_topics.py +50 -0
  105. omnibase_infra/services/registry_api/models/model_topic_summary.py +57 -0
  106. omnibase_infra/services/registry_api/models/model_topic_view.py +63 -0
  107. omnibase_infra/services/registry_api/routes.py +205 -6
  108. omnibase_infra/services/registry_api/service.py +528 -1
  109. omnibase_infra/utils/__init__.py +7 -0
  110. omnibase_infra/utils/util_db_error_context.py +292 -0
  111. omnibase_infra/validation/infra_validators.py +3 -1
  112. omnibase_infra/validation/validation_exemptions.yaml +65 -0
  113. {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.4.0.dist-info}/METADATA +3 -3
  114. {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.4.0.dist-info}/RECORD +117 -58
  115. {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.4.0.dist-info}/WHEEL +0 -0
  116. {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.4.0.dist-info}/entry_points.txt +0 -0
  117. {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,430 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2025 OmniNode Team
3
+ """Intent Execution Router for Contract Persistence Operations.
4
+
5
+ This module routes intents from ContractRegistryReducer to the appropriate
6
+ handler implementations in NodeContractPersistenceEffect for PostgreSQL
7
+ persistence.
8
+
9
+ Architecture:
10
+ IntentExecutionRouter is the bridge between the REDUCER and EFFECT layers:
11
+ - REDUCER (ContractRegistryReducer) emits intents based on event processing
12
+ - ROUTER (this module) maps intent types to handlers and executes them
13
+ - EFFECT (NodeContractPersistenceEffect handlers) perform PostgreSQL I/O
14
+
15
+ This follows the ONEX unidirectional flow: EFFECT -> COMPUTE -> REDUCER -> ORCHESTRATOR,
16
+ where the orchestrator layer uses this router to execute intents emitted by reducers.
17
+
18
+ Intent Routing:
19
+ Each intent payload has an `intent_type` field that serves as the routing key:
20
+ - postgres.upsert_contract -> HandlerPostgresContractUpsert
21
+ - postgres.update_topic -> HandlerPostgresTopicUpdate
22
+ - postgres.mark_stale -> HandlerPostgresMarkStale
23
+ - postgres.update_heartbeat -> HandlerPostgresHeartbeat
24
+ - postgres.deactivate_contract -> HandlerPostgresDeactivate
25
+ - postgres.cleanup_topic_references -> HandlerPostgresCleanupTopics
26
+
27
+ Error Handling:
28
+ The router executes each intent independently. A failure in one intent does
29
+ not prevent execution of subsequent intents. This enables partial success
30
+ scenarios where some operations complete while others need retry.
31
+
32
+ Coroutine Safety:
33
+ This router is coroutine-safe for concurrent calls. Each handler execution
34
+ acquires its own connection from the pool. Thread-safety depends on the
35
+ underlying asyncpg.Pool implementation.
36
+
37
+ Related:
38
+ - ContractRegistryReducer: Emits intents consumed by this router
39
+ - NodeContractPersistenceEffect: Contains handler implementations
40
+ - ServiceKernel: Integrates this router into the event processing pipeline
41
+ - OMN-1869: Implementation ticket
42
+ - OMN-1653: ContractRegistryReducer ticket (source of intents)
43
+ """
44
+
45
+ from __future__ import annotations
46
+
47
+ import logging
48
+ import time
49
+ from typing import TYPE_CHECKING
50
+ from uuid import UUID
51
+
52
+ # Direct import to avoid circular import through omnibase_infra.models
53
+ from omnibase_infra.models.model_backend_result import ModelBackendResult
54
+ from omnibase_infra.nodes.node_contract_persistence_effect.handlers import (
55
+ HandlerPostgresCleanupTopics,
56
+ HandlerPostgresContractUpsert,
57
+ HandlerPostgresDeactivate,
58
+ HandlerPostgresHeartbeat,
59
+ HandlerPostgresMarkStale,
60
+ HandlerPostgresTopicUpdate,
61
+ )
62
+ from omnibase_infra.runtime.models.model_intent_execution_summary import (
63
+ ModelIntentExecutionSummary,
64
+ )
65
+ from omnibase_infra.runtime.protocols.protocol_intent_executor import (
66
+ ProtocolIntentExecutor,
67
+ )
68
+ from omnibase_infra.utils import sanitize_error_message
69
+
70
+ if TYPE_CHECKING:
71
+ import asyncpg
72
+
73
+ from omnibase_core.models.container.model_onex_container import ModelONEXContainer
74
+ from omnibase_core.models.reducer.model_intent import ModelIntent
75
+
76
+ _logger = logging.getLogger(__name__)
77
+
78
+
79
+ # Intent type routing constants
80
+ INTENT_UPSERT_CONTRACT = "postgres.upsert_contract"
81
+ INTENT_UPDATE_TOPIC = "postgres.update_topic"
82
+ INTENT_MARK_STALE = "postgres.mark_stale"
83
+ INTENT_UPDATE_HEARTBEAT = "postgres.update_heartbeat"
84
+ INTENT_DEACTIVATE_CONTRACT = "postgres.deactivate_contract"
85
+ INTENT_CLEANUP_TOPIC_REFERENCES = "postgres.cleanup_topic_references"
86
+
87
+
88
+ class IntentExecutionRouter:
89
+ """Routes and executes intents from ContractRegistryReducer to persistence handlers.
90
+
91
+ This router maps intent types to their corresponding PostgreSQL handlers and
92
+ orchestrates execution. It handles errors gracefully per-intent, enabling
93
+ partial success scenarios where some operations complete while others need
94
+ retry.
95
+
96
+ Attributes:
97
+ _container: ONEX container for dependency injection (optional).
98
+ _pool: asyncpg connection pool for database operations.
99
+ _handlers: Cached handler instances keyed by intent type.
100
+
101
+ Example:
102
+ >>> import asyncpg
103
+ >>> pool = await asyncpg.create_pool(dsn="...")
104
+ >>> router = IntentExecutionRouter(container=None, postgres_pool=pool)
105
+ >>> summary = await router.execute_intents(intents, correlation_id)
106
+ >>> if summary.all_successful:
107
+ ... print("All intents executed successfully")
108
+
109
+ Thread Safety:
110
+ This router is coroutine-safe. Each handler execution acquires its own
111
+ connection from the pool. The handler instances are created once during
112
+ initialization and are stateless.
113
+
114
+ See Also:
115
+ - ContractRegistryReducer: Source of intents
116
+ - NodeContractPersistenceEffect: Contains handler implementations
117
+ - ServiceKernel: Integrates router into event processing
118
+ """
119
+
120
+ def __init__(
121
+ self,
122
+ container: ModelONEXContainer | None,
123
+ postgres_pool: asyncpg.Pool,
124
+ ) -> None:
125
+ """Initialize the intent execution router.
126
+
127
+ Args:
128
+ container: ONEX container for dependency injection. May be None
129
+ if router is used standalone without container DI.
130
+ postgres_pool: asyncpg connection pool for database operations.
131
+ The pool should be pre-configured and ready for use.
132
+
133
+ Raises:
134
+ ValueError: If postgres_pool is None.
135
+ """
136
+ if postgres_pool is None:
137
+ raise ValueError("postgres_pool is required for IntentExecutionRouter")
138
+
139
+ self._container = container
140
+ self._pool = postgres_pool
141
+
142
+ # Initialize handlers with the pool
143
+ # Handlers implement ProtocolIntentExecutor[SpecificPayloadType] structurally.
144
+ # Using object here per ONEX rules (Any is forbidden); handler.handle() call
145
+ # below uses type: ignore since we guarantee correct payload routing at runtime.
146
+ self._handlers: dict[str, object] = {
147
+ INTENT_UPSERT_CONTRACT: HandlerPostgresContractUpsert(postgres_pool),
148
+ INTENT_UPDATE_TOPIC: HandlerPostgresTopicUpdate(postgres_pool),
149
+ INTENT_MARK_STALE: HandlerPostgresMarkStale(postgres_pool),
150
+ INTENT_UPDATE_HEARTBEAT: HandlerPostgresHeartbeat(postgres_pool),
151
+ INTENT_DEACTIVATE_CONTRACT: HandlerPostgresDeactivate(postgres_pool),
152
+ INTENT_CLEANUP_TOPIC_REFERENCES: HandlerPostgresCleanupTopics(
153
+ postgres_pool
154
+ ),
155
+ }
156
+
157
+ _logger.info(
158
+ "IntentExecutionRouter initialized",
159
+ extra={
160
+ "handler_count": len(self._handlers),
161
+ "intent_types": list(self._handlers.keys()),
162
+ },
163
+ )
164
+
165
+ async def execute_intents(
166
+ self,
167
+ intents: tuple[ModelIntent, ...],
168
+ correlation_id: UUID,
169
+ ) -> ModelIntentExecutionSummary:
170
+ """Execute a batch of intents, routing each to its handler.
171
+
172
+ Processes each intent independently, allowing partial success. A failure
173
+ in one intent does not prevent execution of subsequent intents. This
174
+ enables scenarios where some operations complete while others need retry.
175
+
176
+ Args:
177
+ intents: Tuple of intents to execute. Each intent contains a payload
178
+ with an intent_type field used for routing.
179
+ correlation_id: Request correlation ID for distributed tracing.
180
+
181
+ Returns:
182
+ ModelIntentExecutionSummary with:
183
+ - total_intents: Number of intents processed
184
+ - successful_count: Number that succeeded
185
+ - failed_count: Number that failed
186
+ - total_duration_ms: Batch execution time
187
+ - results: Individual ModelBackendResult for each intent
188
+ - correlation_id: Passed through for tracing
189
+
190
+ Note:
191
+ This method never raises exceptions. All errors are captured in the
192
+ results for each intent. This enables callers to inspect partial
193
+ failures and decide on retry strategies.
194
+
195
+ Intents with unknown intent_type values are logged and marked as
196
+ failed with an appropriate error message.
197
+
198
+ Example:
199
+ >>> summary = await router.execute_intents(intents, correlation_id)
200
+ >>> for result in summary.results:
201
+ ... if not result.success:
202
+ ... print(f"Failed: {result.error}")
203
+ """
204
+ start_time = time.perf_counter()
205
+ results: list[ModelBackendResult] = []
206
+ successful_count = 0
207
+ failed_count = 0
208
+
209
+ _logger.info(
210
+ "Starting intent batch execution",
211
+ extra={
212
+ "correlation_id": str(correlation_id),
213
+ "intent_count": len(intents),
214
+ },
215
+ )
216
+
217
+ for intent in intents:
218
+ try:
219
+ result = await self._execute_single_intent(intent, correlation_id)
220
+ results.append(result)
221
+
222
+ if result.success:
223
+ successful_count += 1
224
+ else:
225
+ failed_count += 1
226
+
227
+ except Exception as e: # ONEX: catch-all for unexpected errors
228
+ # Should not happen since _execute_single_intent handles errors,
229
+ # but defense-in-depth to ensure we never crash the batch
230
+ failed_count += 1
231
+ sanitized_error = sanitize_error_message(e)
232
+ _logger.exception(
233
+ "Unexpected error during intent execution",
234
+ extra={
235
+ "correlation_id": str(correlation_id),
236
+ "intent_id": str(getattr(intent, "intent_id", "unknown")),
237
+ "error": sanitized_error,
238
+ },
239
+ )
240
+ results.append(
241
+ ModelBackendResult(
242
+ success=False,
243
+ error=f"Unexpected error: {sanitized_error}",
244
+ error_code="INTENT_EXECUTION_UNEXPECTED_ERROR",
245
+ duration_ms=0.0,
246
+ backend_id="intent_router",
247
+ correlation_id=correlation_id,
248
+ )
249
+ )
250
+
251
+ total_duration_ms = (time.perf_counter() - start_time) * 1000
252
+
253
+ summary = ModelIntentExecutionSummary(
254
+ total_intents=len(intents),
255
+ successful_count=successful_count,
256
+ failed_count=failed_count,
257
+ total_duration_ms=total_duration_ms,
258
+ results=tuple(results),
259
+ correlation_id=correlation_id,
260
+ )
261
+
262
+ _logger.info(
263
+ "Intent batch execution completed",
264
+ extra={
265
+ "correlation_id": str(correlation_id),
266
+ "total_intents": summary.total_intents,
267
+ "successful_count": summary.successful_count,
268
+ "failed_count": summary.failed_count,
269
+ "total_duration_ms": summary.total_duration_ms,
270
+ "all_successful": summary.all_successful,
271
+ },
272
+ )
273
+
274
+ return summary
275
+
276
+ async def _execute_single_intent(
277
+ self,
278
+ intent: ModelIntent,
279
+ correlation_id: UUID,
280
+ ) -> ModelBackendResult:
281
+ """Execute a single intent by routing to the appropriate handler.
282
+
283
+ Args:
284
+ intent: The intent to execute. Contains a payload with intent_type.
285
+ correlation_id: Request correlation ID for distributed tracing.
286
+
287
+ Returns:
288
+ ModelBackendResult from the handler, or an error result if routing
289
+ or execution fails.
290
+
291
+ Note:
292
+ This method never raises exceptions. All errors are captured in
293
+ the returned ModelBackendResult.
294
+ """
295
+ start_time = time.perf_counter()
296
+
297
+ try:
298
+ # Extract payload from intent
299
+ payload = intent.payload
300
+ if payload is None:
301
+ _logger.warning(
302
+ "Intent has no payload",
303
+ extra={
304
+ "correlation_id": str(correlation_id),
305
+ "intent_id": str(intent.intent_id),
306
+ },
307
+ )
308
+ return ModelBackendResult(
309
+ success=False,
310
+ error="Intent has no payload",
311
+ error_code="INTENT_NO_PAYLOAD",
312
+ duration_ms=(time.perf_counter() - start_time) * 1000,
313
+ backend_id="intent_router",
314
+ correlation_id=correlation_id,
315
+ )
316
+
317
+ # Get intent_type from payload
318
+ intent_type = getattr(payload, "intent_type", None)
319
+ if intent_type is None:
320
+ _logger.warning(
321
+ "Payload has no intent_type field",
322
+ extra={
323
+ "correlation_id": str(correlation_id),
324
+ "intent_id": str(intent.intent_id),
325
+ "payload_type": type(payload).__name__,
326
+ },
327
+ )
328
+ return ModelBackendResult(
329
+ success=False,
330
+ error="Payload has no intent_type field",
331
+ error_code="INTENT_TYPE_MISSING",
332
+ duration_ms=(time.perf_counter() - start_time) * 1000,
333
+ backend_id="intent_router",
334
+ correlation_id=correlation_id,
335
+ )
336
+
337
+ # Look up handler for this intent type
338
+ handler = self._handlers.get(intent_type)
339
+ if handler is None:
340
+ _logger.warning(
341
+ "No handler registered for intent type",
342
+ extra={
343
+ "correlation_id": str(correlation_id),
344
+ "intent_id": str(intent.intent_id),
345
+ "intent_type": intent_type,
346
+ "registered_types": list(self._handlers.keys()),
347
+ },
348
+ )
349
+ return ModelBackendResult(
350
+ success=False,
351
+ error=f"No handler for intent type: {intent_type}",
352
+ error_code="INTENT_TYPE_UNKNOWN",
353
+ duration_ms=(time.perf_counter() - start_time) * 1000,
354
+ backend_id="intent_router",
355
+ correlation_id=correlation_id,
356
+ )
357
+
358
+ # Execute the handler
359
+ _logger.debug(
360
+ "Executing handler for intent",
361
+ extra={
362
+ "correlation_id": str(correlation_id),
363
+ "intent_id": str(intent.intent_id),
364
+ "intent_type": intent_type,
365
+ "handler_class": type(handler).__name__,
366
+ },
367
+ )
368
+
369
+ # All handlers implement ProtocolIntentExecutor structurally with signature:
370
+ # handle(payload: SpecificPayloadType, correlation_id: UUID) -> ModelBackendResult
371
+ # Using type: ignore since dict value is object per ONEX rules (Any forbidden)
372
+ result: ModelBackendResult = await handler.handle(payload, correlation_id) # type: ignore[attr-defined]
373
+
374
+ _logger.debug(
375
+ "Handler execution completed",
376
+ extra={
377
+ "correlation_id": str(correlation_id),
378
+ "intent_id": str(intent.intent_id),
379
+ "intent_type": intent_type,
380
+ "success": result.success,
381
+ "duration_ms": result.duration_ms,
382
+ },
383
+ )
384
+
385
+ return result
386
+
387
+ except (
388
+ Exception
389
+ ) as e: # ONEX: catch-all for handler errors not caught internally
390
+ duration_ms = (time.perf_counter() - start_time) * 1000
391
+ sanitized_error = sanitize_error_message(e)
392
+ _logger.exception(
393
+ "Handler execution failed with unexpected error",
394
+ extra={
395
+ "correlation_id": str(correlation_id),
396
+ "intent_id": str(getattr(intent, "intent_id", "unknown")),
397
+ "error": sanitized_error,
398
+ "duration_ms": duration_ms,
399
+ },
400
+ )
401
+ return ModelBackendResult(
402
+ success=False,
403
+ error=sanitized_error,
404
+ error_code="HANDLER_EXECUTION_ERROR",
405
+ duration_ms=duration_ms,
406
+ backend_id="intent_router",
407
+ correlation_id=correlation_id,
408
+ )
409
+
410
+ @property
411
+ def supported_intent_types(self) -> tuple[str, ...]:
412
+ """Get the list of intent types supported by this router.
413
+
414
+ Returns:
415
+ Tuple of supported intent type strings.
416
+ """
417
+ return tuple(self._handlers.keys())
418
+
419
+
420
+ __all__: list[str] = [
421
+ "IntentExecutionRouter",
422
+ "ModelIntentExecutionSummary",
423
+ "ProtocolIntentExecutor",
424
+ "INTENT_UPSERT_CONTRACT",
425
+ "INTENT_UPDATE_TOPIC",
426
+ "INTENT_MARK_STALE",
427
+ "INTENT_UPDATE_HEARTBEAT",
428
+ "INTENT_DEACTIVATE_CONTRACT",
429
+ "INTENT_CLEANUP_TOPIC_REFERENCES",
430
+ ]
@@ -105,6 +105,11 @@ from omnibase_infra.runtime.models.model_health_check_response import (
105
105
  from omnibase_infra.runtime.models.model_health_check_result import (
106
106
  ModelHealthCheckResult,
107
107
  )
108
+
109
+ # NOTE: ModelIntentExecutionSummary is NOT imported here to avoid circular import.
110
+ # It depends on ModelBackendResult from nodes.effects, which loads after runtime.models.
111
+ # Import directly when needed:
112
+ # from omnibase_infra.runtime.models.model_intent_execution_summary import ModelIntentExecutionSummary
108
113
  from omnibase_infra.runtime.models.model_lifecycle_result import (
109
114
  ModelLifecycleResult,
110
115
  )
@@ -190,6 +195,7 @@ __all__: list[str] = [
190
195
  "ModelFailedComponent",
191
196
  "ModelHealthCheckResponse",
192
197
  "ModelHealthCheckResult",
198
+ # NOTE: ModelIntentExecutionSummary excluded - import directly from module
193
199
  "ModelLifecycleResult",
194
200
  "ModelLoggingConfig",
195
201
  "ModelOptionalCorrelationId",
@@ -0,0 +1,41 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2025 OmniNode Team
3
+ """Contract Registry Configuration Model.
4
+
5
+ This module provides the Pydantic model for contract registry event processing
6
+ configuration, controlling the staleness tick timer behavior.
7
+
8
+ Related:
9
+ - OMN-1869: Wire ServiceKernel to Kafka event bus
10
+ - ContractRegistrationEventRouter: Uses this config for tick interval
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from pydantic import BaseModel, ConfigDict, Field
16
+
17
+ __all__: list[str] = ["ModelContractRegistryConfig"]
18
+
19
+
20
+ class ModelContractRegistryConfig(BaseModel):
21
+ """Configuration for contract registry event processing.
22
+
23
+ Controls the behavior of the contract registry staleness tick timer
24
+ and whether contract registry processing is enabled.
25
+
26
+ Attributes:
27
+ tick_interval_seconds: Interval for staleness tick in seconds (min 5s)
28
+ enabled: Whether contract registry processing is enabled
29
+ """
30
+
31
+ model_config = ConfigDict(frozen=True, extra="forbid", from_attributes=True)
32
+
33
+ tick_interval_seconds: int = Field(
34
+ default=60,
35
+ ge=5,
36
+ description="Interval for staleness tick in seconds (min 5s)",
37
+ )
38
+ enabled: bool = Field(
39
+ default=True,
40
+ description="Whether contract registry processing is enabled",
41
+ )
@@ -0,0 +1,79 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2025 OmniNode Team
3
+ """Intent execution summary model for batch execution results.
4
+
5
+ This module provides the ModelIntentExecutionSummary model that aggregates
6
+ execution results from intent batch processing in the IntentExecutionRouter.
7
+
8
+ Related:
9
+ - IntentExecutionRouter: Uses this model for batch execution results
10
+ - OMN-1869: Implementation ticket
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from uuid import UUID
16
+
17
+ from pydantic import BaseModel, ConfigDict, Field
18
+
19
+ # Direct import (not TYPE_CHECKING) because Pydantic v2 needs the class at runtime
20
+ # for forward reference resolution. This module doesn't create a circular import
21
+ # because model_backend_result.py doesn't import from runtime.models.
22
+ from omnibase_infra.models.model_backend_result import (
23
+ ModelBackendResult,
24
+ )
25
+
26
+
27
+ class ModelIntentExecutionSummary(BaseModel):
28
+ """Summary of intent batch execution results.
29
+
30
+ Provides an aggregated view of the batch execution including success/failure
31
+ counts, timing, and individual results for observability and retry decisions.
32
+
33
+ Attributes:
34
+ total_intents: Number of intents processed in the batch.
35
+ successful_count: Number of intents that executed successfully.
36
+ failed_count: Number of intents that failed execution.
37
+ total_duration_ms: Total time for batch execution in milliseconds.
38
+ results: Individual execution results for each intent.
39
+ correlation_id: Correlation ID for distributed tracing.
40
+ """
41
+
42
+ model_config = ConfigDict(frozen=True, extra="forbid", from_attributes=True)
43
+
44
+ total_intents: int = Field(
45
+ default=0, ge=0, description="Number of intents processed."
46
+ )
47
+ successful_count: int = Field(
48
+ default=0, ge=0, description="Number of successful executions."
49
+ )
50
+ failed_count: int = Field(
51
+ default=0, ge=0, description="Number of failed executions."
52
+ )
53
+ total_duration_ms: float = Field(
54
+ default=0.0, ge=0.0, description="Total batch duration in milliseconds."
55
+ )
56
+ results: tuple[ModelBackendResult, ...] = Field(
57
+ default_factory=tuple, description="Individual execution results."
58
+ )
59
+ correlation_id: UUID | None = Field(
60
+ default=None, description="Correlation ID for tracing."
61
+ )
62
+
63
+ @property
64
+ def all_successful(self) -> bool:
65
+ """Check if all intents executed successfully."""
66
+ return self.failed_count == 0 and self.total_intents > 0
67
+
68
+ @property
69
+ def partial_success(self) -> bool:
70
+ """Check if batch had partial success (some passed, some failed)."""
71
+ return self.successful_count > 0 and self.failed_count > 0
72
+
73
+ @property
74
+ def all_failed(self) -> bool:
75
+ """Check if all intents failed."""
76
+ return self.successful_count == 0 and self.total_intents > 0
77
+
78
+
79
+ __all__ = ["ModelIntentExecutionSummary"]
@@ -47,6 +47,9 @@ from __future__ import annotations
47
47
 
48
48
  from pydantic import BaseModel, ConfigDict, Field
49
49
 
50
+ from omnibase_infra.runtime.models.model_contract_registry_config import (
51
+ ModelContractRegistryConfig,
52
+ )
50
53
  from omnibase_infra.runtime.models.model_enabled_protocols_config import (
51
54
  ModelEnabledProtocolsConfig,
52
55
  )
@@ -72,6 +75,7 @@ class ModelRuntimeConfig(BaseModel):
72
75
  protocols: Enabled protocols configuration [RESERVED]
73
76
  logging: Logging configuration [RESERVED]
74
77
  shutdown: Shutdown configuration [ACTIVE - grace_period_seconds used]
78
+ contract_registry: Contract registry configuration [ACTIVE]
75
79
 
76
80
  Field Status Legend:
77
81
  [ACTIVE] - Currently used by kernel.py
@@ -145,6 +149,10 @@ class ModelRuntimeConfig(BaseModel):
145
149
  default_factory=ModelShutdownConfig,
146
150
  description="Shutdown configuration",
147
151
  )
152
+ contract_registry: ModelContractRegistryConfig = Field(
153
+ default_factory=ModelContractRegistryConfig,
154
+ description="Contract registry configuration",
155
+ )
148
156
 
149
157
 
150
158
  __all__: list[str] = ["ModelRuntimeConfig"]
@@ -23,15 +23,31 @@ Related:
23
23
 
24
24
  from __future__ import annotations
25
25
 
26
+ from typing import TYPE_CHECKING
27
+
26
28
  # Re-export from omnibase_core for convenience
27
29
  from omnibase_core.protocols.notifications import (
28
30
  ProtocolTransitionNotificationPublisher,
29
31
  )
32
+ from omnibase_infra.runtime.protocols.protocol_intent_executor import (
33
+ PayloadT_contra,
34
+ ProtocolIntentExecutor,
35
+ )
30
36
  from omnibase_infra.runtime.protocols.protocol_runtime_scheduler import (
31
37
  ProtocolRuntimeScheduler,
32
38
  )
33
39
 
40
+ if TYPE_CHECKING:
41
+ # IntentPayloadType is only available for type checking - it references
42
+ # models from nodes.* which aren't loaded during package initialization.
43
+ from omnibase_infra.runtime.protocols.protocol_intent_executor import (
44
+ IntentPayloadType,
45
+ )
46
+
47
+ # NOTE: IntentPayloadType is only available under TYPE_CHECKING (see above)
34
48
  __all__: list[str] = [
49
+ "PayloadT_contra",
50
+ "ProtocolIntentExecutor",
35
51
  "ProtocolRuntimeScheduler",
36
52
  "ProtocolTransitionNotificationPublisher",
37
53
  ]