omnibase_infra 0.2.7__py3-none-any.whl → 0.2.9__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 +4 -0
- omnibase_infra/enums/enum_declarative_node_violation.py +102 -0
- omnibase_infra/event_bus/adapters/__init__.py +31 -0
- omnibase_infra/event_bus/adapters/adapter_protocol_event_publisher_kafka.py +517 -0
- omnibase_infra/mixins/mixin_async_circuit_breaker.py +113 -1
- omnibase_infra/models/__init__.py +9 -0
- omnibase_infra/models/event_bus/__init__.py +22 -0
- omnibase_infra/models/event_bus/model_consumer_retry_config.py +367 -0
- omnibase_infra/models/event_bus/model_dlq_config.py +177 -0
- omnibase_infra/models/event_bus/model_idempotency_config.py +131 -0
- omnibase_infra/models/event_bus/model_offset_policy_config.py +107 -0
- omnibase_infra/models/resilience/model_circuit_breaker_config.py +15 -0
- omnibase_infra/models/validation/__init__.py +8 -0
- omnibase_infra/models/validation/model_declarative_node_validation_result.py +139 -0
- omnibase_infra/models/validation/model_declarative_node_violation.py +169 -0
- omnibase_infra/nodes/architecture_validator/__init__.py +28 -7
- omnibase_infra/nodes/architecture_validator/constants.py +36 -0
- omnibase_infra/nodes/architecture_validator/handlers/__init__.py +28 -0
- omnibase_infra/nodes/architecture_validator/handlers/contract.yaml +120 -0
- omnibase_infra/nodes/architecture_validator/handlers/handler_architecture_validation.py +359 -0
- omnibase_infra/nodes/architecture_validator/node.py +1 -0
- omnibase_infra/nodes/architecture_validator/node_architecture_validator.py +48 -336
- omnibase_infra/nodes/node_ledger_projection_compute/__init__.py +16 -2
- omnibase_infra/nodes/node_ledger_projection_compute/contract.yaml +14 -4
- omnibase_infra/nodes/node_ledger_projection_compute/handlers/__init__.py +18 -0
- omnibase_infra/nodes/node_ledger_projection_compute/handlers/contract.yaml +53 -0
- omnibase_infra/nodes/node_ledger_projection_compute/handlers/handler_ledger_projection.py +354 -0
- omnibase_infra/nodes/node_ledger_projection_compute/node.py +20 -256
- omnibase_infra/nodes/node_registry_effect/node.py +20 -73
- omnibase_infra/protocols/protocol_dispatch_engine.py +90 -0
- omnibase_infra/runtime/__init__.py +11 -0
- omnibase_infra/runtime/baseline_subscriptions.py +150 -0
- omnibase_infra/runtime/event_bus_subcontract_wiring.py +455 -24
- omnibase_infra/runtime/kafka_contract_source.py +13 -5
- omnibase_infra/runtime/service_message_dispatch_engine.py +112 -0
- omnibase_infra/runtime/service_runtime_host_process.py +6 -11
- omnibase_infra/services/__init__.py +36 -0
- omnibase_infra/services/contract_publisher/__init__.py +95 -0
- omnibase_infra/services/contract_publisher/config.py +199 -0
- omnibase_infra/services/contract_publisher/errors.py +243 -0
- omnibase_infra/services/contract_publisher/models/__init__.py +28 -0
- omnibase_infra/services/contract_publisher/models/model_contract_error.py +67 -0
- omnibase_infra/services/contract_publisher/models/model_infra_error.py +62 -0
- omnibase_infra/services/contract_publisher/models/model_publish_result.py +112 -0
- omnibase_infra/services/contract_publisher/models/model_publish_stats.py +79 -0
- omnibase_infra/services/contract_publisher/service.py +617 -0
- omnibase_infra/services/contract_publisher/sources/__init__.py +52 -0
- omnibase_infra/services/contract_publisher/sources/model_discovered.py +155 -0
- omnibase_infra/services/contract_publisher/sources/protocol.py +101 -0
- omnibase_infra/services/contract_publisher/sources/source_composite.py +309 -0
- omnibase_infra/services/contract_publisher/sources/source_filesystem.py +174 -0
- omnibase_infra/services/contract_publisher/sources/source_package.py +221 -0
- omnibase_infra/services/observability/__init__.py +40 -0
- omnibase_infra/services/observability/agent_actions/__init__.py +64 -0
- omnibase_infra/services/observability/agent_actions/config.py +209 -0
- omnibase_infra/services/observability/agent_actions/consumer.py +1320 -0
- omnibase_infra/services/observability/agent_actions/models/__init__.py +87 -0
- omnibase_infra/services/observability/agent_actions/models/model_agent_action.py +142 -0
- omnibase_infra/services/observability/agent_actions/models/model_detection_failure.py +125 -0
- omnibase_infra/services/observability/agent_actions/models/model_envelope.py +85 -0
- omnibase_infra/services/observability/agent_actions/models/model_execution_log.py +159 -0
- omnibase_infra/services/observability/agent_actions/models/model_performance_metric.py +130 -0
- omnibase_infra/services/observability/agent_actions/models/model_routing_decision.py +138 -0
- omnibase_infra/services/observability/agent_actions/models/model_transformation_event.py +124 -0
- omnibase_infra/services/observability/agent_actions/tests/__init__.py +20 -0
- omnibase_infra/services/observability/agent_actions/tests/test_consumer.py +1154 -0
- omnibase_infra/services/observability/agent_actions/tests/test_models.py +645 -0
- omnibase_infra/services/observability/agent_actions/tests/test_writer.py +709 -0
- omnibase_infra/services/observability/agent_actions/writer_postgres.py +926 -0
- omnibase_infra/validation/__init__.py +12 -0
- omnibase_infra/validation/contracts/declarative_node.validation.yaml +143 -0
- omnibase_infra/validation/validation_exemptions.yaml +93 -0
- omnibase_infra/validation/validator_declarative_node.py +850 -0
- {omnibase_infra-0.2.7.dist-info → omnibase_infra-0.2.9.dist-info}/METADATA +3 -3
- {omnibase_infra-0.2.7.dist-info → omnibase_infra-0.2.9.dist-info}/RECORD +79 -27
- {omnibase_infra-0.2.7.dist-info → omnibase_infra-0.2.9.dist-info}/WHEEL +0 -0
- {omnibase_infra-0.2.7.dist-info → omnibase_infra-0.2.9.dist-info}/entry_points.txt +0 -0
- {omnibase_infra-0.2.7.dist-info → omnibase_infra-0.2.9.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,926 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 OmniNode Team
|
|
3
|
+
"""PostgreSQL Writer for Agent Actions Observability.
|
|
4
|
+
|
|
5
|
+
This module provides a PostgreSQL writer for persisting agent observability
|
|
6
|
+
events consumed from Kafka. It handles batch inserts with idempotency
|
|
7
|
+
guarantees and circuit breaker resilience.
|
|
8
|
+
|
|
9
|
+
Design Decisions:
|
|
10
|
+
- Pool injection: asyncpg.Pool is injected, not created/managed
|
|
11
|
+
- Batch inserts: Uses executemany for efficient batch processing
|
|
12
|
+
- Idempotency: ON CONFLICT DO NOTHING/UPDATE per table contract
|
|
13
|
+
- Circuit breaker: MixinAsyncCircuitBreaker for resilience
|
|
14
|
+
- JSONB serialization: dict fields serialized to JSON strings
|
|
15
|
+
|
|
16
|
+
Idempotency Contract:
|
|
17
|
+
| Table | Unique Key | Conflict Action |
|
|
18
|
+
|-------------------------------|------------------|-----------------|
|
|
19
|
+
| agent_actions | id | DO NOTHING |
|
|
20
|
+
| agent_routing_decisions | id | DO NOTHING |
|
|
21
|
+
| agent_transformation_events | id | DO NOTHING |
|
|
22
|
+
| router_performance_metrics | id | DO NOTHING |
|
|
23
|
+
| agent_detection_failures | correlation_id | DO NOTHING |
|
|
24
|
+
| agent_execution_logs | execution_id | DO UPDATE |
|
|
25
|
+
|
|
26
|
+
Example:
|
|
27
|
+
>>> import asyncpg
|
|
28
|
+
>>> from omnibase_infra.services.observability.agent_actions.writer_postgres import (
|
|
29
|
+
... WriterAgentActionsPostgres,
|
|
30
|
+
... )
|
|
31
|
+
>>>
|
|
32
|
+
>>> pool = await asyncpg.create_pool(dsn="postgresql://...")
|
|
33
|
+
>>> writer = WriterAgentActionsPostgres(pool)
|
|
34
|
+
>>>
|
|
35
|
+
>>> # Write batch of agent actions
|
|
36
|
+
>>> count = await writer.write_agent_actions(actions)
|
|
37
|
+
>>> print(f"Wrote {count} agent actions")
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
from __future__ import annotations
|
|
41
|
+
|
|
42
|
+
import json
|
|
43
|
+
import logging
|
|
44
|
+
from collections.abc import Mapping
|
|
45
|
+
from uuid import UUID, uuid4
|
|
46
|
+
|
|
47
|
+
import asyncpg
|
|
48
|
+
|
|
49
|
+
from omnibase_core.types import JsonType
|
|
50
|
+
from omnibase_infra.enums import EnumInfraTransportType
|
|
51
|
+
from omnibase_infra.errors import (
|
|
52
|
+
InfraConnectionError,
|
|
53
|
+
InfraTimeoutError,
|
|
54
|
+
ModelInfraErrorContext,
|
|
55
|
+
ModelTimeoutErrorContext,
|
|
56
|
+
RuntimeHostError,
|
|
57
|
+
)
|
|
58
|
+
from omnibase_infra.mixins import MixinAsyncCircuitBreaker
|
|
59
|
+
from omnibase_infra.services.observability.agent_actions.models import (
|
|
60
|
+
ModelAgentAction,
|
|
61
|
+
ModelDetectionFailure,
|
|
62
|
+
ModelExecutionLog,
|
|
63
|
+
ModelPerformanceMetric,
|
|
64
|
+
ModelRoutingDecision,
|
|
65
|
+
ModelTransformationEvent,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
logger = logging.getLogger(__name__)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class WriterAgentActionsPostgres(MixinAsyncCircuitBreaker):
|
|
72
|
+
"""PostgreSQL writer for agent observability events.
|
|
73
|
+
|
|
74
|
+
Provides batch write methods for each observability table with idempotency
|
|
75
|
+
guarantees and circuit breaker resilience. The asyncpg.Pool is injected
|
|
76
|
+
and its lifecycle is managed externally.
|
|
77
|
+
|
|
78
|
+
Features:
|
|
79
|
+
- Batch inserts via executemany for efficiency
|
|
80
|
+
- Idempotent writes via ON CONFLICT clauses
|
|
81
|
+
- Circuit breaker for database resilience
|
|
82
|
+
- JSONB field serialization
|
|
83
|
+
- Correlation ID propagation for tracing
|
|
84
|
+
|
|
85
|
+
Attributes:
|
|
86
|
+
_pool: Injected asyncpg connection pool.
|
|
87
|
+
circuit_breaker_threshold: Failure threshold before opening circuit.
|
|
88
|
+
circuit_breaker_reset_timeout: Seconds before auto-reset.
|
|
89
|
+
DEFAULT_QUERY_TIMEOUT_SECONDS: Default timeout for database queries.
|
|
90
|
+
|
|
91
|
+
Example:
|
|
92
|
+
>>> pool = await asyncpg.create_pool(dsn="postgresql://...")
|
|
93
|
+
>>> writer = WriterAgentActionsPostgres(
|
|
94
|
+
... pool,
|
|
95
|
+
... circuit_breaker_threshold=5,
|
|
96
|
+
... circuit_breaker_reset_timeout=60.0,
|
|
97
|
+
... circuit_breaker_half_open_successes=2,
|
|
98
|
+
... query_timeout=30.0,
|
|
99
|
+
... )
|
|
100
|
+
>>>
|
|
101
|
+
>>> # Write batch of routing decisions
|
|
102
|
+
>>> count = await writer.write_routing_decisions(decisions)
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
DEFAULT_QUERY_TIMEOUT_SECONDS: float = 30.0
|
|
106
|
+
|
|
107
|
+
def __init__(
|
|
108
|
+
self,
|
|
109
|
+
pool: asyncpg.Pool,
|
|
110
|
+
circuit_breaker_threshold: int = 5,
|
|
111
|
+
circuit_breaker_reset_timeout: float = 60.0,
|
|
112
|
+
circuit_breaker_half_open_successes: int = 1,
|
|
113
|
+
query_timeout: float | None = None,
|
|
114
|
+
) -> None:
|
|
115
|
+
"""Initialize the PostgreSQL writer with an injected pool.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
pool: asyncpg connection pool (lifecycle managed externally).
|
|
119
|
+
circuit_breaker_threshold: Failures before opening circuit (default: 5).
|
|
120
|
+
circuit_breaker_reset_timeout: Seconds before auto-reset (default: 60.0).
|
|
121
|
+
circuit_breaker_half_open_successes: Successful requests required to close
|
|
122
|
+
circuit from half-open state (default: 1).
|
|
123
|
+
query_timeout: Timeout in seconds for database queries. Used in error
|
|
124
|
+
context for timeout diagnostics (default: DEFAULT_QUERY_TIMEOUT_SECONDS).
|
|
125
|
+
|
|
126
|
+
Raises:
|
|
127
|
+
ProtocolConfigurationError: If circuit breaker parameters are invalid.
|
|
128
|
+
"""
|
|
129
|
+
self._pool = pool
|
|
130
|
+
self._query_timeout = query_timeout or self.DEFAULT_QUERY_TIMEOUT_SECONDS
|
|
131
|
+
|
|
132
|
+
# Initialize circuit breaker mixin
|
|
133
|
+
self._init_circuit_breaker(
|
|
134
|
+
threshold=circuit_breaker_threshold,
|
|
135
|
+
reset_timeout=circuit_breaker_reset_timeout,
|
|
136
|
+
service_name="agent-actions-postgres-writer",
|
|
137
|
+
transport_type=EnumInfraTransportType.DATABASE,
|
|
138
|
+
half_open_successes=circuit_breaker_half_open_successes,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
logger.info(
|
|
142
|
+
"WriterAgentActionsPostgres initialized",
|
|
143
|
+
extra={
|
|
144
|
+
"circuit_breaker_threshold": circuit_breaker_threshold,
|
|
145
|
+
"circuit_breaker_reset_timeout": circuit_breaker_reset_timeout,
|
|
146
|
+
"circuit_breaker_half_open_successes": circuit_breaker_half_open_successes,
|
|
147
|
+
"query_timeout": self._query_timeout,
|
|
148
|
+
},
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
@staticmethod
|
|
152
|
+
def _serialize_json(value: Mapping[str, object] | None) -> str | None:
|
|
153
|
+
"""Serialize a mapping to JSON string for JSONB columns.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
value: Mapping to serialize, or None.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
JSON string if value is not None, otherwise None.
|
|
160
|
+
"""
|
|
161
|
+
if value is None:
|
|
162
|
+
return None
|
|
163
|
+
return json.dumps(dict(value))
|
|
164
|
+
|
|
165
|
+
@staticmethod
|
|
166
|
+
def _serialize_list(value: list[str] | None) -> str | None:
|
|
167
|
+
"""Serialize a list to JSON string for JSONB/array columns.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
value: List to serialize, or None.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
JSON string if value is not None, otherwise None.
|
|
174
|
+
"""
|
|
175
|
+
if value is None:
|
|
176
|
+
return None
|
|
177
|
+
return json.dumps(value)
|
|
178
|
+
|
|
179
|
+
async def write_agent_actions(
|
|
180
|
+
self,
|
|
181
|
+
events: list[ModelAgentAction],
|
|
182
|
+
correlation_id: UUID | None = None,
|
|
183
|
+
) -> int:
|
|
184
|
+
"""Write batch of agent action events to PostgreSQL.
|
|
185
|
+
|
|
186
|
+
Uses INSERT ... ON CONFLICT (id) DO NOTHING for idempotency.
|
|
187
|
+
Append-only audit log - duplicates are silently ignored.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
events: List of agent action events to write.
|
|
191
|
+
correlation_id: Optional correlation ID for tracing.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
Count of events in the batch (executemany doesn't return affected rows).
|
|
195
|
+
|
|
196
|
+
Raises:
|
|
197
|
+
InfraConnectionError: If database connection fails.
|
|
198
|
+
InfraTimeoutError: If operation times out.
|
|
199
|
+
InfraUnavailableError: If circuit breaker is open.
|
|
200
|
+
"""
|
|
201
|
+
if not events:
|
|
202
|
+
return 0
|
|
203
|
+
|
|
204
|
+
op_correlation_id = correlation_id or uuid4()
|
|
205
|
+
|
|
206
|
+
# Check circuit breaker
|
|
207
|
+
async with self._circuit_breaker_lock:
|
|
208
|
+
await self._check_circuit_breaker(
|
|
209
|
+
operation="write_agent_actions",
|
|
210
|
+
correlation_id=op_correlation_id,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
context = ModelInfraErrorContext(
|
|
214
|
+
transport_type=EnumInfraTransportType.DATABASE,
|
|
215
|
+
operation="write_agent_actions",
|
|
216
|
+
target_name="agent_actions",
|
|
217
|
+
correlation_id=op_correlation_id,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
sql = """
|
|
221
|
+
INSERT INTO agent_actions (
|
|
222
|
+
id, correlation_id, agent_name, action_type, action_name,
|
|
223
|
+
created_at, status, duration_ms, result, error_message, metadata
|
|
224
|
+
)
|
|
225
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
|
226
|
+
ON CONFLICT (id) DO NOTHING
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
try:
|
|
230
|
+
async with self._pool.acquire() as conn:
|
|
231
|
+
await conn.executemany(
|
|
232
|
+
sql,
|
|
233
|
+
[
|
|
234
|
+
(
|
|
235
|
+
e.id,
|
|
236
|
+
e.correlation_id,
|
|
237
|
+
e.agent_name,
|
|
238
|
+
e.action_type,
|
|
239
|
+
e.action_name,
|
|
240
|
+
e.created_at,
|
|
241
|
+
e.status,
|
|
242
|
+
e.duration_ms,
|
|
243
|
+
e.result,
|
|
244
|
+
e.error_message,
|
|
245
|
+
self._serialize_json(e.metadata),
|
|
246
|
+
)
|
|
247
|
+
for e in events
|
|
248
|
+
],
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
# Record success
|
|
252
|
+
async with self._circuit_breaker_lock:
|
|
253
|
+
await self._reset_circuit_breaker()
|
|
254
|
+
|
|
255
|
+
logger.debug(
|
|
256
|
+
"Wrote agent actions batch",
|
|
257
|
+
extra={
|
|
258
|
+
"count": len(events),
|
|
259
|
+
"correlation_id": str(op_correlation_id),
|
|
260
|
+
},
|
|
261
|
+
)
|
|
262
|
+
return len(events)
|
|
263
|
+
|
|
264
|
+
except asyncpg.QueryCanceledError as e:
|
|
265
|
+
async with self._circuit_breaker_lock:
|
|
266
|
+
await self._record_circuit_failure(
|
|
267
|
+
operation="write_agent_actions",
|
|
268
|
+
correlation_id=op_correlation_id,
|
|
269
|
+
)
|
|
270
|
+
raise InfraTimeoutError(
|
|
271
|
+
"Write agent actions timed out",
|
|
272
|
+
context=ModelTimeoutErrorContext(
|
|
273
|
+
transport_type=context.transport_type,
|
|
274
|
+
operation=context.operation,
|
|
275
|
+
target_name=context.target_name,
|
|
276
|
+
correlation_id=context.correlation_id,
|
|
277
|
+
timeout_seconds=self._query_timeout,
|
|
278
|
+
),
|
|
279
|
+
) from e
|
|
280
|
+
except asyncpg.PostgresConnectionError as e:
|
|
281
|
+
async with self._circuit_breaker_lock:
|
|
282
|
+
await self._record_circuit_failure(
|
|
283
|
+
operation="write_agent_actions",
|
|
284
|
+
correlation_id=op_correlation_id,
|
|
285
|
+
)
|
|
286
|
+
raise InfraConnectionError(
|
|
287
|
+
"Database connection failed during write_agent_actions",
|
|
288
|
+
context=context,
|
|
289
|
+
) from e
|
|
290
|
+
except asyncpg.PostgresError as e:
|
|
291
|
+
async with self._circuit_breaker_lock:
|
|
292
|
+
await self._record_circuit_failure(
|
|
293
|
+
operation="write_agent_actions",
|
|
294
|
+
correlation_id=op_correlation_id,
|
|
295
|
+
)
|
|
296
|
+
raise RuntimeHostError(
|
|
297
|
+
f"Database error during write_agent_actions: {type(e).__name__}",
|
|
298
|
+
context=context,
|
|
299
|
+
) from e
|
|
300
|
+
|
|
301
|
+
async def write_routing_decisions(
|
|
302
|
+
self,
|
|
303
|
+
events: list[ModelRoutingDecision],
|
|
304
|
+
correlation_id: UUID | None = None,
|
|
305
|
+
) -> int:
|
|
306
|
+
"""Write batch of routing decision events to PostgreSQL.
|
|
307
|
+
|
|
308
|
+
Uses INSERT ... ON CONFLICT (id) DO NOTHING for idempotency.
|
|
309
|
+
Append-only audit log - duplicates are silently ignored.
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
events: List of routing decision events to write.
|
|
313
|
+
correlation_id: Optional correlation ID for tracing.
|
|
314
|
+
|
|
315
|
+
Returns:
|
|
316
|
+
Count of events in the batch.
|
|
317
|
+
|
|
318
|
+
Raises:
|
|
319
|
+
InfraConnectionError: If database connection fails.
|
|
320
|
+
InfraTimeoutError: If operation times out.
|
|
321
|
+
InfraUnavailableError: If circuit breaker is open.
|
|
322
|
+
"""
|
|
323
|
+
if not events:
|
|
324
|
+
return 0
|
|
325
|
+
|
|
326
|
+
op_correlation_id = correlation_id or uuid4()
|
|
327
|
+
|
|
328
|
+
# Check circuit breaker
|
|
329
|
+
async with self._circuit_breaker_lock:
|
|
330
|
+
await self._check_circuit_breaker(
|
|
331
|
+
operation="write_routing_decisions",
|
|
332
|
+
correlation_id=op_correlation_id,
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
context = ModelInfraErrorContext(
|
|
336
|
+
transport_type=EnumInfraTransportType.DATABASE,
|
|
337
|
+
operation="write_routing_decisions",
|
|
338
|
+
target_name="agent_routing_decisions",
|
|
339
|
+
correlation_id=op_correlation_id,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
sql = """
|
|
343
|
+
INSERT INTO agent_routing_decisions (
|
|
344
|
+
id, correlation_id, selected_agent, confidence_score, created_at,
|
|
345
|
+
request_type, alternatives, routing_reason, domain, metadata
|
|
346
|
+
)
|
|
347
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
|
348
|
+
ON CONFLICT (id) DO NOTHING
|
|
349
|
+
"""
|
|
350
|
+
|
|
351
|
+
try:
|
|
352
|
+
async with self._pool.acquire() as conn:
|
|
353
|
+
await conn.executemany(
|
|
354
|
+
sql,
|
|
355
|
+
[
|
|
356
|
+
(
|
|
357
|
+
e.id,
|
|
358
|
+
e.correlation_id,
|
|
359
|
+
e.selected_agent,
|
|
360
|
+
e.confidence_score,
|
|
361
|
+
e.created_at,
|
|
362
|
+
e.request_type,
|
|
363
|
+
self._serialize_list(e.alternatives),
|
|
364
|
+
e.routing_reason,
|
|
365
|
+
e.domain,
|
|
366
|
+
self._serialize_json(e.metadata),
|
|
367
|
+
)
|
|
368
|
+
for e in events
|
|
369
|
+
],
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
# Record success
|
|
373
|
+
async with self._circuit_breaker_lock:
|
|
374
|
+
await self._reset_circuit_breaker()
|
|
375
|
+
|
|
376
|
+
logger.debug(
|
|
377
|
+
"Wrote routing decisions batch",
|
|
378
|
+
extra={
|
|
379
|
+
"count": len(events),
|
|
380
|
+
"correlation_id": str(op_correlation_id),
|
|
381
|
+
},
|
|
382
|
+
)
|
|
383
|
+
return len(events)
|
|
384
|
+
|
|
385
|
+
except asyncpg.QueryCanceledError as e:
|
|
386
|
+
async with self._circuit_breaker_lock:
|
|
387
|
+
await self._record_circuit_failure(
|
|
388
|
+
operation="write_routing_decisions",
|
|
389
|
+
correlation_id=op_correlation_id,
|
|
390
|
+
)
|
|
391
|
+
raise InfraTimeoutError(
|
|
392
|
+
"Write routing decisions timed out",
|
|
393
|
+
context=ModelTimeoutErrorContext(
|
|
394
|
+
transport_type=context.transport_type,
|
|
395
|
+
operation=context.operation,
|
|
396
|
+
target_name=context.target_name,
|
|
397
|
+
correlation_id=context.correlation_id,
|
|
398
|
+
timeout_seconds=self._query_timeout,
|
|
399
|
+
),
|
|
400
|
+
) from e
|
|
401
|
+
except asyncpg.PostgresConnectionError as e:
|
|
402
|
+
async with self._circuit_breaker_lock:
|
|
403
|
+
await self._record_circuit_failure(
|
|
404
|
+
operation="write_routing_decisions",
|
|
405
|
+
correlation_id=op_correlation_id,
|
|
406
|
+
)
|
|
407
|
+
raise InfraConnectionError(
|
|
408
|
+
"Database connection failed during write_routing_decisions",
|
|
409
|
+
context=context,
|
|
410
|
+
) from e
|
|
411
|
+
except asyncpg.PostgresError as e:
|
|
412
|
+
async with self._circuit_breaker_lock:
|
|
413
|
+
await self._record_circuit_failure(
|
|
414
|
+
operation="write_routing_decisions",
|
|
415
|
+
correlation_id=op_correlation_id,
|
|
416
|
+
)
|
|
417
|
+
raise RuntimeHostError(
|
|
418
|
+
f"Database error during write_routing_decisions: {type(e).__name__}",
|
|
419
|
+
context=context,
|
|
420
|
+
) from e
|
|
421
|
+
|
|
422
|
+
async def write_transformation_events(
|
|
423
|
+
self,
|
|
424
|
+
events: list[ModelTransformationEvent],
|
|
425
|
+
correlation_id: UUID | None = None,
|
|
426
|
+
) -> int:
|
|
427
|
+
"""Write batch of transformation events to PostgreSQL.
|
|
428
|
+
|
|
429
|
+
Uses INSERT ... ON CONFLICT (id) DO NOTHING for idempotency.
|
|
430
|
+
Append-only audit log - duplicates are silently ignored.
|
|
431
|
+
|
|
432
|
+
Args:
|
|
433
|
+
events: List of transformation events to write.
|
|
434
|
+
correlation_id: Optional correlation ID for tracing.
|
|
435
|
+
|
|
436
|
+
Returns:
|
|
437
|
+
Count of events in the batch.
|
|
438
|
+
|
|
439
|
+
Raises:
|
|
440
|
+
InfraConnectionError: If database connection fails.
|
|
441
|
+
InfraTimeoutError: If operation times out.
|
|
442
|
+
InfraUnavailableError: If circuit breaker is open.
|
|
443
|
+
"""
|
|
444
|
+
if not events:
|
|
445
|
+
return 0
|
|
446
|
+
|
|
447
|
+
op_correlation_id = correlation_id or uuid4()
|
|
448
|
+
|
|
449
|
+
# Check circuit breaker
|
|
450
|
+
async with self._circuit_breaker_lock:
|
|
451
|
+
await self._check_circuit_breaker(
|
|
452
|
+
operation="write_transformation_events",
|
|
453
|
+
correlation_id=op_correlation_id,
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
context = ModelInfraErrorContext(
|
|
457
|
+
transport_type=EnumInfraTransportType.DATABASE,
|
|
458
|
+
operation="write_transformation_events",
|
|
459
|
+
target_name="agent_transformation_events",
|
|
460
|
+
correlation_id=op_correlation_id,
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
sql = """
|
|
464
|
+
INSERT INTO agent_transformation_events (
|
|
465
|
+
id, correlation_id, source_agent, target_agent, created_at,
|
|
466
|
+
trigger, context, metadata
|
|
467
|
+
)
|
|
468
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
469
|
+
ON CONFLICT (id) DO NOTHING
|
|
470
|
+
"""
|
|
471
|
+
|
|
472
|
+
try:
|
|
473
|
+
async with self._pool.acquire() as conn:
|
|
474
|
+
await conn.executemany(
|
|
475
|
+
sql,
|
|
476
|
+
[
|
|
477
|
+
(
|
|
478
|
+
e.id,
|
|
479
|
+
e.correlation_id,
|
|
480
|
+
e.source_agent,
|
|
481
|
+
e.target_agent,
|
|
482
|
+
e.created_at,
|
|
483
|
+
e.trigger,
|
|
484
|
+
e.context,
|
|
485
|
+
self._serialize_json(e.metadata),
|
|
486
|
+
)
|
|
487
|
+
for e in events
|
|
488
|
+
],
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
# Record success
|
|
492
|
+
async with self._circuit_breaker_lock:
|
|
493
|
+
await self._reset_circuit_breaker()
|
|
494
|
+
|
|
495
|
+
logger.debug(
|
|
496
|
+
"Wrote transformation events batch",
|
|
497
|
+
extra={
|
|
498
|
+
"count": len(events),
|
|
499
|
+
"correlation_id": str(op_correlation_id),
|
|
500
|
+
},
|
|
501
|
+
)
|
|
502
|
+
return len(events)
|
|
503
|
+
|
|
504
|
+
except asyncpg.QueryCanceledError as e:
|
|
505
|
+
async with self._circuit_breaker_lock:
|
|
506
|
+
await self._record_circuit_failure(
|
|
507
|
+
operation="write_transformation_events",
|
|
508
|
+
correlation_id=op_correlation_id,
|
|
509
|
+
)
|
|
510
|
+
raise InfraTimeoutError(
|
|
511
|
+
"Write transformation events timed out",
|
|
512
|
+
context=ModelTimeoutErrorContext(
|
|
513
|
+
transport_type=context.transport_type,
|
|
514
|
+
operation=context.operation,
|
|
515
|
+
target_name=context.target_name,
|
|
516
|
+
correlation_id=context.correlation_id,
|
|
517
|
+
timeout_seconds=self._query_timeout,
|
|
518
|
+
),
|
|
519
|
+
) from e
|
|
520
|
+
except asyncpg.PostgresConnectionError as e:
|
|
521
|
+
async with self._circuit_breaker_lock:
|
|
522
|
+
await self._record_circuit_failure(
|
|
523
|
+
operation="write_transformation_events",
|
|
524
|
+
correlation_id=op_correlation_id,
|
|
525
|
+
)
|
|
526
|
+
raise InfraConnectionError(
|
|
527
|
+
"Database connection failed during write_transformation_events",
|
|
528
|
+
context=context,
|
|
529
|
+
) from e
|
|
530
|
+
except asyncpg.PostgresError as e:
|
|
531
|
+
async with self._circuit_breaker_lock:
|
|
532
|
+
await self._record_circuit_failure(
|
|
533
|
+
operation="write_transformation_events",
|
|
534
|
+
correlation_id=op_correlation_id,
|
|
535
|
+
)
|
|
536
|
+
raise RuntimeHostError(
|
|
537
|
+
f"Database error during write_transformation_events: {type(e).__name__}",
|
|
538
|
+
context=context,
|
|
539
|
+
) from e
|
|
540
|
+
|
|
541
|
+
async def write_performance_metrics(
|
|
542
|
+
self,
|
|
543
|
+
events: list[ModelPerformanceMetric],
|
|
544
|
+
correlation_id: UUID | None = None,
|
|
545
|
+
) -> int:
|
|
546
|
+
"""Write batch of performance metrics to PostgreSQL.
|
|
547
|
+
|
|
548
|
+
Uses INSERT ... ON CONFLICT (id) DO NOTHING for idempotency.
|
|
549
|
+
Append-only time-series - duplicates are silently ignored.
|
|
550
|
+
|
|
551
|
+
Args:
|
|
552
|
+
events: List of performance metric events to write.
|
|
553
|
+
correlation_id: Optional correlation ID for tracing.
|
|
554
|
+
|
|
555
|
+
Returns:
|
|
556
|
+
Count of events in the batch.
|
|
557
|
+
|
|
558
|
+
Raises:
|
|
559
|
+
InfraConnectionError: If database connection fails.
|
|
560
|
+
InfraTimeoutError: If operation times out.
|
|
561
|
+
InfraUnavailableError: If circuit breaker is open.
|
|
562
|
+
"""
|
|
563
|
+
if not events:
|
|
564
|
+
return 0
|
|
565
|
+
|
|
566
|
+
op_correlation_id = correlation_id or uuid4()
|
|
567
|
+
|
|
568
|
+
# Check circuit breaker
|
|
569
|
+
async with self._circuit_breaker_lock:
|
|
570
|
+
await self._check_circuit_breaker(
|
|
571
|
+
operation="write_performance_metrics",
|
|
572
|
+
correlation_id=op_correlation_id,
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
context = ModelInfraErrorContext(
|
|
576
|
+
transport_type=EnumInfraTransportType.DATABASE,
|
|
577
|
+
operation="write_performance_metrics",
|
|
578
|
+
target_name="router_performance_metrics",
|
|
579
|
+
correlation_id=op_correlation_id,
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
sql = """
|
|
583
|
+
INSERT INTO router_performance_metrics (
|
|
584
|
+
id, metric_name, metric_value, created_at,
|
|
585
|
+
correlation_id, unit, agent_name, labels, metadata
|
|
586
|
+
)
|
|
587
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
|
588
|
+
ON CONFLICT (id) DO NOTHING
|
|
589
|
+
"""
|
|
590
|
+
|
|
591
|
+
try:
|
|
592
|
+
async with self._pool.acquire() as conn:
|
|
593
|
+
await conn.executemany(
|
|
594
|
+
sql,
|
|
595
|
+
[
|
|
596
|
+
(
|
|
597
|
+
e.id,
|
|
598
|
+
e.metric_name,
|
|
599
|
+
e.metric_value,
|
|
600
|
+
e.created_at,
|
|
601
|
+
e.correlation_id,
|
|
602
|
+
e.unit,
|
|
603
|
+
e.agent_name,
|
|
604
|
+
self._serialize_json(e.labels),
|
|
605
|
+
self._serialize_json(e.metadata),
|
|
606
|
+
)
|
|
607
|
+
for e in events
|
|
608
|
+
],
|
|
609
|
+
)
|
|
610
|
+
|
|
611
|
+
# Record success
|
|
612
|
+
async with self._circuit_breaker_lock:
|
|
613
|
+
await self._reset_circuit_breaker()
|
|
614
|
+
|
|
615
|
+
logger.debug(
|
|
616
|
+
"Wrote performance metrics batch",
|
|
617
|
+
extra={
|
|
618
|
+
"count": len(events),
|
|
619
|
+
"correlation_id": str(op_correlation_id),
|
|
620
|
+
},
|
|
621
|
+
)
|
|
622
|
+
return len(events)
|
|
623
|
+
|
|
624
|
+
except asyncpg.QueryCanceledError as e:
|
|
625
|
+
async with self._circuit_breaker_lock:
|
|
626
|
+
await self._record_circuit_failure(
|
|
627
|
+
operation="write_performance_metrics",
|
|
628
|
+
correlation_id=op_correlation_id,
|
|
629
|
+
)
|
|
630
|
+
raise InfraTimeoutError(
|
|
631
|
+
"Write performance metrics timed out",
|
|
632
|
+
context=ModelTimeoutErrorContext(
|
|
633
|
+
transport_type=context.transport_type,
|
|
634
|
+
operation=context.operation,
|
|
635
|
+
target_name=context.target_name,
|
|
636
|
+
correlation_id=context.correlation_id,
|
|
637
|
+
timeout_seconds=self._query_timeout,
|
|
638
|
+
),
|
|
639
|
+
) from e
|
|
640
|
+
except asyncpg.PostgresConnectionError as e:
|
|
641
|
+
async with self._circuit_breaker_lock:
|
|
642
|
+
await self._record_circuit_failure(
|
|
643
|
+
operation="write_performance_metrics",
|
|
644
|
+
correlation_id=op_correlation_id,
|
|
645
|
+
)
|
|
646
|
+
raise InfraConnectionError(
|
|
647
|
+
"Database connection failed during write_performance_metrics",
|
|
648
|
+
context=context,
|
|
649
|
+
) from e
|
|
650
|
+
except asyncpg.PostgresError as e:
|
|
651
|
+
async with self._circuit_breaker_lock:
|
|
652
|
+
await self._record_circuit_failure(
|
|
653
|
+
operation="write_performance_metrics",
|
|
654
|
+
correlation_id=op_correlation_id,
|
|
655
|
+
)
|
|
656
|
+
raise RuntimeHostError(
|
|
657
|
+
f"Database error during write_performance_metrics: {type(e).__name__}",
|
|
658
|
+
context=context,
|
|
659
|
+
) from e
|
|
660
|
+
|
|
661
|
+
async def write_detection_failures(
|
|
662
|
+
self,
|
|
663
|
+
events: list[ModelDetectionFailure],
|
|
664
|
+
correlation_id: UUID | None = None,
|
|
665
|
+
) -> int:
|
|
666
|
+
"""Write batch of detection failure events to PostgreSQL.
|
|
667
|
+
|
|
668
|
+
Uses INSERT ... ON CONFLICT (correlation_id) DO NOTHING for idempotency.
|
|
669
|
+
One failure per correlation - duplicates are silently ignored.
|
|
670
|
+
|
|
671
|
+
Args:
|
|
672
|
+
events: List of detection failure events to write.
|
|
673
|
+
correlation_id: Optional correlation ID for tracing.
|
|
674
|
+
|
|
675
|
+
Returns:
|
|
676
|
+
Count of events in the batch.
|
|
677
|
+
|
|
678
|
+
Raises:
|
|
679
|
+
InfraConnectionError: If database connection fails.
|
|
680
|
+
InfraTimeoutError: If operation times out.
|
|
681
|
+
InfraUnavailableError: If circuit breaker is open.
|
|
682
|
+
"""
|
|
683
|
+
if not events:
|
|
684
|
+
return 0
|
|
685
|
+
|
|
686
|
+
op_correlation_id = correlation_id or uuid4()
|
|
687
|
+
|
|
688
|
+
# Check circuit breaker
|
|
689
|
+
async with self._circuit_breaker_lock:
|
|
690
|
+
await self._check_circuit_breaker(
|
|
691
|
+
operation="write_detection_failures",
|
|
692
|
+
correlation_id=op_correlation_id,
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
context = ModelInfraErrorContext(
|
|
696
|
+
transport_type=EnumInfraTransportType.DATABASE,
|
|
697
|
+
operation="write_detection_failures",
|
|
698
|
+
target_name="agent_detection_failures",
|
|
699
|
+
correlation_id=op_correlation_id,
|
|
700
|
+
)
|
|
701
|
+
|
|
702
|
+
sql = """
|
|
703
|
+
INSERT INTO agent_detection_failures (
|
|
704
|
+
correlation_id, failure_reason, created_at,
|
|
705
|
+
request_summary, attempted_patterns, fallback_used, error_code, metadata
|
|
706
|
+
)
|
|
707
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
708
|
+
ON CONFLICT (correlation_id) DO NOTHING
|
|
709
|
+
"""
|
|
710
|
+
|
|
711
|
+
try:
|
|
712
|
+
async with self._pool.acquire() as conn:
|
|
713
|
+
await conn.executemany(
|
|
714
|
+
sql,
|
|
715
|
+
[
|
|
716
|
+
(
|
|
717
|
+
e.correlation_id,
|
|
718
|
+
e.failure_reason,
|
|
719
|
+
e.created_at,
|
|
720
|
+
e.request_summary,
|
|
721
|
+
self._serialize_list(e.attempted_patterns),
|
|
722
|
+
e.fallback_used,
|
|
723
|
+
e.error_code,
|
|
724
|
+
self._serialize_json(e.metadata),
|
|
725
|
+
)
|
|
726
|
+
for e in events
|
|
727
|
+
],
|
|
728
|
+
)
|
|
729
|
+
|
|
730
|
+
# Record success
|
|
731
|
+
async with self._circuit_breaker_lock:
|
|
732
|
+
await self._reset_circuit_breaker()
|
|
733
|
+
|
|
734
|
+
logger.debug(
|
|
735
|
+
"Wrote detection failures batch",
|
|
736
|
+
extra={
|
|
737
|
+
"count": len(events),
|
|
738
|
+
"correlation_id": str(op_correlation_id),
|
|
739
|
+
},
|
|
740
|
+
)
|
|
741
|
+
return len(events)
|
|
742
|
+
|
|
743
|
+
except asyncpg.QueryCanceledError as e:
|
|
744
|
+
async with self._circuit_breaker_lock:
|
|
745
|
+
await self._record_circuit_failure(
|
|
746
|
+
operation="write_detection_failures",
|
|
747
|
+
correlation_id=op_correlation_id,
|
|
748
|
+
)
|
|
749
|
+
raise InfraTimeoutError(
|
|
750
|
+
"Write detection failures timed out",
|
|
751
|
+
context=ModelTimeoutErrorContext(
|
|
752
|
+
transport_type=context.transport_type,
|
|
753
|
+
operation=context.operation,
|
|
754
|
+
target_name=context.target_name,
|
|
755
|
+
correlation_id=context.correlation_id,
|
|
756
|
+
timeout_seconds=self._query_timeout,
|
|
757
|
+
),
|
|
758
|
+
) from e
|
|
759
|
+
except asyncpg.PostgresConnectionError as e:
|
|
760
|
+
async with self._circuit_breaker_lock:
|
|
761
|
+
await self._record_circuit_failure(
|
|
762
|
+
operation="write_detection_failures",
|
|
763
|
+
correlation_id=op_correlation_id,
|
|
764
|
+
)
|
|
765
|
+
raise InfraConnectionError(
|
|
766
|
+
"Database connection failed during write_detection_failures",
|
|
767
|
+
context=context,
|
|
768
|
+
) from e
|
|
769
|
+
except asyncpg.PostgresError as e:
|
|
770
|
+
async with self._circuit_breaker_lock:
|
|
771
|
+
await self._record_circuit_failure(
|
|
772
|
+
operation="write_detection_failures",
|
|
773
|
+
correlation_id=op_correlation_id,
|
|
774
|
+
)
|
|
775
|
+
raise RuntimeHostError(
|
|
776
|
+
f"Database error during write_detection_failures: {type(e).__name__}",
|
|
777
|
+
context=context,
|
|
778
|
+
) from e
|
|
779
|
+
|
|
780
|
+
async def write_execution_logs(
|
|
781
|
+
self,
|
|
782
|
+
events: list[ModelExecutionLog],
|
|
783
|
+
correlation_id: UUID | None = None,
|
|
784
|
+
) -> int:
|
|
785
|
+
"""Write batch of execution log events to PostgreSQL.
|
|
786
|
+
|
|
787
|
+
Uses INSERT ... ON CONFLICT (execution_id) DO UPDATE for lifecycle tracking.
|
|
788
|
+
Supports status transitions (started -> running -> completed/failed).
|
|
789
|
+
Updates status, completed_at, duration_ms, quality_score, error_message,
|
|
790
|
+
error_type, metadata, and updated_at on conflict.
|
|
791
|
+
|
|
792
|
+
Args:
|
|
793
|
+
events: List of execution log events to write.
|
|
794
|
+
correlation_id: Optional correlation ID for tracing.
|
|
795
|
+
|
|
796
|
+
Returns:
|
|
797
|
+
Count of events in the batch.
|
|
798
|
+
|
|
799
|
+
Raises:
|
|
800
|
+
InfraConnectionError: If database connection fails.
|
|
801
|
+
InfraTimeoutError: If operation times out.
|
|
802
|
+
InfraUnavailableError: If circuit breaker is open.
|
|
803
|
+
"""
|
|
804
|
+
if not events:
|
|
805
|
+
return 0
|
|
806
|
+
|
|
807
|
+
op_correlation_id = correlation_id or uuid4()
|
|
808
|
+
|
|
809
|
+
# Check circuit breaker
|
|
810
|
+
async with self._circuit_breaker_lock:
|
|
811
|
+
await self._check_circuit_breaker(
|
|
812
|
+
operation="write_execution_logs",
|
|
813
|
+
correlation_id=op_correlation_id,
|
|
814
|
+
)
|
|
815
|
+
|
|
816
|
+
context = ModelInfraErrorContext(
|
|
817
|
+
transport_type=EnumInfraTransportType.DATABASE,
|
|
818
|
+
operation="write_execution_logs",
|
|
819
|
+
target_name="agent_execution_logs",
|
|
820
|
+
correlation_id=op_correlation_id,
|
|
821
|
+
)
|
|
822
|
+
|
|
823
|
+
sql = """
|
|
824
|
+
INSERT INTO agent_execution_logs (
|
|
825
|
+
execution_id, correlation_id, agent_name, status,
|
|
826
|
+
created_at, updated_at, started_at, completed_at,
|
|
827
|
+
duration_ms, exit_code, error_message, input_summary,
|
|
828
|
+
output_summary, metadata
|
|
829
|
+
)
|
|
830
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
|
|
831
|
+
ON CONFLICT (execution_id) DO UPDATE SET
|
|
832
|
+
status = EXCLUDED.status,
|
|
833
|
+
completed_at = EXCLUDED.completed_at,
|
|
834
|
+
duration_ms = EXCLUDED.duration_ms,
|
|
835
|
+
exit_code = EXCLUDED.exit_code,
|
|
836
|
+
error_message = EXCLUDED.error_message,
|
|
837
|
+
output_summary = EXCLUDED.output_summary,
|
|
838
|
+
metadata = EXCLUDED.metadata,
|
|
839
|
+
updated_at = NOW()
|
|
840
|
+
"""
|
|
841
|
+
|
|
842
|
+
try:
|
|
843
|
+
async with self._pool.acquire() as conn:
|
|
844
|
+
await conn.executemany(
|
|
845
|
+
sql,
|
|
846
|
+
[
|
|
847
|
+
(
|
|
848
|
+
e.execution_id,
|
|
849
|
+
e.correlation_id,
|
|
850
|
+
e.agent_name,
|
|
851
|
+
e.status,
|
|
852
|
+
e.created_at,
|
|
853
|
+
e.updated_at,
|
|
854
|
+
e.started_at,
|
|
855
|
+
e.completed_at,
|
|
856
|
+
e.duration_ms,
|
|
857
|
+
e.exit_code,
|
|
858
|
+
e.error_message,
|
|
859
|
+
e.input_summary,
|
|
860
|
+
e.output_summary,
|
|
861
|
+
self._serialize_json(e.metadata),
|
|
862
|
+
)
|
|
863
|
+
for e in events
|
|
864
|
+
],
|
|
865
|
+
)
|
|
866
|
+
|
|
867
|
+
# Record success
|
|
868
|
+
async with self._circuit_breaker_lock:
|
|
869
|
+
await self._reset_circuit_breaker()
|
|
870
|
+
|
|
871
|
+
logger.debug(
|
|
872
|
+
"Wrote execution logs batch",
|
|
873
|
+
extra={
|
|
874
|
+
"count": len(events),
|
|
875
|
+
"correlation_id": str(op_correlation_id),
|
|
876
|
+
},
|
|
877
|
+
)
|
|
878
|
+
return len(events)
|
|
879
|
+
|
|
880
|
+
except asyncpg.QueryCanceledError as e:
|
|
881
|
+
async with self._circuit_breaker_lock:
|
|
882
|
+
await self._record_circuit_failure(
|
|
883
|
+
operation="write_execution_logs",
|
|
884
|
+
correlation_id=op_correlation_id,
|
|
885
|
+
)
|
|
886
|
+
raise InfraTimeoutError(
|
|
887
|
+
"Write execution logs timed out",
|
|
888
|
+
context=ModelTimeoutErrorContext(
|
|
889
|
+
transport_type=context.transport_type,
|
|
890
|
+
operation=context.operation,
|
|
891
|
+
target_name=context.target_name,
|
|
892
|
+
correlation_id=context.correlation_id,
|
|
893
|
+
timeout_seconds=self._query_timeout,
|
|
894
|
+
),
|
|
895
|
+
) from e
|
|
896
|
+
except asyncpg.PostgresConnectionError as e:
|
|
897
|
+
async with self._circuit_breaker_lock:
|
|
898
|
+
await self._record_circuit_failure(
|
|
899
|
+
operation="write_execution_logs",
|
|
900
|
+
correlation_id=op_correlation_id,
|
|
901
|
+
)
|
|
902
|
+
raise InfraConnectionError(
|
|
903
|
+
"Database connection failed during write_execution_logs",
|
|
904
|
+
context=context,
|
|
905
|
+
) from e
|
|
906
|
+
except asyncpg.PostgresError as e:
|
|
907
|
+
async with self._circuit_breaker_lock:
|
|
908
|
+
await self._record_circuit_failure(
|
|
909
|
+
operation="write_execution_logs",
|
|
910
|
+
correlation_id=op_correlation_id,
|
|
911
|
+
)
|
|
912
|
+
raise RuntimeHostError(
|
|
913
|
+
f"Database error during write_execution_logs: {type(e).__name__}",
|
|
914
|
+
context=context,
|
|
915
|
+
) from e
|
|
916
|
+
|
|
917
|
+
def get_circuit_breaker_state(self) -> dict[str, JsonType]:
|
|
918
|
+
"""Return current circuit breaker state for health checks.
|
|
919
|
+
|
|
920
|
+
Returns:
|
|
921
|
+
Dict containing circuit breaker state information.
|
|
922
|
+
"""
|
|
923
|
+
return self._get_circuit_breaker_state()
|
|
924
|
+
|
|
925
|
+
|
|
926
|
+
__all__ = ["WriterAgentActionsPostgres"]
|