omnibase_infra 0.2.5__py3-none-any.whl → 0.2.7__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/constants_topic_patterns.py +26 -0
- omnibase_infra/enums/__init__.py +3 -0
- omnibase_infra/enums/enum_consumer_group_purpose.py +92 -0
- omnibase_infra/enums/enum_handler_source_mode.py +16 -2
- omnibase_infra/errors/__init__.py +4 -0
- omnibase_infra/errors/error_binding_resolution.py +128 -0
- omnibase_infra/event_bus/configs/kafka_event_bus_config.yaml +0 -2
- omnibase_infra/event_bus/event_bus_inmemory.py +64 -10
- omnibase_infra/event_bus/event_bus_kafka.py +105 -47
- omnibase_infra/event_bus/mixin_kafka_broadcast.py +3 -7
- omnibase_infra/event_bus/mixin_kafka_dlq.py +12 -6
- omnibase_infra/event_bus/models/config/model_kafka_event_bus_config.py +0 -81
- omnibase_infra/event_bus/testing/__init__.py +26 -0
- omnibase_infra/event_bus/testing/adapter_protocol_event_publisher_inmemory.py +418 -0
- omnibase_infra/event_bus/testing/model_publisher_metrics.py +64 -0
- omnibase_infra/handlers/handler_consul.py +2 -0
- omnibase_infra/handlers/mixins/__init__.py +5 -0
- omnibase_infra/handlers/mixins/mixin_consul_service.py +274 -10
- omnibase_infra/handlers/mixins/mixin_consul_topic_index.py +585 -0
- omnibase_infra/handlers/models/model_filesystem_config.py +4 -4
- omnibase_infra/migrations/001_create_event_ledger.sql +166 -0
- omnibase_infra/migrations/001_drop_event_ledger.sql +18 -0
- omnibase_infra/mixins/mixin_node_introspection.py +189 -19
- omnibase_infra/models/__init__.py +8 -0
- omnibase_infra/models/bindings/__init__.py +59 -0
- omnibase_infra/models/bindings/constants.py +144 -0
- omnibase_infra/models/bindings/model_binding_resolution_result.py +103 -0
- omnibase_infra/models/bindings/model_operation_binding.py +44 -0
- omnibase_infra/models/bindings/model_operation_bindings_subcontract.py +152 -0
- omnibase_infra/models/bindings/model_parsed_binding.py +52 -0
- omnibase_infra/models/discovery/model_introspection_config.py +25 -17
- omnibase_infra/models/dispatch/__init__.py +8 -0
- omnibase_infra/models/dispatch/model_debug_trace_snapshot.py +114 -0
- omnibase_infra/models/dispatch/model_materialized_dispatch.py +141 -0
- omnibase_infra/models/handlers/model_handler_source_config.py +1 -1
- omnibase_infra/models/model_node_identity.py +126 -0
- omnibase_infra/models/projection/model_snapshot_topic_config.py +3 -2
- omnibase_infra/models/registration/__init__.py +9 -0
- omnibase_infra/models/registration/model_event_bus_topic_entry.py +59 -0
- omnibase_infra/models/registration/model_node_event_bus_config.py +99 -0
- omnibase_infra/models/registration/model_node_introspection_event.py +11 -0
- omnibase_infra/models/runtime/__init__.py +9 -0
- omnibase_infra/models/validation/model_coverage_metrics.py +2 -2
- omnibase_infra/nodes/__init__.py +9 -0
- omnibase_infra/nodes/contract_registry_reducer/__init__.py +29 -0
- omnibase_infra/nodes/contract_registry_reducer/contract.yaml +255 -0
- omnibase_infra/nodes/contract_registry_reducer/models/__init__.py +38 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_contract_registry_state.py +266 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_payload_cleanup_topic_references.py +55 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_payload_deactivate_contract.py +58 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_payload_mark_stale.py +49 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_payload_update_heartbeat.py +71 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_payload_update_topic.py +66 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_payload_upsert_contract.py +92 -0
- omnibase_infra/nodes/contract_registry_reducer/node.py +121 -0
- omnibase_infra/nodes/contract_registry_reducer/reducer.py +784 -0
- omnibase_infra/nodes/contract_registry_reducer/registry/__init__.py +9 -0
- omnibase_infra/nodes/contract_registry_reducer/registry/registry_infra_contract_registry_reducer.py +101 -0
- omnibase_infra/nodes/handlers/consul/contract.yaml +85 -0
- omnibase_infra/nodes/handlers/db/contract.yaml +72 -0
- omnibase_infra/nodes/handlers/graph/contract.yaml +127 -0
- omnibase_infra/nodes/handlers/http/contract.yaml +74 -0
- omnibase_infra/nodes/handlers/intent/contract.yaml +66 -0
- omnibase_infra/nodes/handlers/mcp/contract.yaml +69 -0
- omnibase_infra/nodes/handlers/vault/contract.yaml +91 -0
- omnibase_infra/nodes/node_ledger_projection_compute/__init__.py +50 -0
- omnibase_infra/nodes/node_ledger_projection_compute/contract.yaml +104 -0
- omnibase_infra/nodes/node_ledger_projection_compute/node.py +284 -0
- omnibase_infra/nodes/node_ledger_projection_compute/registry/__init__.py +29 -0
- omnibase_infra/nodes/node_ledger_projection_compute/registry/registry_infra_ledger_projection.py +118 -0
- omnibase_infra/nodes/node_ledger_write_effect/__init__.py +82 -0
- omnibase_infra/nodes/node_ledger_write_effect/contract.yaml +200 -0
- omnibase_infra/nodes/node_ledger_write_effect/handlers/__init__.py +22 -0
- omnibase_infra/nodes/node_ledger_write_effect/handlers/handler_ledger_append.py +372 -0
- omnibase_infra/nodes/node_ledger_write_effect/handlers/handler_ledger_query.py +597 -0
- omnibase_infra/nodes/node_ledger_write_effect/models/__init__.py +31 -0
- omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_append_result.py +54 -0
- omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_entry.py +92 -0
- omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_query.py +53 -0
- omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_query_result.py +41 -0
- omnibase_infra/nodes/node_ledger_write_effect/node.py +89 -0
- omnibase_infra/nodes/node_ledger_write_effect/protocols/__init__.py +13 -0
- omnibase_infra/nodes/node_ledger_write_effect/protocols/protocol_ledger_persistence.py +127 -0
- omnibase_infra/nodes/node_ledger_write_effect/registry/__init__.py +9 -0
- omnibase_infra/nodes/node_ledger_write_effect/registry/registry_infra_ledger_write.py +121 -0
- omnibase_infra/nodes/node_registration_orchestrator/registry/registry_infra_node_registration_orchestrator.py +7 -5
- omnibase_infra/nodes/reducers/models/__init__.py +7 -2
- omnibase_infra/nodes/reducers/models/model_payload_consul_register.py +11 -0
- omnibase_infra/nodes/reducers/models/model_payload_ledger_append.py +133 -0
- omnibase_infra/nodes/reducers/registration_reducer.py +1 -0
- omnibase_infra/protocols/__init__.py +3 -0
- omnibase_infra/protocols/protocol_dispatch_engine.py +152 -0
- omnibase_infra/runtime/__init__.py +60 -0
- omnibase_infra/runtime/binding_resolver.py +753 -0
- omnibase_infra/runtime/constants_security.py +70 -0
- omnibase_infra/runtime/contract_loaders/__init__.py +9 -0
- omnibase_infra/runtime/contract_loaders/operation_bindings_loader.py +789 -0
- omnibase_infra/runtime/emit_daemon/__init__.py +97 -0
- omnibase_infra/runtime/emit_daemon/cli.py +844 -0
- omnibase_infra/runtime/emit_daemon/client.py +811 -0
- omnibase_infra/runtime/emit_daemon/config.py +535 -0
- omnibase_infra/runtime/emit_daemon/daemon.py +812 -0
- omnibase_infra/runtime/emit_daemon/event_registry.py +477 -0
- omnibase_infra/runtime/emit_daemon/model_daemon_request.py +139 -0
- omnibase_infra/runtime/emit_daemon/model_daemon_response.py +191 -0
- omnibase_infra/runtime/emit_daemon/queue.py +618 -0
- omnibase_infra/runtime/event_bus_subcontract_wiring.py +466 -0
- omnibase_infra/runtime/handler_source_resolver.py +43 -2
- omnibase_infra/runtime/kafka_contract_source.py +984 -0
- omnibase_infra/runtime/models/__init__.py +13 -0
- omnibase_infra/runtime/models/model_contract_load_result.py +224 -0
- omnibase_infra/runtime/models/model_runtime_contract_config.py +268 -0
- omnibase_infra/runtime/models/model_runtime_scheduler_config.py +4 -3
- omnibase_infra/runtime/models/model_security_config.py +109 -0
- omnibase_infra/runtime/publisher_topic_scoped.py +294 -0
- omnibase_infra/runtime/runtime_contract_config_loader.py +406 -0
- omnibase_infra/runtime/service_kernel.py +76 -6
- omnibase_infra/runtime/service_message_dispatch_engine.py +558 -15
- omnibase_infra/runtime/service_runtime_host_process.py +770 -20
- omnibase_infra/runtime/transition_notification_publisher.py +3 -2
- omnibase_infra/runtime/util_wiring.py +206 -62
- omnibase_infra/services/mcp/service_mcp_tool_sync.py +27 -9
- omnibase_infra/services/session/config_consumer.py +25 -8
- omnibase_infra/services/session/config_store.py +2 -2
- omnibase_infra/services/session/consumer.py +1 -1
- omnibase_infra/topics/__init__.py +45 -0
- omnibase_infra/topics/platform_topic_suffixes.py +140 -0
- omnibase_infra/topics/util_topic_composition.py +95 -0
- omnibase_infra/types/typed_dict/__init__.py +9 -1
- omnibase_infra/types/typed_dict/typed_dict_envelope_build_params.py +115 -0
- omnibase_infra/utils/__init__.py +9 -0
- omnibase_infra/utils/util_consumer_group.py +232 -0
- omnibase_infra/validation/infra_validators.py +18 -1
- omnibase_infra/validation/validation_exemptions.yaml +192 -0
- {omnibase_infra-0.2.5.dist-info → omnibase_infra-0.2.7.dist-info}/METADATA +3 -3
- {omnibase_infra-0.2.5.dist-info → omnibase_infra-0.2.7.dist-info}/RECORD +139 -52
- {omnibase_infra-0.2.5.dist-info → omnibase_infra-0.2.7.dist-info}/entry_points.txt +1 -0
- {omnibase_infra-0.2.5.dist-info → omnibase_infra-0.2.7.dist-info}/WHEEL +0 -0
- {omnibase_infra-0.2.5.dist-info → omnibase_infra-0.2.7.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 OmniNode Team
|
|
3
|
+
"""Event Registry - Event type to Kafka topic mapping for Hook Event Daemon.
|
|
4
|
+
|
|
5
|
+
This module provides the event registry that maps semantic event types
|
|
6
|
+
(e.g., "prompt.submitted") to Kafka topics and handles metadata injection.
|
|
7
|
+
|
|
8
|
+
The registry is the central configuration point for:
|
|
9
|
+
- Event type → topic routing
|
|
10
|
+
- Partition key extraction
|
|
11
|
+
- Payload validation
|
|
12
|
+
- Metadata injection (correlation IDs, timestamps, schema versions)
|
|
13
|
+
|
|
14
|
+
Example Usage:
|
|
15
|
+
```python
|
|
16
|
+
from omnibase_infra.runtime.emit_daemon.event_registry import (
|
|
17
|
+
EventRegistry,
|
|
18
|
+
ModelEventRegistration,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# Create registry with environment prefix
|
|
22
|
+
registry = EventRegistry(environment="dev")
|
|
23
|
+
|
|
24
|
+
# Register a custom event type
|
|
25
|
+
registry.register(
|
|
26
|
+
ModelEventRegistration(
|
|
27
|
+
event_type="custom.event",
|
|
28
|
+
topic_template="{env}.onex.evt.custom.event.v1",
|
|
29
|
+
partition_key_field="session_id",
|
|
30
|
+
required_fields=["session_id", "user_id"],
|
|
31
|
+
)
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# Resolve topic for event type
|
|
35
|
+
topic = registry.resolve_topic("prompt.submitted")
|
|
36
|
+
# Returns: "dev.onex.evt.omniclaude.prompt-submitted.v1"
|
|
37
|
+
|
|
38
|
+
# Inject metadata into payload
|
|
39
|
+
enriched = registry.inject_metadata(
|
|
40
|
+
event_type="prompt.submitted",
|
|
41
|
+
payload={"prompt": "Hello", "session_id": "abc123"},
|
|
42
|
+
correlation_id="corr-123",
|
|
43
|
+
)
|
|
44
|
+
# Returns payload with correlation_id, causation_id, emitted_at, schema_version
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Integration Points:
|
|
48
|
+
- EmitDaemon uses this registry to route events to correct Kafka topics
|
|
49
|
+
- Hook events from OmniClaude are routed through this registry
|
|
50
|
+
- Metadata injection ensures event traceability across the system
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
from __future__ import annotations
|
|
54
|
+
|
|
55
|
+
from datetime import UTC, datetime
|
|
56
|
+
from uuid import uuid4
|
|
57
|
+
|
|
58
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
59
|
+
|
|
60
|
+
from omnibase_core.errors import OnexError
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class ModelEventRegistration(BaseModel):
|
|
64
|
+
"""Registration configuration for a single event type.
|
|
65
|
+
|
|
66
|
+
Defines how a semantic event type (e.g., "prompt.submitted") maps to
|
|
67
|
+
Kafka infrastructure including topic naming, partition keys, and
|
|
68
|
+
payload validation rules.
|
|
69
|
+
|
|
70
|
+
Attributes:
|
|
71
|
+
event_type: Semantic event type identifier (e.g., "prompt.submitted").
|
|
72
|
+
This is the logical name used by event emitters.
|
|
73
|
+
topic_template: Kafka topic name template with {env} placeholder.
|
|
74
|
+
Example: "{env}.onex.evt.omniclaude.prompt-submitted.v1"
|
|
75
|
+
partition_key_field: Optional field name in payload to use as partition key.
|
|
76
|
+
When set, ensures events with same key go to same partition for ordering.
|
|
77
|
+
required_fields: List of field names that must be present in payload.
|
|
78
|
+
Validation will fail if any required field is missing.
|
|
79
|
+
schema_version: Semantic version of the event schema (default: "1.0.0").
|
|
80
|
+
Injected into event metadata for schema evolution tracking.
|
|
81
|
+
|
|
82
|
+
Example:
|
|
83
|
+
>>> reg = ModelEventRegistration(
|
|
84
|
+
... event_type="prompt.submitted",
|
|
85
|
+
... topic_template="{env}.onex.evt.omniclaude.prompt-submitted.v1",
|
|
86
|
+
... partition_key_field="session_id",
|
|
87
|
+
... required_fields=["prompt", "session_id"],
|
|
88
|
+
... schema_version="1.0.0",
|
|
89
|
+
... )
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
model_config = ConfigDict(
|
|
93
|
+
strict=True,
|
|
94
|
+
frozen=True,
|
|
95
|
+
extra="forbid",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
event_type: str = Field(
|
|
99
|
+
description="Semantic event type identifier (e.g., 'prompt.submitted')",
|
|
100
|
+
)
|
|
101
|
+
topic_template: str = Field(
|
|
102
|
+
description="Kafka topic name template with {env} placeholder",
|
|
103
|
+
)
|
|
104
|
+
partition_key_field: str | None = Field(
|
|
105
|
+
default=None,
|
|
106
|
+
description="Optional field name in payload to use as partition key",
|
|
107
|
+
)
|
|
108
|
+
required_fields: list[str] = Field(
|
|
109
|
+
default_factory=list,
|
|
110
|
+
description="List of field names that must be present in payload",
|
|
111
|
+
)
|
|
112
|
+
schema_version: str = Field(
|
|
113
|
+
default="1.0.0",
|
|
114
|
+
description="Semantic version of the event schema",
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class EventRegistry:
|
|
119
|
+
"""Registry for event type to Kafka topic mappings.
|
|
120
|
+
|
|
121
|
+
Manages the mapping between semantic event types and Kafka infrastructure,
|
|
122
|
+
including topic resolution, partition key extraction, payload validation,
|
|
123
|
+
and metadata injection.
|
|
124
|
+
|
|
125
|
+
The registry is initialized with default OmniClaude event types and can
|
|
126
|
+
be extended with custom event registrations.
|
|
127
|
+
|
|
128
|
+
Attributes:
|
|
129
|
+
environment: Deployment environment name (e.g., "dev", "staging", "prod").
|
|
130
|
+
Used to substitute {env} placeholder in topic templates.
|
|
131
|
+
|
|
132
|
+
Example:
|
|
133
|
+
>>> registry = EventRegistry(environment="dev")
|
|
134
|
+
>>> topic = registry.resolve_topic("prompt.submitted")
|
|
135
|
+
>>> print(topic)
|
|
136
|
+
'dev.onex.evt.omniclaude.prompt-submitted.v1'
|
|
137
|
+
|
|
138
|
+
>>> registry.validate_payload("prompt.submitted", {"prompt": "Hello"})
|
|
139
|
+
True
|
|
140
|
+
|
|
141
|
+
>>> enriched = registry.inject_metadata(
|
|
142
|
+
... "prompt.submitted",
|
|
143
|
+
... {"prompt": "Hello"},
|
|
144
|
+
... )
|
|
145
|
+
>>> "correlation_id" in enriched
|
|
146
|
+
True
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
def __init__(self, environment: str = "dev") -> None:
|
|
150
|
+
"""Initialize the event registry with environment prefix.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
environment: Deployment environment name used to substitute
|
|
154
|
+
{env} placeholder in topic templates. Defaults to "dev".
|
|
155
|
+
|
|
156
|
+
Example:
|
|
157
|
+
>>> registry = EventRegistry(environment="staging")
|
|
158
|
+
>>> registry.resolve_topic("prompt.submitted")
|
|
159
|
+
'staging.onex.evt.omniclaude.prompt-submitted.v1'
|
|
160
|
+
"""
|
|
161
|
+
self._environment = environment
|
|
162
|
+
self._registrations: dict[str, ModelEventRegistration] = {}
|
|
163
|
+
self._register_defaults()
|
|
164
|
+
|
|
165
|
+
def _register_defaults(self) -> None:
|
|
166
|
+
"""Register default OmniClaude event types.
|
|
167
|
+
|
|
168
|
+
Registers the standard event types emitted by OmniClaude hooks:
|
|
169
|
+
- prompt.submitted: User prompt submission events
|
|
170
|
+
- session.started: Session initialization events
|
|
171
|
+
- session.ended: Session termination events
|
|
172
|
+
- tool.executed: Tool execution events
|
|
173
|
+
"""
|
|
174
|
+
defaults = [
|
|
175
|
+
ModelEventRegistration(
|
|
176
|
+
event_type="prompt.submitted",
|
|
177
|
+
topic_template="{env}.onex.evt.omniclaude.prompt-submitted.v1",
|
|
178
|
+
partition_key_field="session_id",
|
|
179
|
+
required_fields=["prompt"],
|
|
180
|
+
),
|
|
181
|
+
ModelEventRegistration(
|
|
182
|
+
event_type="session.started",
|
|
183
|
+
topic_template="{env}.onex.evt.omniclaude.session-started.v1",
|
|
184
|
+
partition_key_field="session_id",
|
|
185
|
+
required_fields=["session_id"],
|
|
186
|
+
),
|
|
187
|
+
ModelEventRegistration(
|
|
188
|
+
event_type="session.ended",
|
|
189
|
+
topic_template="{env}.onex.evt.omniclaude.session-ended.v1",
|
|
190
|
+
partition_key_field="session_id",
|
|
191
|
+
required_fields=["session_id"],
|
|
192
|
+
),
|
|
193
|
+
ModelEventRegistration(
|
|
194
|
+
event_type="tool.executed",
|
|
195
|
+
topic_template="{env}.onex.evt.omniclaude.tool-executed.v1",
|
|
196
|
+
partition_key_field="session_id",
|
|
197
|
+
required_fields=["tool_name"],
|
|
198
|
+
),
|
|
199
|
+
]
|
|
200
|
+
for registration in defaults:
|
|
201
|
+
self._registrations[registration.event_type] = registration
|
|
202
|
+
|
|
203
|
+
def register(self, registration: ModelEventRegistration) -> None:
|
|
204
|
+
"""Register an event type mapping.
|
|
205
|
+
|
|
206
|
+
Adds or updates a registration for the given event type.
|
|
207
|
+
Existing registrations for the same event type are overwritten.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
registration: Event registration configuration.
|
|
211
|
+
|
|
212
|
+
Example:
|
|
213
|
+
>>> registry = EventRegistry()
|
|
214
|
+
>>> registry.register(
|
|
215
|
+
... ModelEventRegistration(
|
|
216
|
+
... event_type="custom.event",
|
|
217
|
+
... topic_template="{env}.onex.evt.custom.event.v1",
|
|
218
|
+
... )
|
|
219
|
+
... )
|
|
220
|
+
>>> registry.resolve_topic("custom.event")
|
|
221
|
+
'dev.onex.evt.custom.event.v1'
|
|
222
|
+
"""
|
|
223
|
+
self._registrations[registration.event_type] = registration
|
|
224
|
+
|
|
225
|
+
def resolve_topic(self, event_type: str) -> str:
|
|
226
|
+
"""Get the Kafka topic for an event type.
|
|
227
|
+
|
|
228
|
+
Resolves the topic template by substituting the {env} placeholder
|
|
229
|
+
with the configured environment name.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
event_type: Semantic event type identifier.
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
Fully resolved Kafka topic name.
|
|
236
|
+
|
|
237
|
+
Raises:
|
|
238
|
+
OnexError: If the event type is not registered.
|
|
239
|
+
|
|
240
|
+
Example:
|
|
241
|
+
>>> registry = EventRegistry(environment="prod")
|
|
242
|
+
>>> registry.resolve_topic("prompt.submitted")
|
|
243
|
+
'prod.onex.evt.omniclaude.prompt-submitted.v1'
|
|
244
|
+
"""
|
|
245
|
+
registration = self._registrations.get(event_type)
|
|
246
|
+
if registration is None:
|
|
247
|
+
registered = list(self._registrations.keys())
|
|
248
|
+
raise OnexError(
|
|
249
|
+
f"Unknown event type: '{event_type}'. Registered types: {registered}"
|
|
250
|
+
)
|
|
251
|
+
return registration.topic_template.format(env=self._environment)
|
|
252
|
+
|
|
253
|
+
def get_partition_key(
|
|
254
|
+
self,
|
|
255
|
+
event_type: str,
|
|
256
|
+
payload: dict[str, object],
|
|
257
|
+
) -> str | None:
|
|
258
|
+
"""Extract partition key from payload based on registration.
|
|
259
|
+
|
|
260
|
+
Uses the configured partition_key_field to extract the value
|
|
261
|
+
from the payload. Returns None if no partition key is configured
|
|
262
|
+
or the field is not present in the payload.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
event_type: Semantic event type identifier.
|
|
266
|
+
payload: Event payload dictionary.
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
Partition key value as string, or None if not applicable.
|
|
270
|
+
|
|
271
|
+
Raises:
|
|
272
|
+
OnexError: If the event type is not registered.
|
|
273
|
+
|
|
274
|
+
Example:
|
|
275
|
+
>>> registry = EventRegistry()
|
|
276
|
+
>>> key = registry.get_partition_key(
|
|
277
|
+
... "prompt.submitted",
|
|
278
|
+
... {"prompt": "Hello", "session_id": "sess-123"},
|
|
279
|
+
... )
|
|
280
|
+
>>> print(key)
|
|
281
|
+
'sess-123'
|
|
282
|
+
"""
|
|
283
|
+
registration = self._registrations.get(event_type)
|
|
284
|
+
if registration is None:
|
|
285
|
+
registered = list(self._registrations.keys())
|
|
286
|
+
raise OnexError(
|
|
287
|
+
f"Unknown event type: '{event_type}'. Registered types: {registered}"
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
if registration.partition_key_field is None:
|
|
291
|
+
return None
|
|
292
|
+
|
|
293
|
+
value = payload.get(registration.partition_key_field)
|
|
294
|
+
if value is None:
|
|
295
|
+
return None
|
|
296
|
+
|
|
297
|
+
return str(value)
|
|
298
|
+
|
|
299
|
+
def validate_payload(
|
|
300
|
+
self,
|
|
301
|
+
event_type: str,
|
|
302
|
+
payload: dict[str, object],
|
|
303
|
+
) -> bool:
|
|
304
|
+
"""Validate payload has all required fields.
|
|
305
|
+
|
|
306
|
+
Checks that all fields specified in the registration's required_fields
|
|
307
|
+
are present in the payload.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
event_type: Semantic event type identifier.
|
|
311
|
+
payload: Event payload dictionary to validate.
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
True if validation passes.
|
|
315
|
+
|
|
316
|
+
Raises:
|
|
317
|
+
OnexError: If the event type is not registered or if any
|
|
318
|
+
required field is missing from the payload.
|
|
319
|
+
|
|
320
|
+
Example:
|
|
321
|
+
>>> registry = EventRegistry()
|
|
322
|
+
>>> registry.validate_payload(
|
|
323
|
+
... "prompt.submitted",
|
|
324
|
+
... {"prompt": "Hello"},
|
|
325
|
+
... )
|
|
326
|
+
True
|
|
327
|
+
|
|
328
|
+
>>> registry.validate_payload(
|
|
329
|
+
... "prompt.submitted",
|
|
330
|
+
... {},
|
|
331
|
+
... )
|
|
332
|
+
Traceback (most recent call last):
|
|
333
|
+
...
|
|
334
|
+
OnexError: Missing required fields for 'prompt.submitted': ['prompt']
|
|
335
|
+
"""
|
|
336
|
+
registration = self._registrations.get(event_type)
|
|
337
|
+
if registration is None:
|
|
338
|
+
registered = list(self._registrations.keys())
|
|
339
|
+
raise OnexError(
|
|
340
|
+
f"Unknown event type: '{event_type}'. Registered types: {registered}"
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
missing_fields = [
|
|
344
|
+
field for field in registration.required_fields if field not in payload
|
|
345
|
+
]
|
|
346
|
+
|
|
347
|
+
if missing_fields:
|
|
348
|
+
raise OnexError(
|
|
349
|
+
f"Missing required fields for '{event_type}': {missing_fields}"
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
return True
|
|
353
|
+
|
|
354
|
+
def inject_metadata(
|
|
355
|
+
self,
|
|
356
|
+
event_type: str,
|
|
357
|
+
payload: dict[str, object],
|
|
358
|
+
correlation_id: str | None = None,
|
|
359
|
+
causation_id: str | None = None,
|
|
360
|
+
) -> dict[str, object]:
|
|
361
|
+
"""Inject correlation_id, causation_id, emitted_at, and schema_version.
|
|
362
|
+
|
|
363
|
+
Creates a new payload dictionary with metadata fields added.
|
|
364
|
+
The original payload is not modified.
|
|
365
|
+
|
|
366
|
+
Injected fields:
|
|
367
|
+
- correlation_id: Trace ID for the event chain (auto-generated if None)
|
|
368
|
+
- causation_id: ID of the event that caused this event (None if root event)
|
|
369
|
+
- emitted_at: ISO-8601 timestamp of when the event was emitted
|
|
370
|
+
- schema_version: Version of the event schema from registration
|
|
371
|
+
|
|
372
|
+
Args:
|
|
373
|
+
event_type: Semantic event type identifier.
|
|
374
|
+
payload: Event payload dictionary to enrich.
|
|
375
|
+
correlation_id: Optional correlation ID for tracing. If None,
|
|
376
|
+
a new UUID will be generated.
|
|
377
|
+
causation_id: Optional ID of the event that directly caused this event.
|
|
378
|
+
This parameter enables event chain tracing by linking derived events
|
|
379
|
+
back to their source. It should be populated when:
|
|
380
|
+
|
|
381
|
+
- An event handler processes event A and emits event B as a result
|
|
382
|
+
- A saga/workflow step emits a follow-up event
|
|
383
|
+
- Any event is produced as a direct consequence of another event
|
|
384
|
+
|
|
385
|
+
When None (the default), indicates this is a root event with no
|
|
386
|
+
direct cause in the event stream (e.g., user-initiated actions,
|
|
387
|
+
scheduled jobs, external triggers).
|
|
388
|
+
|
|
389
|
+
Note: This is an extension point for future event chain tracing
|
|
390
|
+
functionality. Current EmitDaemon usage passes None for all events
|
|
391
|
+
since hook events are root events initiated by user actions.
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
New dictionary with original payload plus injected metadata.
|
|
395
|
+
|
|
396
|
+
Raises:
|
|
397
|
+
OnexError: If the event type is not registered.
|
|
398
|
+
|
|
399
|
+
Example:
|
|
400
|
+
>>> registry = EventRegistry()
|
|
401
|
+
>>> # Root event (no causation_id)
|
|
402
|
+
>>> root_event = registry.inject_metadata(
|
|
403
|
+
... "prompt.submitted",
|
|
404
|
+
... {"prompt": "Hello"},
|
|
405
|
+
... correlation_id="corr-123",
|
|
406
|
+
... )
|
|
407
|
+
>>> root_event["causation_id"] is None
|
|
408
|
+
True
|
|
409
|
+
>>>
|
|
410
|
+
>>> # Derived event (with causation_id linking to root)
|
|
411
|
+
>>> derived_event = registry.inject_metadata(
|
|
412
|
+
... "tool.executed",
|
|
413
|
+
... {"tool_name": "search"},
|
|
414
|
+
... correlation_id="corr-123", # Same correlation for chain
|
|
415
|
+
... causation_id=root_event["correlation_id"], # Links to cause
|
|
416
|
+
... )
|
|
417
|
+
>>> derived_event["causation_id"]
|
|
418
|
+
'corr-123'
|
|
419
|
+
>>> "emitted_at" in derived_event
|
|
420
|
+
True
|
|
421
|
+
>>> derived_event["schema_version"]
|
|
422
|
+
'1.0.0'
|
|
423
|
+
"""
|
|
424
|
+
registration = self._registrations.get(event_type)
|
|
425
|
+
if registration is None:
|
|
426
|
+
registered = list(self._registrations.keys())
|
|
427
|
+
raise OnexError(
|
|
428
|
+
f"Unknown event type: '{event_type}'. Registered types: {registered}"
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
# Create new dict with original payload
|
|
432
|
+
enriched: dict[str, object] = dict(payload)
|
|
433
|
+
|
|
434
|
+
# Inject metadata
|
|
435
|
+
enriched["correlation_id"] = correlation_id or str(uuid4())
|
|
436
|
+
enriched["causation_id"] = causation_id
|
|
437
|
+
enriched["emitted_at"] = datetime.now(UTC).isoformat()
|
|
438
|
+
enriched["schema_version"] = registration.schema_version
|
|
439
|
+
|
|
440
|
+
return enriched
|
|
441
|
+
|
|
442
|
+
def get_registration(self, event_type: str) -> ModelEventRegistration | None:
|
|
443
|
+
"""Get the registration for an event type.
|
|
444
|
+
|
|
445
|
+
Args:
|
|
446
|
+
event_type: Semantic event type identifier.
|
|
447
|
+
|
|
448
|
+
Returns:
|
|
449
|
+
The registration configuration, or None if not registered.
|
|
450
|
+
|
|
451
|
+
Example:
|
|
452
|
+
>>> registry = EventRegistry()
|
|
453
|
+
>>> reg = registry.get_registration("prompt.submitted")
|
|
454
|
+
>>> reg.topic_template
|
|
455
|
+
'{env}.onex.evt.omniclaude.prompt-submitted.v1'
|
|
456
|
+
"""
|
|
457
|
+
return self._registrations.get(event_type)
|
|
458
|
+
|
|
459
|
+
def list_event_types(self) -> list[str]:
|
|
460
|
+
"""List all registered event types.
|
|
461
|
+
|
|
462
|
+
Returns:
|
|
463
|
+
List of registered event type identifiers.
|
|
464
|
+
|
|
465
|
+
Example:
|
|
466
|
+
>>> registry = EventRegistry()
|
|
467
|
+
>>> types = registry.list_event_types()
|
|
468
|
+
>>> "prompt.submitted" in types
|
|
469
|
+
True
|
|
470
|
+
"""
|
|
471
|
+
return list(self._registrations.keys())
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
__all__: list[str] = [
|
|
475
|
+
"EventRegistry",
|
|
476
|
+
"ModelEventRegistration",
|
|
477
|
+
]
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 OmniNode Team
|
|
3
|
+
"""Daemon request models for emit daemon protocol.
|
|
4
|
+
|
|
5
|
+
This module defines the strongly-typed request models for the emit daemon
|
|
6
|
+
Unix socket protocol. Using Pydantic models instead of dict[str, object]
|
|
7
|
+
provides compile-time type safety and eliminates runtime isinstance checks.
|
|
8
|
+
|
|
9
|
+
Request Types:
|
|
10
|
+
- ModelDaemonPingRequest: Health check / ping command
|
|
11
|
+
- ModelDaemonEmitRequest: Event emission request
|
|
12
|
+
|
|
13
|
+
Protocol:
|
|
14
|
+
Requests are JSON-encoded and sent as newline-delimited messages over
|
|
15
|
+
the Unix socket. The daemon discriminates between request types based
|
|
16
|
+
on the presence of the "command" field (commands) vs "event_type" field
|
|
17
|
+
(event emissions).
|
|
18
|
+
|
|
19
|
+
Related Tickets:
|
|
20
|
+
- OMN-1610: Hook Event Daemon MVP
|
|
21
|
+
|
|
22
|
+
.. versionadded:: 0.2.6
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
from typing import Annotated, Literal
|
|
28
|
+
|
|
29
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
30
|
+
|
|
31
|
+
from omnibase_core.types import JsonType
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ModelDaemonPingRequest(BaseModel):
|
|
35
|
+
"""Request model for daemon ping/health check command.
|
|
36
|
+
|
|
37
|
+
This command is used to verify the daemon is running and get
|
|
38
|
+
current queue status.
|
|
39
|
+
|
|
40
|
+
Example:
|
|
41
|
+
```python
|
|
42
|
+
request = ModelDaemonPingRequest()
|
|
43
|
+
# Serializes to: {"command": "ping"}
|
|
44
|
+
```
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
model_config = ConfigDict(
|
|
48
|
+
frozen=True,
|
|
49
|
+
extra="forbid",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
command: Literal["ping"] = Field(
|
|
53
|
+
default="ping",
|
|
54
|
+
description="Command identifier for ping request",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class ModelDaemonEmitRequest(BaseModel):
|
|
59
|
+
"""Request model for event emission.
|
|
60
|
+
|
|
61
|
+
Contains the event type and payload to be published to Kafka
|
|
62
|
+
via the daemon's persistent connection.
|
|
63
|
+
|
|
64
|
+
Attributes:
|
|
65
|
+
event_type: Semantic event type (e.g., "prompt.submitted").
|
|
66
|
+
Must be registered with the daemon's EventRegistry.
|
|
67
|
+
payload: Event payload. Must contain required fields for the
|
|
68
|
+
event type as defined in EventRegistry.
|
|
69
|
+
|
|
70
|
+
Example:
|
|
71
|
+
```python
|
|
72
|
+
request = ModelDaemonEmitRequest(
|
|
73
|
+
event_type="prompt.submitted",
|
|
74
|
+
payload={"prompt_id": "abc123", "session_id": "sess-456"},
|
|
75
|
+
)
|
|
76
|
+
```
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
model_config = ConfigDict(
|
|
80
|
+
frozen=True,
|
|
81
|
+
extra="forbid",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
event_type: str = Field(
|
|
85
|
+
...,
|
|
86
|
+
description="Semantic event type (e.g., 'prompt.submitted')",
|
|
87
|
+
min_length=1,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
payload: JsonType = Field(
|
|
91
|
+
default_factory=dict,
|
|
92
|
+
description="Event payload data",
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# Type alias for discriminated union of all request types
|
|
97
|
+
# Use field presence for discrimination:
|
|
98
|
+
# - "command" field present -> ModelDaemonPingRequest
|
|
99
|
+
# - "event_type" field present -> ModelDaemonEmitRequest
|
|
100
|
+
ModelDaemonRequest = Annotated[
|
|
101
|
+
ModelDaemonPingRequest | ModelDaemonEmitRequest,
|
|
102
|
+
Field(description="Union of all daemon request types"),
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def parse_daemon_request(
|
|
107
|
+
data: dict[str, object],
|
|
108
|
+
) -> ModelDaemonPingRequest | ModelDaemonEmitRequest:
|
|
109
|
+
"""Parse raw dict into typed request model.
|
|
110
|
+
|
|
111
|
+
Discriminates between request types based on field presence:
|
|
112
|
+
- "command" field present -> ModelDaemonPingRequest
|
|
113
|
+
- "event_type" field present -> ModelDaemonEmitRequest
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
data: Raw request dict from JSON parsing
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Typed request model
|
|
120
|
+
|
|
121
|
+
Raises:
|
|
122
|
+
ValueError: If request format is invalid
|
|
123
|
+
"""
|
|
124
|
+
if "command" in data:
|
|
125
|
+
return ModelDaemonPingRequest.model_validate(data)
|
|
126
|
+
elif "event_type" in data:
|
|
127
|
+
return ModelDaemonEmitRequest.model_validate(data)
|
|
128
|
+
else:
|
|
129
|
+
raise ValueError(
|
|
130
|
+
"Invalid request: must contain either 'command' or 'event_type' field"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
__all__: list[str] = [
|
|
135
|
+
"ModelDaemonEmitRequest",
|
|
136
|
+
"ModelDaemonPingRequest",
|
|
137
|
+
"ModelDaemonRequest",
|
|
138
|
+
"parse_daemon_request",
|
|
139
|
+
]
|