omnibase_infra 0.3.1__py3-none-any.whl → 0.3.2__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.
- omnibase_infra/__init__.py +1 -1
- omnibase_infra/enums/__init__.py +3 -0
- omnibase_infra/enums/enum_consumer_group_purpose.py +9 -0
- omnibase_infra/enums/enum_postgres_error_code.py +188 -0
- omnibase_infra/handlers/registration_storage/handler_registration_storage_postgres.py +29 -20
- omnibase_infra/mixins/__init__.py +14 -0
- omnibase_infra/mixins/mixin_postgres_error_response.py +314 -0
- omnibase_infra/mixins/mixin_postgres_op_executor.py +298 -0
- omnibase_infra/models/__init__.py +3 -0
- omnibase_infra/{nodes/effects/models → models}/model_backend_result.py +22 -6
- omnibase_infra/models/projection/__init__.py +11 -0
- omnibase_infra/models/projection/model_contract_projection.py +170 -0
- omnibase_infra/models/projection/model_topic_projection.py +148 -0
- omnibase_infra/nodes/contract_registry_reducer/__init__.py +5 -0
- omnibase_infra/nodes/contract_registry_reducer/contract_registration_event_router.py +689 -0
- omnibase_infra/nodes/effects/__init__.py +1 -1
- omnibase_infra/nodes/effects/models/__init__.py +6 -4
- omnibase_infra/nodes/effects/models/model_registry_response.py +1 -1
- omnibase_infra/nodes/effects/protocol_consul_client.py +1 -1
- omnibase_infra/nodes/effects/protocol_postgres_adapter.py +1 -1
- omnibase_infra/nodes/effects/registry_effect.py +1 -1
- omnibase_infra/nodes/node_contract_persistence_effect/__init__.py +101 -0
- omnibase_infra/nodes/node_contract_persistence_effect/contract.yaml +490 -0
- omnibase_infra/nodes/node_contract_persistence_effect/handlers/__init__.py +74 -0
- omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_cleanup_topics.py +217 -0
- omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_contract_upsert.py +242 -0
- omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_deactivate.py +194 -0
- omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_heartbeat.py +243 -0
- omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_mark_stale.py +208 -0
- omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_topic_update.py +298 -0
- omnibase_infra/nodes/node_contract_persistence_effect/models/__init__.py +15 -0
- omnibase_infra/nodes/node_contract_persistence_effect/models/model_persistence_result.py +52 -0
- omnibase_infra/nodes/node_contract_persistence_effect/node.py +114 -0
- omnibase_infra/nodes/node_contract_persistence_effect/registry/__init__.py +27 -0
- omnibase_infra/nodes/node_contract_persistence_effect/registry/registry_infra_contract_persistence_effect.py +220 -0
- omnibase_infra/nodes/node_registry_effect/models/__init__.py +2 -2
- omnibase_infra/projectors/__init__.py +6 -0
- omnibase_infra/projectors/projection_reader_contract.py +1301 -0
- omnibase_infra/runtime/__init__.py +5 -0
- omnibase_infra/runtime/contract_registration_event_router.py +500 -0
- omnibase_infra/runtime/db/__init__.py +4 -0
- omnibase_infra/runtime/db/models/__init__.py +15 -10
- omnibase_infra/runtime/db/models/model_db_operation.py +40 -0
- omnibase_infra/runtime/db/models/model_db_param.py +24 -0
- omnibase_infra/runtime/db/models/model_db_repository_contract.py +40 -0
- omnibase_infra/runtime/db/models/model_db_return.py +26 -0
- omnibase_infra/runtime/db/models/model_db_safety_policy.py +32 -0
- omnibase_infra/runtime/intent_execution_router.py +430 -0
- omnibase_infra/runtime/models/__init__.py +6 -0
- omnibase_infra/runtime/models/model_contract_registry_config.py +41 -0
- omnibase_infra/runtime/models/model_intent_execution_summary.py +79 -0
- omnibase_infra/runtime/models/model_runtime_config.py +8 -0
- omnibase_infra/runtime/protocols/__init__.py +16 -0
- omnibase_infra/runtime/protocols/protocol_intent_executor.py +107 -0
- omnibase_infra/runtime/request_response_wiring.py +785 -0
- omnibase_infra/runtime/service_kernel.py +295 -8
- omnibase_infra/services/registry_api/models/__init__.py +25 -0
- omnibase_infra/services/registry_api/models/model_contract_ref.py +44 -0
- omnibase_infra/services/registry_api/models/model_contract_view.py +81 -0
- omnibase_infra/services/registry_api/models/model_response_contracts.py +50 -0
- omnibase_infra/services/registry_api/models/model_response_topics.py +50 -0
- omnibase_infra/services/registry_api/models/model_topic_summary.py +57 -0
- omnibase_infra/services/registry_api/models/model_topic_view.py +63 -0
- omnibase_infra/services/registry_api/routes.py +205 -6
- omnibase_infra/services/registry_api/service.py +528 -1
- omnibase_infra/validation/infra_validators.py +3 -1
- omnibase_infra/validation/validation_exemptions.yaml +54 -0
- {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.3.2.dist-info}/METADATA +3 -3
- {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.3.2.dist-info}/RECORD +72 -34
- {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.3.2.dist-info}/WHEEL +0 -0
- {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.3.2.dist-info}/entry_points.txt +0 -0
- {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.3.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 OmniNode Team
|
|
3
|
+
"""Database repository contract model."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
8
|
+
|
|
9
|
+
from omnibase_infra.runtime.db.models.model_db_operation import ModelDbOperation
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ModelDbRepositoryContract(BaseModel):
|
|
13
|
+
"""Complete database repository contract.
|
|
14
|
+
|
|
15
|
+
Defines all operations, tables, and configuration for a database repository.
|
|
16
|
+
|
|
17
|
+
Attributes:
|
|
18
|
+
name: Repository name (used for identification)
|
|
19
|
+
engine: Database engine ("postgres", "mysql", etc.)
|
|
20
|
+
database_ref: Reference to database connection configuration
|
|
21
|
+
tables: List of tables this repository operates on
|
|
22
|
+
models: Mapping of model names to their module paths
|
|
23
|
+
ops: Mapping of operation names to their definitions
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
27
|
+
|
|
28
|
+
name: str = Field(..., description="Repository name")
|
|
29
|
+
engine: str = Field(default="postgres", description="Database engine")
|
|
30
|
+
database_ref: str = Field(..., description="Database connection reference")
|
|
31
|
+
tables: list[str] = Field(default_factory=list, description="Tables operated on")
|
|
32
|
+
models: dict[str, str] = Field(
|
|
33
|
+
default_factory=dict, description="Model name -> module path mapping"
|
|
34
|
+
)
|
|
35
|
+
ops: dict[str, ModelDbOperation] = Field(
|
|
36
|
+
default_factory=dict, description="Operation name -> definition mapping"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
__all__ = ["ModelDbRepositoryContract"]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 OmniNode Team
|
|
3
|
+
"""Database return type model for SQL operations."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ModelDbReturn(BaseModel):
|
|
11
|
+
"""Return type specification for a database operation.
|
|
12
|
+
|
|
13
|
+
Attributes:
|
|
14
|
+
model_ref: Reference to the model type for results (e.g., "User")
|
|
15
|
+
many: If True, operation returns multiple rows; if False, single row
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
19
|
+
|
|
20
|
+
model_ref: str = Field(default="", description="Model reference for results")
|
|
21
|
+
many: bool = Field(
|
|
22
|
+
default=False, description="Whether operation returns multiple rows"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
__all__ = ["ModelDbReturn"]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 OmniNode Team
|
|
3
|
+
"""Database safety policy model for SQL operations."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ModelDbSafetyPolicy(BaseModel):
|
|
11
|
+
"""Safety policy constraints for database operations.
|
|
12
|
+
|
|
13
|
+
Attributes:
|
|
14
|
+
require_where_clause: If True, require WHERE clause for updates/deletes
|
|
15
|
+
max_affected_rows: Maximum number of rows that can be affected
|
|
16
|
+
allow_full_table_scan: If True, allow queries without index usage
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
20
|
+
|
|
21
|
+
require_where_clause: bool = Field(
|
|
22
|
+
default=True, description="Require WHERE clause for updates/deletes"
|
|
23
|
+
)
|
|
24
|
+
max_affected_rows: int | None = Field(
|
|
25
|
+
default=None, description="Maximum affected rows (None = unlimited)"
|
|
26
|
+
)
|
|
27
|
+
allow_full_table_scan: bool = Field(
|
|
28
|
+
default=True, description="Allow queries without index usage"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
__all__ = ["ModelDbSafetyPolicy"]
|
|
@@ -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"]
|