omnibase_infra 0.3.2__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.
- omnibase_infra/__init__.py +1 -1
- omnibase_infra/errors/__init__.py +4 -0
- omnibase_infra/errors/error_infra.py +60 -0
- omnibase_infra/handlers/__init__.py +3 -0
- omnibase_infra/handlers/handler_slack_webhook.py +426 -0
- omnibase_infra/handlers/models/__init__.py +14 -0
- omnibase_infra/handlers/models/enum_alert_severity.py +36 -0
- omnibase_infra/handlers/models/model_slack_alert.py +24 -0
- omnibase_infra/handlers/models/model_slack_alert_payload.py +77 -0
- omnibase_infra/handlers/models/model_slack_alert_result.py +73 -0
- omnibase_infra/mixins/mixin_node_introspection.py +42 -20
- omnibase_infra/models/discovery/model_dependency_spec.py +1 -0
- omnibase_infra/models/discovery/model_discovered_capabilities.py +1 -1
- omnibase_infra/models/discovery/model_introspection_config.py +28 -1
- omnibase_infra/models/discovery/model_introspection_performance_metrics.py +1 -0
- omnibase_infra/models/discovery/model_introspection_task_config.py +1 -0
- omnibase_infra/models/runtime/__init__.py +4 -0
- omnibase_infra/models/runtime/model_resolved_dependencies.py +116 -0
- omnibase_infra/nodes/contract_registry_reducer/contract.yaml +6 -5
- omnibase_infra/nodes/contract_registry_reducer/reducer.py +9 -26
- omnibase_infra/nodes/node_contract_persistence_effect/node.py +18 -1
- omnibase_infra/nodes/node_contract_persistence_effect/registry/registry_infra_contract_persistence_effect.py +33 -2
- omnibase_infra/nodes/node_registration_orchestrator/models/model_postgres_intent_payload.py +8 -12
- omnibase_infra/nodes/node_slack_alerter_effect/__init__.py +33 -0
- omnibase_infra/nodes/node_slack_alerter_effect/contract.yaml +291 -0
- omnibase_infra/nodes/node_slack_alerter_effect/node.py +106 -0
- omnibase_infra/runtime/__init__.py +7 -0
- omnibase_infra/runtime/baseline_subscriptions.py +13 -6
- omnibase_infra/runtime/contract_dependency_resolver.py +455 -0
- omnibase_infra/runtime/contract_registration_event_router.py +5 -5
- omnibase_infra/runtime/emit_daemon/event_registry.py +34 -22
- omnibase_infra/runtime/event_bus_subcontract_wiring.py +63 -23
- omnibase_infra/runtime/publisher_topic_scoped.py +16 -11
- omnibase_infra/runtime/registry_policy.py +29 -15
- omnibase_infra/runtime/request_response_wiring.py +15 -7
- omnibase_infra/runtime/service_runtime_host_process.py +149 -5
- omnibase_infra/runtime/util_version.py +5 -1
- omnibase_infra/schemas/schema_latency_baseline.sql +135 -0
- omnibase_infra/services/contract_publisher/config.py +4 -4
- omnibase_infra/services/contract_publisher/service.py +8 -5
- omnibase_infra/services/observability/injection_effectiveness/__init__.py +67 -0
- omnibase_infra/services/observability/injection_effectiveness/config.py +295 -0
- omnibase_infra/services/observability/injection_effectiveness/consumer.py +1461 -0
- omnibase_infra/services/observability/injection_effectiveness/models/__init__.py +32 -0
- omnibase_infra/services/observability/injection_effectiveness/models/model_agent_match.py +79 -0
- omnibase_infra/services/observability/injection_effectiveness/models/model_context_utilization.py +118 -0
- omnibase_infra/services/observability/injection_effectiveness/models/model_latency_breakdown.py +107 -0
- omnibase_infra/services/observability/injection_effectiveness/models/model_pattern_utilization.py +46 -0
- omnibase_infra/services/observability/injection_effectiveness/writer_postgres.py +596 -0
- omnibase_infra/utils/__init__.py +7 -0
- omnibase_infra/utils/util_db_error_context.py +292 -0
- omnibase_infra/validation/validation_exemptions.yaml +11 -0
- {omnibase_infra-0.3.2.dist-info → omnibase_infra-0.4.0.dist-info}/METADATA +2 -2
- {omnibase_infra-0.3.2.dist-info → omnibase_infra-0.4.0.dist-info}/RECORD +57 -36
- {omnibase_infra-0.3.2.dist-info → omnibase_infra-0.4.0.dist-info}/WHEEL +0 -0
- {omnibase_infra-0.3.2.dist-info → omnibase_infra-0.4.0.dist-info}/entry_points.txt +0 -0
- {omnibase_infra-0.3.2.dist-info → omnibase_infra-0.4.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 OmniNode Team
|
|
3
|
+
"""Contract dependency resolver for ONEX nodes.
|
|
4
|
+
|
|
5
|
+
This module provides ContractDependencyResolver, which reads protocol
|
|
6
|
+
dependencies from a node's contract.yaml and resolves them from the
|
|
7
|
+
container's service_registry.
|
|
8
|
+
|
|
9
|
+
Part of OMN-1732: Runtime dependency injection for zero-code nodes.
|
|
10
|
+
|
|
11
|
+
Architecture:
|
|
12
|
+
- Container: service provider (owns protocol instances)
|
|
13
|
+
- Runtime (this resolver): wiring authority (resolves + validates)
|
|
14
|
+
- Node: consumer (receives resolved deps via constructor)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import importlib
|
|
20
|
+
import logging
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from types import SimpleNamespace
|
|
23
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
24
|
+
|
|
25
|
+
import yaml
|
|
26
|
+
|
|
27
|
+
from omnibase_infra.enums import EnumInfraTransportType
|
|
28
|
+
from omnibase_infra.errors import (
|
|
29
|
+
ModelInfraErrorContext,
|
|
30
|
+
ProtocolConfigurationError,
|
|
31
|
+
ProtocolDependencyResolutionError,
|
|
32
|
+
)
|
|
33
|
+
from omnibase_infra.models.runtime.model_resolved_dependencies import (
|
|
34
|
+
ModelResolvedDependencies,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
if TYPE_CHECKING:
|
|
38
|
+
from omnibase_core.container import ModelONEXContainer
|
|
39
|
+
from omnibase_core.models.contracts import ModelContractBase
|
|
40
|
+
|
|
41
|
+
logger = logging.getLogger(__name__)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ContractDependencyResolver:
|
|
45
|
+
"""Resolves protocol dependencies from contracts using container.
|
|
46
|
+
|
|
47
|
+
Reads the `dependencies` field from a node's contract, filters for
|
|
48
|
+
protocol-type dependencies, imports the protocol classes, and resolves
|
|
49
|
+
instances from the container's service_registry.
|
|
50
|
+
|
|
51
|
+
This resolver implements the "fail-fast" principle: if any required
|
|
52
|
+
protocol cannot be resolved, it raises ProtocolDependencyResolutionError
|
|
53
|
+
immediately rather than allowing node creation with missing dependencies.
|
|
54
|
+
|
|
55
|
+
Example:
|
|
56
|
+
>>> from omnibase_core.container import ModelONEXContainer
|
|
57
|
+
>>>
|
|
58
|
+
>>> container = ModelONEXContainer()
|
|
59
|
+
>>> # ... register protocols in container ...
|
|
60
|
+
>>>
|
|
61
|
+
>>> resolver = ContractDependencyResolver(container)
|
|
62
|
+
>>> resolved = await resolver.resolve(node_contract)
|
|
63
|
+
>>>
|
|
64
|
+
>>> # Use resolved dependencies for node creation
|
|
65
|
+
>>> node = NodeRegistry.create(container, dependencies=resolved)
|
|
66
|
+
|
|
67
|
+
.. versionadded:: 0.x.x
|
|
68
|
+
Part of OMN-1732 runtime dependency injection.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def __init__(self, container: ModelONEXContainer) -> None:
|
|
72
|
+
"""Initialize the resolver with a container.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
container: ONEX container with service_registry for protocol resolution.
|
|
76
|
+
"""
|
|
77
|
+
self._container = container
|
|
78
|
+
|
|
79
|
+
async def resolve(
|
|
80
|
+
self,
|
|
81
|
+
contract: ModelContractBase,
|
|
82
|
+
*,
|
|
83
|
+
allow_missing: bool = False,
|
|
84
|
+
) -> ModelResolvedDependencies:
|
|
85
|
+
"""Resolve protocol dependencies from a contract.
|
|
86
|
+
|
|
87
|
+
Reads contract.dependencies, filters for protocol-type entries,
|
|
88
|
+
and resolves each from the container's service_registry.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
contract: The node contract containing dependency declarations.
|
|
92
|
+
allow_missing: If True, skip missing protocols instead of raising.
|
|
93
|
+
Default False (fail-fast behavior).
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
ModelResolvedDependencies containing resolved protocol instances.
|
|
97
|
+
|
|
98
|
+
Raises:
|
|
99
|
+
ProtocolDependencyResolutionError: If any required protocol cannot
|
|
100
|
+
be resolved and allow_missing is False.
|
|
101
|
+
|
|
102
|
+
Example:
|
|
103
|
+
>>> resolved = await resolver.resolve(contract)
|
|
104
|
+
>>> adapter = resolved.get("ProtocolPostgresAdapter")
|
|
105
|
+
"""
|
|
106
|
+
# Extract protocol dependencies from contract
|
|
107
|
+
protocol_deps = self._extract_protocol_dependencies(contract)
|
|
108
|
+
|
|
109
|
+
if not protocol_deps:
|
|
110
|
+
logger.debug(
|
|
111
|
+
"No protocol dependencies declared in contract",
|
|
112
|
+
extra={"contract_name": getattr(contract, "name", "unknown")},
|
|
113
|
+
)
|
|
114
|
+
return ModelResolvedDependencies()
|
|
115
|
+
|
|
116
|
+
# Resolve each protocol from container
|
|
117
|
+
# ONEX_EXCLUDE: any_type - dict holds heterogeneous protocol instances resolved at runtime
|
|
118
|
+
resolved: dict[str, Any] = {}
|
|
119
|
+
missing: list[str] = []
|
|
120
|
+
resolution_errors: dict[str, str] = {}
|
|
121
|
+
|
|
122
|
+
for dep in protocol_deps:
|
|
123
|
+
class_name = dep.get("class_name")
|
|
124
|
+
module_path = dep.get("module")
|
|
125
|
+
|
|
126
|
+
if not class_name:
|
|
127
|
+
logger.warning(
|
|
128
|
+
"Protocol dependency missing class_name, skipping",
|
|
129
|
+
extra={"dependency": dep},
|
|
130
|
+
)
|
|
131
|
+
continue
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
# Import the protocol class
|
|
135
|
+
protocol_class = self._import_protocol_class(class_name, module_path)
|
|
136
|
+
|
|
137
|
+
# Resolve from container
|
|
138
|
+
instance = await self._resolve_from_container(protocol_class)
|
|
139
|
+
resolved[class_name] = instance
|
|
140
|
+
|
|
141
|
+
logger.debug(
|
|
142
|
+
"Resolved protocol dependency",
|
|
143
|
+
extra={
|
|
144
|
+
"protocol": class_name,
|
|
145
|
+
"module_path": module_path,
|
|
146
|
+
},
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
except Exception as e:
|
|
150
|
+
error_msg = str(e)
|
|
151
|
+
resolution_errors[class_name] = error_msg
|
|
152
|
+
missing.append(class_name)
|
|
153
|
+
|
|
154
|
+
logger.warning(
|
|
155
|
+
"Failed to resolve protocol dependency",
|
|
156
|
+
extra={
|
|
157
|
+
"protocol": class_name,
|
|
158
|
+
"module_path": module_path,
|
|
159
|
+
"error": error_msg,
|
|
160
|
+
},
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# Fail-fast if any protocols are missing
|
|
164
|
+
if missing and not allow_missing:
|
|
165
|
+
contract_name = getattr(contract, "name", "unknown")
|
|
166
|
+
context = ModelInfraErrorContext.with_correlation(
|
|
167
|
+
transport_type=EnumInfraTransportType.RUNTIME,
|
|
168
|
+
operation="resolve_dependencies",
|
|
169
|
+
target_name=contract_name,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
error_details = "\n".join(
|
|
173
|
+
f" - {proto}: {resolution_errors.get(proto, 'unknown error')}"
|
|
174
|
+
for proto in missing
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
raise ProtocolDependencyResolutionError(
|
|
178
|
+
f"Cannot create node '{contract_name}': missing required protocols.\n"
|
|
179
|
+
f"The following protocols are declared in contract.yaml but could not "
|
|
180
|
+
f"be resolved from the container:\n{error_details}\n\n"
|
|
181
|
+
f"Ensure these protocols are registered in the container before "
|
|
182
|
+
f"node creation via container.service_registry.register_instance().",
|
|
183
|
+
context=context,
|
|
184
|
+
missing_protocols=missing,
|
|
185
|
+
node_name=contract_name,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
return ModelResolvedDependencies(protocols=resolved)
|
|
189
|
+
|
|
190
|
+
def _extract_protocol_dependencies(
|
|
191
|
+
self,
|
|
192
|
+
contract: ModelContractBase,
|
|
193
|
+
) -> list[dict[str, str]]:
|
|
194
|
+
"""Extract protocol-type dependencies from contract.
|
|
195
|
+
|
|
196
|
+
Supports multiple detection patterns:
|
|
197
|
+
1. `type` field = "protocol" (YAML contract style)
|
|
198
|
+
2. `is_protocol()` method returns True
|
|
199
|
+
3. `dependency_type` attribute with value "PROTOCOL"
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
contract: The contract to extract dependencies from.
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
List of dependency dicts with keys: name, type, class_name, module
|
|
206
|
+
"""
|
|
207
|
+
dependencies: list[dict[str, str]] = []
|
|
208
|
+
|
|
209
|
+
# Get dependencies attribute if present
|
|
210
|
+
contract_deps = getattr(contract, "dependencies", None)
|
|
211
|
+
if not contract_deps:
|
|
212
|
+
return dependencies
|
|
213
|
+
|
|
214
|
+
for dep in contract_deps:
|
|
215
|
+
is_protocol = self._is_protocol_dependency(dep)
|
|
216
|
+
|
|
217
|
+
if is_protocol:
|
|
218
|
+
dep_dict: dict[str, str] = {}
|
|
219
|
+
|
|
220
|
+
# Extract fields - handle both attribute and dict access
|
|
221
|
+
if hasattr(dep, "name"):
|
|
222
|
+
dep_dict["name"] = str(dep.name) if dep.name else ""
|
|
223
|
+
if hasattr(dep, "class_name"):
|
|
224
|
+
dep_dict["class_name"] = (
|
|
225
|
+
str(dep.class_name) if dep.class_name else ""
|
|
226
|
+
)
|
|
227
|
+
if hasattr(dep, "module"):
|
|
228
|
+
dep_dict["module"] = str(dep.module) if dep.module else ""
|
|
229
|
+
|
|
230
|
+
# Only add if we have a class_name
|
|
231
|
+
if dep_dict.get("class_name"):
|
|
232
|
+
dependencies.append(dep_dict)
|
|
233
|
+
|
|
234
|
+
return dependencies
|
|
235
|
+
|
|
236
|
+
# ONEX_EXCLUDE: any_type - dep can be ModelContractDependency or dict from various sources
|
|
237
|
+
def _is_protocol_dependency(self, dep: Any) -> bool:
|
|
238
|
+
"""Check if a dependency is a protocol dependency.
|
|
239
|
+
|
|
240
|
+
Supports multiple detection patterns:
|
|
241
|
+
1. `type` field = "protocol" (YAML contract style)
|
|
242
|
+
2. `is_protocol()` method returns True
|
|
243
|
+
3. `dependency_type` attribute with value "PROTOCOL"
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
dep: The dependency object to check.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
True if this is a protocol dependency, False otherwise.
|
|
250
|
+
"""
|
|
251
|
+
# Check is_protocol() method first (most explicit)
|
|
252
|
+
if hasattr(dep, "is_protocol") and callable(dep.is_protocol):
|
|
253
|
+
if dep.is_protocol():
|
|
254
|
+
return True
|
|
255
|
+
|
|
256
|
+
# Check dependency_type attribute (enum or string)
|
|
257
|
+
if hasattr(dep, "dependency_type"):
|
|
258
|
+
dep_type_val = dep.dependency_type
|
|
259
|
+
if hasattr(dep_type_val, "value"):
|
|
260
|
+
# Enum with .value
|
|
261
|
+
if str(dep_type_val.value).upper() == "PROTOCOL":
|
|
262
|
+
return True
|
|
263
|
+
elif str(dep_type_val).upper() == "PROTOCOL":
|
|
264
|
+
return True
|
|
265
|
+
|
|
266
|
+
# Check type field (YAML contract style: type: "protocol")
|
|
267
|
+
if hasattr(dep, "type"):
|
|
268
|
+
type_val = getattr(dep, "type", "")
|
|
269
|
+
if str(type_val).lower() == "protocol":
|
|
270
|
+
return True
|
|
271
|
+
|
|
272
|
+
return False
|
|
273
|
+
|
|
274
|
+
def _import_protocol_class(
|
|
275
|
+
self,
|
|
276
|
+
class_name: str,
|
|
277
|
+
module_path: str | None,
|
|
278
|
+
) -> type:
|
|
279
|
+
"""Import a protocol class by name and module.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
class_name: The class name to import
|
|
283
|
+
module_path: The module path (required for import)
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
The imported class type.
|
|
287
|
+
|
|
288
|
+
Raises:
|
|
289
|
+
ImportError: If the class cannot be imported.
|
|
290
|
+
"""
|
|
291
|
+
if not module_path:
|
|
292
|
+
raise ImportError(
|
|
293
|
+
f"Protocol '{class_name}' has no module path specified in contract. "
|
|
294
|
+
f"Cannot import without module path."
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
try:
|
|
298
|
+
module = importlib.import_module(module_path)
|
|
299
|
+
protocol_class = getattr(module, class_name)
|
|
300
|
+
return cast("type", protocol_class)
|
|
301
|
+
except ModuleNotFoundError as e:
|
|
302
|
+
raise ImportError(
|
|
303
|
+
f"Module '{module_path}' not found for protocol '{class_name}': {e}"
|
|
304
|
+
) from e
|
|
305
|
+
except AttributeError as e:
|
|
306
|
+
raise ImportError(
|
|
307
|
+
f"Class '{class_name}' not found in module '{module_path}': {e}"
|
|
308
|
+
) from e
|
|
309
|
+
|
|
310
|
+
# ONEX_EXCLUDE: any_type - returns protocol instance, type varies by resolved protocol class
|
|
311
|
+
async def _resolve_from_container(self, protocol_class: type) -> Any:
|
|
312
|
+
"""Resolve a protocol instance from the container.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
protocol_class: The protocol class type to resolve.
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
The resolved protocol instance.
|
|
319
|
+
|
|
320
|
+
Raises:
|
|
321
|
+
RuntimeError: If container.service_registry is None.
|
|
322
|
+
Exception: If resolution fails (propagated from container).
|
|
323
|
+
"""
|
|
324
|
+
if self._container.service_registry is None:
|
|
325
|
+
raise RuntimeError(
|
|
326
|
+
"Container service_registry is None. "
|
|
327
|
+
"Ensure container is properly initialized with service_registry enabled."
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
return await self._container.service_registry.resolve_service(protocol_class)
|
|
331
|
+
|
|
332
|
+
async def resolve_from_path(
|
|
333
|
+
self,
|
|
334
|
+
contract_path: Path,
|
|
335
|
+
*,
|
|
336
|
+
allow_missing: bool = False,
|
|
337
|
+
) -> ModelResolvedDependencies:
|
|
338
|
+
"""Resolve protocol dependencies from a contract.yaml file path.
|
|
339
|
+
|
|
340
|
+
Loads the contract YAML, extracts the dependencies section, and resolves
|
|
341
|
+
each protocol from the container's service_registry. This is a convenience
|
|
342
|
+
method that combines file loading with dependency resolution.
|
|
343
|
+
|
|
344
|
+
Part of OMN-1903: Runtime dependency injection integration.
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
contract_path: Path object to the contract.yaml file.
|
|
348
|
+
allow_missing: If True, skip missing protocols instead of raising.
|
|
349
|
+
Default False (fail-fast behavior).
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
ModelResolvedDependencies containing resolved protocol instances.
|
|
353
|
+
Returns empty ModelResolvedDependencies if contract has no dependencies.
|
|
354
|
+
|
|
355
|
+
Raises:
|
|
356
|
+
ProtocolConfigurationError: If contract file cannot be loaded or parsed.
|
|
357
|
+
ProtocolDependencyResolutionError: If any required protocol cannot
|
|
358
|
+
be resolved and allow_missing is False.
|
|
359
|
+
|
|
360
|
+
Example:
|
|
361
|
+
>>> resolver = ContractDependencyResolver(container)
|
|
362
|
+
>>> resolved = await resolver.resolve_from_path(
|
|
363
|
+
... Path("src/nodes/my_node/contract.yaml")
|
|
364
|
+
... )
|
|
365
|
+
>>> adapter = resolved.get("ProtocolPostgresAdapter")
|
|
366
|
+
|
|
367
|
+
.. versionadded:: 0.x.x
|
|
368
|
+
Part of OMN-1903 runtime dependency injection integration.
|
|
369
|
+
"""
|
|
370
|
+
path = contract_path
|
|
371
|
+
|
|
372
|
+
# Load and parse the contract YAML
|
|
373
|
+
contract_data = self._load_contract_yaml(path)
|
|
374
|
+
|
|
375
|
+
# Check if contract has dependencies section
|
|
376
|
+
if "dependencies" not in contract_data or not contract_data["dependencies"]:
|
|
377
|
+
logger.debug(
|
|
378
|
+
"No dependencies section in contract, skipping resolution",
|
|
379
|
+
extra={"contract_path": str(path)},
|
|
380
|
+
)
|
|
381
|
+
return ModelResolvedDependencies()
|
|
382
|
+
|
|
383
|
+
# Create a lightweight contract object for the resolver
|
|
384
|
+
# Uses SimpleNamespace for duck-typing compatibility with resolve()
|
|
385
|
+
contract_name = contract_data.get("name", path.stem)
|
|
386
|
+
dependencies = [SimpleNamespace(**dep) for dep in contract_data["dependencies"]]
|
|
387
|
+
contract = SimpleNamespace(name=contract_name, dependencies=dependencies)
|
|
388
|
+
|
|
389
|
+
logger.debug(
|
|
390
|
+
"Resolving dependencies from contract path",
|
|
391
|
+
extra={
|
|
392
|
+
"contract_path": str(path),
|
|
393
|
+
"contract_name": contract_name,
|
|
394
|
+
"dependency_count": len(dependencies),
|
|
395
|
+
},
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
# Type ignore: contract is a SimpleNamespace duck-typed to match ModelContractBase
|
|
399
|
+
# The resolver uses getattr() internally so any object with name/dependencies works
|
|
400
|
+
return await self.resolve(contract, allow_missing=allow_missing) # type: ignore[arg-type]
|
|
401
|
+
|
|
402
|
+
# ONEX_EXCLUDE: any_type - yaml.safe_load returns heterogeneous dict, values vary by contract schema
|
|
403
|
+
def _load_contract_yaml(self, path: Path) -> dict[str, Any]:
|
|
404
|
+
"""Load and parse a contract.yaml file.
|
|
405
|
+
|
|
406
|
+
Args:
|
|
407
|
+
path: Path to the contract YAML file.
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
Parsed YAML content as a dictionary.
|
|
411
|
+
|
|
412
|
+
Raises:
|
|
413
|
+
ProtocolConfigurationError: If file doesn't exist or cannot be parsed.
|
|
414
|
+
"""
|
|
415
|
+
if not path.exists():
|
|
416
|
+
context = ModelInfraErrorContext.with_correlation(
|
|
417
|
+
transport_type=EnumInfraTransportType.FILESYSTEM,
|
|
418
|
+
operation="load_contract_yaml",
|
|
419
|
+
target_name=str(path),
|
|
420
|
+
)
|
|
421
|
+
raise ProtocolConfigurationError(
|
|
422
|
+
f"Contract file not found: {path}",
|
|
423
|
+
context=context,
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
try:
|
|
427
|
+
with open(path, encoding="utf-8") as f:
|
|
428
|
+
data = yaml.safe_load(f)
|
|
429
|
+
if data is None:
|
|
430
|
+
data = {}
|
|
431
|
+
# ONEX_EXCLUDE: any_type - yaml.safe_load returns Any, we validate structure elsewhere
|
|
432
|
+
return cast("dict[str, Any]", data)
|
|
433
|
+
except yaml.YAMLError as e:
|
|
434
|
+
context = ModelInfraErrorContext.with_correlation(
|
|
435
|
+
transport_type=EnumInfraTransportType.FILESYSTEM,
|
|
436
|
+
operation="load_contract_yaml",
|
|
437
|
+
target_name=str(path),
|
|
438
|
+
)
|
|
439
|
+
raise ProtocolConfigurationError(
|
|
440
|
+
f"Failed to parse contract YAML at {path}: {e}",
|
|
441
|
+
context=context,
|
|
442
|
+
) from e
|
|
443
|
+
except OSError as e:
|
|
444
|
+
context = ModelInfraErrorContext.with_correlation(
|
|
445
|
+
transport_type=EnumInfraTransportType.FILESYSTEM,
|
|
446
|
+
operation="load_contract_yaml",
|
|
447
|
+
target_name=str(path),
|
|
448
|
+
)
|
|
449
|
+
raise ProtocolConfigurationError(
|
|
450
|
+
f"Failed to read contract file at {path}: {e}",
|
|
451
|
+
context=context,
|
|
452
|
+
) from e
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
__all__ = ["ContractDependencyResolver"]
|
|
@@ -11,10 +11,10 @@ The router:
|
|
|
11
11
|
- Routes to ContractRegistryReducer.reduce() for state transitions
|
|
12
12
|
- Emits intents for Effect layer execution (PostgreSQL operations)
|
|
13
13
|
|
|
14
|
-
Topics Handled:
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
14
|
+
Topics Handled (realm-agnostic, no environment prefix):
|
|
15
|
+
- onex.evt.platform.contract-registered.v1 -> ModelContractRegisteredEvent
|
|
16
|
+
- onex.evt.platform.contract-deregistered.v1 -> ModelContractDeregisteredEvent
|
|
17
|
+
- onex.evt.platform.node-heartbeat.v1 -> ModelNodeHeartbeatEvent
|
|
18
18
|
|
|
19
19
|
Design:
|
|
20
20
|
This class encapsulates the message routing logic for contract registration
|
|
@@ -69,7 +69,7 @@ if TYPE_CHECKING:
|
|
|
69
69
|
logger = logging.getLogger(__name__)
|
|
70
70
|
|
|
71
71
|
# Topic suffix patterns for event type matching
|
|
72
|
-
#
|
|
72
|
+
# Topics are realm-agnostic (no environment prefix)
|
|
73
73
|
TOPIC_SUFFIX_CONTRACT_REGISTERED = "onex.evt.platform.contract-registered.v1"
|
|
74
74
|
TOPIC_SUFFIX_CONTRACT_DEREGISTERED = "onex.evt.platform.contract-deregistered.v1"
|
|
75
75
|
TOPIC_SUFFIX_NODE_HEARTBEAT = "onex.evt.platform.node-heartbeat.v1"
|
|
@@ -25,15 +25,15 @@ Example Usage:
|
|
|
25
25
|
registry.register(
|
|
26
26
|
ModelEventRegistration(
|
|
27
27
|
event_type="custom.event",
|
|
28
|
-
topic_template="
|
|
28
|
+
topic_template="onex.evt.custom.event.v1",
|
|
29
29
|
partition_key_field="session_id",
|
|
30
30
|
required_fields=["session_id", "user_id"],
|
|
31
31
|
)
|
|
32
32
|
)
|
|
33
33
|
|
|
34
|
-
# Resolve topic for event type
|
|
34
|
+
# Resolve topic for event type (realm-agnostic, no env prefix)
|
|
35
35
|
topic = registry.resolve_topic("prompt.submitted")
|
|
36
|
-
# Returns: "
|
|
36
|
+
# Returns: "onex.evt.omniclaude.prompt-submitted.v1"
|
|
37
37
|
|
|
38
38
|
# Inject metadata into payload
|
|
39
39
|
enriched = registry.inject_metadata(
|
|
@@ -70,8 +70,10 @@ class ModelEventRegistration(BaseModel):
|
|
|
70
70
|
Attributes:
|
|
71
71
|
event_type: Semantic event type identifier (e.g., "prompt.submitted").
|
|
72
72
|
This is the logical name used by event emitters.
|
|
73
|
-
topic_template: Kafka topic name
|
|
74
|
-
Example: "
|
|
73
|
+
topic_template: Kafka topic name (realm-agnostic, no environment prefix).
|
|
74
|
+
Example: "onex.evt.omniclaude.prompt-submitted.v1"
|
|
75
|
+
Note: Topics are realm-agnostic in ONEX. The environment/realm is
|
|
76
|
+
enforced via envelope identity, not topic naming.
|
|
75
77
|
partition_key_field: Optional field name in payload to use as partition key.
|
|
76
78
|
When set, ensures events with same key go to same partition for ordering.
|
|
77
79
|
required_fields: List of field names that must be present in payload.
|
|
@@ -82,7 +84,7 @@ class ModelEventRegistration(BaseModel):
|
|
|
82
84
|
Example:
|
|
83
85
|
>>> reg = ModelEventRegistration(
|
|
84
86
|
... event_type="prompt.submitted",
|
|
85
|
-
... topic_template="
|
|
87
|
+
... topic_template="onex.evt.omniclaude.prompt-submitted.v1",
|
|
86
88
|
... partition_key_field="session_id",
|
|
87
89
|
... required_fields=["prompt", "session_id"],
|
|
88
90
|
... schema_version="1.0.0",
|
|
@@ -99,7 +101,7 @@ class ModelEventRegistration(BaseModel):
|
|
|
99
101
|
description="Semantic event type identifier (e.g., 'prompt.submitted')",
|
|
100
102
|
)
|
|
101
103
|
topic_template: str = Field(
|
|
102
|
-
description="Kafka topic name
|
|
104
|
+
description="Kafka topic name (realm-agnostic, no environment prefix)",
|
|
103
105
|
)
|
|
104
106
|
partition_key_field: str | None = Field(
|
|
105
107
|
default=None,
|
|
@@ -133,7 +135,7 @@ class EventRegistry:
|
|
|
133
135
|
>>> registry = EventRegistry(environment="dev")
|
|
134
136
|
>>> topic = registry.resolve_topic("prompt.submitted")
|
|
135
137
|
>>> print(topic)
|
|
136
|
-
'
|
|
138
|
+
'onex.evt.omniclaude.prompt-submitted.v1'
|
|
137
139
|
|
|
138
140
|
>>> registry.validate_payload("prompt.submitted", {"prompt": "Hello"})
|
|
139
141
|
True
|
|
@@ -144,6 +146,11 @@ class EventRegistry:
|
|
|
144
146
|
... )
|
|
145
147
|
>>> "correlation_id" in enriched
|
|
146
148
|
True
|
|
149
|
+
|
|
150
|
+
Note:
|
|
151
|
+
Topics are realm-agnostic in ONEX. The environment is stored for
|
|
152
|
+
potential use in consumer group derivation by related components,
|
|
153
|
+
but topics themselves do not include environment prefixes.
|
|
147
154
|
"""
|
|
148
155
|
|
|
149
156
|
def __init__(self, environment: str = "dev") -> None:
|
|
@@ -156,7 +163,11 @@ class EventRegistry:
|
|
|
156
163
|
Example:
|
|
157
164
|
>>> registry = EventRegistry(environment="staging")
|
|
158
165
|
>>> registry.resolve_topic("prompt.submitted")
|
|
159
|
-
'
|
|
166
|
+
'onex.evt.omniclaude.prompt-submitted.v1'
|
|
167
|
+
|
|
168
|
+
Note:
|
|
169
|
+
The environment is stored for potential consumer group derivation
|
|
170
|
+
in related components. Topics themselves are realm-agnostic.
|
|
160
171
|
"""
|
|
161
172
|
self._environment = environment
|
|
162
173
|
self._registrations: dict[str, ModelEventRegistration] = {}
|
|
@@ -174,25 +185,25 @@ class EventRegistry:
|
|
|
174
185
|
defaults = [
|
|
175
186
|
ModelEventRegistration(
|
|
176
187
|
event_type="prompt.submitted",
|
|
177
|
-
topic_template="
|
|
188
|
+
topic_template="onex.evt.omniclaude.prompt-submitted.v1",
|
|
178
189
|
partition_key_field="session_id",
|
|
179
190
|
required_fields=["prompt"],
|
|
180
191
|
),
|
|
181
192
|
ModelEventRegistration(
|
|
182
193
|
event_type="session.started",
|
|
183
|
-
topic_template="
|
|
194
|
+
topic_template="onex.evt.omniclaude.session-started.v1",
|
|
184
195
|
partition_key_field="session_id",
|
|
185
196
|
required_fields=["session_id"],
|
|
186
197
|
),
|
|
187
198
|
ModelEventRegistration(
|
|
188
199
|
event_type="session.ended",
|
|
189
|
-
topic_template="
|
|
200
|
+
topic_template="onex.evt.omniclaude.session-ended.v1",
|
|
190
201
|
partition_key_field="session_id",
|
|
191
202
|
required_fields=["session_id"],
|
|
192
203
|
),
|
|
193
204
|
ModelEventRegistration(
|
|
194
205
|
event_type="tool.executed",
|
|
195
|
-
topic_template="
|
|
206
|
+
topic_template="onex.evt.omniclaude.tool-executed.v1",
|
|
196
207
|
partition_key_field="session_id",
|
|
197
208
|
required_fields=["tool_name"],
|
|
198
209
|
),
|
|
@@ -214,25 +225,26 @@ class EventRegistry:
|
|
|
214
225
|
>>> registry.register(
|
|
215
226
|
... ModelEventRegistration(
|
|
216
227
|
... event_type="custom.event",
|
|
217
|
-
... topic_template="
|
|
228
|
+
... topic_template="onex.evt.custom.event.v1",
|
|
218
229
|
... )
|
|
219
230
|
... )
|
|
220
231
|
>>> registry.resolve_topic("custom.event")
|
|
221
|
-
'
|
|
232
|
+
'onex.evt.custom.event.v1'
|
|
222
233
|
"""
|
|
223
234
|
self._registrations[registration.event_type] = registration
|
|
224
235
|
|
|
225
236
|
def resolve_topic(self, event_type: str) -> str:
|
|
226
|
-
"""Get the Kafka topic for an event type.
|
|
237
|
+
"""Get the Kafka topic for an event type (realm-agnostic).
|
|
227
238
|
|
|
228
|
-
|
|
229
|
-
|
|
239
|
+
Topics are realm-agnostic in ONEX. The environment/realm is enforced via
|
|
240
|
+
envelope identity, not topic naming. This enables cross-environment event
|
|
241
|
+
routing when needed while maintaining proper isolation through identity.
|
|
230
242
|
|
|
231
243
|
Args:
|
|
232
244
|
event_type: Semantic event type identifier.
|
|
233
245
|
|
|
234
246
|
Returns:
|
|
235
|
-
|
|
247
|
+
Kafka topic name (no environment prefix).
|
|
236
248
|
|
|
237
249
|
Raises:
|
|
238
250
|
OnexError: If the event type is not registered.
|
|
@@ -240,7 +252,7 @@ class EventRegistry:
|
|
|
240
252
|
Example:
|
|
241
253
|
>>> registry = EventRegistry(environment="prod")
|
|
242
254
|
>>> registry.resolve_topic("prompt.submitted")
|
|
243
|
-
'
|
|
255
|
+
'onex.evt.omniclaude.prompt-submitted.v1'
|
|
244
256
|
"""
|
|
245
257
|
registration = self._registrations.get(event_type)
|
|
246
258
|
if registration is None:
|
|
@@ -248,7 +260,7 @@ class EventRegistry:
|
|
|
248
260
|
raise OnexError(
|
|
249
261
|
f"Unknown event type: '{event_type}'. Registered types: {registered}"
|
|
250
262
|
)
|
|
251
|
-
return registration.topic_template
|
|
263
|
+
return registration.topic_template
|
|
252
264
|
|
|
253
265
|
def get_partition_key(
|
|
254
266
|
self,
|
|
@@ -452,7 +464,7 @@ class EventRegistry:
|
|
|
452
464
|
>>> registry = EventRegistry()
|
|
453
465
|
>>> reg = registry.get_registration("prompt.submitted")
|
|
454
466
|
>>> reg.topic_template
|
|
455
|
-
'
|
|
467
|
+
'onex.evt.omniclaude.prompt-submitted.v1'
|
|
456
468
|
"""
|
|
457
469
|
return self._registrations.get(event_type)
|
|
458
470
|
|