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.
- 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/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/handlers/registration_storage/handler_registration_storage_postgres.py +29 -20
- omnibase_infra/mixins/__init__.py +14 -0
- omnibase_infra/mixins/mixin_node_introspection.py +42 -20
- 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/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/{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/models/runtime/__init__.py +4 -0
- omnibase_infra/models/runtime/model_resolved_dependencies.py +116 -0
- omnibase_infra/nodes/contract_registry_reducer/__init__.py +5 -0
- omnibase_infra/nodes/contract_registry_reducer/contract.yaml +6 -5
- omnibase_infra/nodes/contract_registry_reducer/contract_registration_event_router.py +689 -0
- omnibase_infra/nodes/contract_registry_reducer/reducer.py +9 -26
- 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 +131 -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 +251 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_postgres_intent_payload.py +8 -12
- omnibase_infra/nodes/node_registry_effect/models/__init__.py +2 -2
- 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/projectors/__init__.py +6 -0
- omnibase_infra/projectors/projection_reader_contract.py +1301 -0
- omnibase_infra/runtime/__init__.py +12 -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 +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/emit_daemon/event_registry.py +34 -22
- omnibase_infra/runtime/event_bus_subcontract_wiring.py +63 -23
- 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/publisher_topic_scoped.py +16 -11
- omnibase_infra/runtime/registry_policy.py +29 -15
- omnibase_infra/runtime/request_response_wiring.py +793 -0
- omnibase_infra/runtime/service_kernel.py +295 -8
- 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/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/utils/__init__.py +7 -0
- omnibase_infra/utils/util_db_error_context.py +292 -0
- omnibase_infra/validation/infra_validators.py +3 -1
- omnibase_infra/validation/validation_exemptions.yaml +65 -0
- {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.4.0.dist-info}/METADATA +3 -3
- {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.4.0.dist-info}/RECORD +117 -58
- {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.4.0.dist-info}/WHEEL +0 -0
- {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.4.0.dist-info}/entry_points.txt +0 -0
- {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.4.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -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"]
|
|
@@ -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
|
|
|
@@ -9,7 +9,7 @@ all Kafka plumbing - nodes/handlers never create consumers or producers directly
|
|
|
9
9
|
Architecture:
|
|
10
10
|
The EventBusSubcontractWiring class is responsible for:
|
|
11
11
|
1. Reading `subscribe_topics` from ModelEventBusSubcontract
|
|
12
|
-
2.
|
|
12
|
+
2. Passing topic suffixes through unchanged (topics are realm-agnostic)
|
|
13
13
|
3. Creating Kafka subscriptions with appropriate consumer groups
|
|
14
14
|
4. Bridging received messages to the MessageDispatchEngine
|
|
15
15
|
5. Managing subscription lifecycle (creation and cleanup)
|
|
@@ -50,16 +50,21 @@ DLQ Consumer Group Alignment:
|
|
|
50
50
|
3. Using it in all _publish_to_dlq calls within the callback closure
|
|
51
51
|
|
|
52
52
|
Topic Resolution:
|
|
53
|
+
Topics are realm-agnostic and do NOT include environment prefixes.
|
|
54
|
+
The environment/realm is a routing boundary enforced via envelope identity,
|
|
55
|
+
not a topic prefix. This enables cross-environment event routing when needed.
|
|
56
|
+
|
|
53
57
|
Topic suffixes from contracts follow the ONEX naming convention:
|
|
54
58
|
onex.{kind}.{producer}.{event-name}.v{n}
|
|
55
59
|
|
|
56
|
-
The wiring
|
|
57
|
-
|
|
60
|
+
The wiring passes these topic suffixes through unchanged:
|
|
61
|
+
onex.{kind}.{producer}.{event-name}.v{n}
|
|
58
62
|
|
|
59
63
|
Example:
|
|
60
64
|
- Contract declares: "onex.evt.omniintelligence.intent-classified.v1"
|
|
61
|
-
- Resolved
|
|
62
|
-
|
|
65
|
+
- Resolved: "onex.evt.omniintelligence.intent-classified.v1"
|
|
66
|
+
|
|
67
|
+
Note: Consumer groups still include environment for isolation.
|
|
63
68
|
|
|
64
69
|
Related:
|
|
65
70
|
- OMN-1621: Runtime consumes event_bus subcontract for contract-driven wiring
|
|
@@ -93,12 +98,13 @@ from omnibase_core.protocols.event_bus.protocol_event_bus_subscriber import (
|
|
|
93
98
|
from omnibase_core.protocols.event_bus.protocol_event_message import (
|
|
94
99
|
ProtocolEventMessage,
|
|
95
100
|
)
|
|
96
|
-
from omnibase_infra.enums import EnumInfraTransportType
|
|
101
|
+
from omnibase_infra.enums import EnumConsumerGroupPurpose, EnumInfraTransportType
|
|
97
102
|
from omnibase_infra.errors import (
|
|
98
103
|
ModelInfraErrorContext,
|
|
99
104
|
ProtocolConfigurationError,
|
|
100
105
|
RuntimeHostError,
|
|
101
106
|
)
|
|
107
|
+
from omnibase_infra.models import ModelNodeIdentity
|
|
102
108
|
from omnibase_infra.models.event_bus import (
|
|
103
109
|
ModelConsumerRetryConfig,
|
|
104
110
|
ModelDlqConfig,
|
|
@@ -106,6 +112,7 @@ from omnibase_infra.models.event_bus import (
|
|
|
106
112
|
ModelOffsetPolicyConfig,
|
|
107
113
|
)
|
|
108
114
|
from omnibase_infra.protocols import ProtocolDispatchEngine, ProtocolIdempotencyStore
|
|
115
|
+
from omnibase_infra.utils import compute_consumer_group_id
|
|
109
116
|
|
|
110
117
|
if TYPE_CHECKING:
|
|
111
118
|
from omnibase_infra.event_bus.event_bus_inmemory import EventBusInmemory
|
|
@@ -124,7 +131,7 @@ class EventBusSubcontractWiring:
|
|
|
124
131
|
|
|
125
132
|
Responsibilities:
|
|
126
133
|
- Parse subscribe_topics from ModelEventBusSubcontract
|
|
127
|
-
-
|
|
134
|
+
- Pass topic suffixes through unchanged (topics are realm-agnostic)
|
|
128
135
|
- Create Kafka subscriptions with appropriate consumer groups
|
|
129
136
|
- Deserialize incoming messages to ModelEventEnvelope
|
|
130
137
|
- Check idempotency and skip duplicate messages (if enabled)
|
|
@@ -194,7 +201,7 @@ class EventBusSubcontractWiring:
|
|
|
194
201
|
Attributes:
|
|
195
202
|
_event_bus: The event bus implementation (Kafka or in-memory)
|
|
196
203
|
_dispatch_engine: Engine to dispatch received messages to handlers
|
|
197
|
-
_environment: Environment
|
|
204
|
+
_environment: Environment identifier for consumer groups (e.g., 'dev', 'prod')
|
|
198
205
|
_node_name: Name of the node/handler for consumer group and logging
|
|
199
206
|
_idempotency_store: Optional store for tracking processed messages
|
|
200
207
|
_idempotency_config: Configuration for idempotency behavior
|
|
@@ -217,6 +224,8 @@ class EventBusSubcontractWiring:
|
|
|
217
224
|
dispatch_engine: ProtocolDispatchEngine,
|
|
218
225
|
environment: str,
|
|
219
226
|
node_name: str,
|
|
227
|
+
service: str,
|
|
228
|
+
version: str,
|
|
220
229
|
idempotency_store: ProtocolIdempotencyStore | None = None,
|
|
221
230
|
idempotency_config: ModelIdempotencyConfig | None = None,
|
|
222
231
|
dlq_config: ModelDlqConfig | None = None,
|
|
@@ -227,14 +236,19 @@ class EventBusSubcontractWiring:
|
|
|
227
236
|
|
|
228
237
|
Args:
|
|
229
238
|
event_bus: The event bus implementation (EventBusKafka or EventBusInmemory).
|
|
230
|
-
Must implement subscribe(topic,
|
|
239
|
+
Must implement subscribe(topic, node_identity, on_message) -> unsubscribe callable.
|
|
231
240
|
Duck typed per ONEX patterns.
|
|
232
241
|
dispatch_engine: Engine to dispatch received messages to handlers.
|
|
233
242
|
Must implement ProtocolDispatchEngine interface.
|
|
234
243
|
Must be frozen (registrations complete) before wiring subscriptions.
|
|
235
|
-
environment: Environment
|
|
236
|
-
Used
|
|
244
|
+
environment: Environment identifier (e.g., 'dev', 'prod').
|
|
245
|
+
Used for consumer group naming and node identity. Topics are
|
|
246
|
+
realm-agnostic and do not include environment prefixes.
|
|
237
247
|
node_name: Name of the node/handler for consumer group identification and logging.
|
|
248
|
+
service: Service name for node identity (e.g., 'omniintelligence', 'omnibridge').
|
|
249
|
+
Used to derive consumer group ID.
|
|
250
|
+
version: Version string for node identity (e.g., 'v1', 'v1.0.0').
|
|
251
|
+
Used to derive consumer group ID.
|
|
238
252
|
idempotency_store: Optional idempotency store for message deduplication.
|
|
239
253
|
If provided with enabled config, messages are deduplicated by envelope_id.
|
|
240
254
|
idempotency_config: Optional configuration for idempotency behavior.
|
|
@@ -254,15 +268,21 @@ class EventBusSubcontractWiring:
|
|
|
254
268
|
Attempting to dispatch to an unfrozen engine will raise an error.
|
|
255
269
|
|
|
256
270
|
Raises:
|
|
257
|
-
ValueError: If environment is empty or whitespace-only.
|
|
271
|
+
ValueError: If environment, service, or version is empty or whitespace-only.
|
|
258
272
|
"""
|
|
259
273
|
if not environment or not environment.strip():
|
|
260
274
|
raise ValueError("environment must be a non-empty string")
|
|
275
|
+
if not service or not service.strip():
|
|
276
|
+
raise ValueError("service must be a non-empty string")
|
|
277
|
+
if not version or not version.strip():
|
|
278
|
+
raise ValueError("version must be a non-empty string")
|
|
261
279
|
|
|
262
280
|
self._event_bus = event_bus
|
|
263
281
|
self._dispatch_engine = dispatch_engine
|
|
264
282
|
self._environment = environment
|
|
265
283
|
self._node_name = node_name
|
|
284
|
+
self._service = service
|
|
285
|
+
self._version = version
|
|
266
286
|
self._idempotency_store = idempotency_store
|
|
267
287
|
self._idempotency_config = idempotency_config or ModelIdempotencyConfig()
|
|
268
288
|
self._dlq_config = dlq_config or ModelDlqConfig()
|
|
@@ -274,28 +294,36 @@ class EventBusSubcontractWiring:
|
|
|
274
294
|
self._retry_counts: dict[UUID, int] = {}
|
|
275
295
|
|
|
276
296
|
def resolve_topic(self, topic_suffix: str) -> str:
|
|
277
|
-
"""Resolve topic suffix to
|
|
297
|
+
"""Resolve topic suffix to topic name (realm-agnostic, no environment prefix).
|
|
298
|
+
|
|
299
|
+
Topics are realm-agnostic in ONEX. The environment/realm is enforced via
|
|
300
|
+
envelope identity, not topic naming. This enables cross-environment event
|
|
301
|
+
routing when needed while maintaining proper isolation through identity.
|
|
278
302
|
|
|
279
303
|
Topic suffixes from contracts follow the ONEX naming convention:
|
|
280
304
|
onex.{kind}.{producer}.{event-name}.v{n}
|
|
281
305
|
|
|
282
|
-
This method
|
|
283
|
-
|
|
306
|
+
This method returns the topic suffix unchanged:
|
|
307
|
+
onex.{kind}.{producer}.{event-name}.v{n}
|
|
284
308
|
|
|
285
309
|
Args:
|
|
286
310
|
topic_suffix: ONEX format topic suffix
|
|
287
311
|
(e.g., 'onex.evt.omniintelligence.intent-classified.v1')
|
|
288
312
|
|
|
289
313
|
Returns:
|
|
290
|
-
|
|
291
|
-
(e.g., '
|
|
314
|
+
Topic name (same as suffix, no environment prefix)
|
|
315
|
+
(e.g., 'onex.evt.omniintelligence.intent-classified.v1')
|
|
292
316
|
|
|
293
317
|
Example:
|
|
294
318
|
>>> wiring = EventBusSubcontractWiring(bus, engine, "dev")
|
|
295
319
|
>>> wiring.resolve_topic("onex.evt.user.created.v1")
|
|
296
|
-
'
|
|
320
|
+
'onex.evt.user.created.v1'
|
|
321
|
+
|
|
322
|
+
Note:
|
|
323
|
+
Consumer groups still include environment for proper isolation.
|
|
324
|
+
See wire_subscriptions() for consumer group naming.
|
|
297
325
|
"""
|
|
298
|
-
return
|
|
326
|
+
return topic_suffix
|
|
299
327
|
|
|
300
328
|
async def wire_subscriptions(
|
|
301
329
|
self,
|
|
@@ -348,9 +376,21 @@ class EventBusSubcontractWiring:
|
|
|
348
376
|
|
|
349
377
|
for topic_suffix in subcontract.subscribe_topics:
|
|
350
378
|
full_topic = self.resolve_topic(topic_suffix)
|
|
351
|
-
|
|
352
|
-
#
|
|
353
|
-
|
|
379
|
+
|
|
380
|
+
# Create typed node identity for consumer group derivation
|
|
381
|
+
# The event bus derives consumer group as: {env}.{service}.{node_name}.{purpose}.{version}
|
|
382
|
+
node_identity = ModelNodeIdentity(
|
|
383
|
+
env=self._environment,
|
|
384
|
+
service=self._service,
|
|
385
|
+
node_name=node_name,
|
|
386
|
+
version=self._version,
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
# Consumer group for logging and DLQ traceability
|
|
390
|
+
# Use shared helper for consistent derivation across codebase
|
|
391
|
+
consumer_group = compute_consumer_group_id(
|
|
392
|
+
node_identity, EnumConsumerGroupPurpose.CONSUME
|
|
393
|
+
)
|
|
354
394
|
|
|
355
395
|
# Create dispatch callback for this topic, capturing the consumer_group
|
|
356
396
|
# used for this subscription to ensure DLQ messages have consistent
|
|
@@ -360,7 +400,7 @@ class EventBusSubcontractWiring:
|
|
|
360
400
|
# Subscribe and store unsubscribe callable
|
|
361
401
|
unsubscribe = await self._event_bus.subscribe(
|
|
362
402
|
topic=full_topic,
|
|
363
|
-
|
|
403
|
+
node_identity=node_identity,
|
|
364
404
|
on_message=callback,
|
|
365
405
|
)
|
|
366
406
|
self._unsubscribe_callables.append(unsubscribe)
|