bazaar-compute-node 0.1.3__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.
Files changed (62) hide show
  1. bazaar_compute_node/__init__.py +3 -0
  2. bazaar_compute_node/app/__init__.py +1 -0
  3. bazaar_compute_node/app/application.py +398 -0
  4. bazaar_compute_node/app/attachments.py +154 -0
  5. bazaar_compute_node/app/command.py +342 -0
  6. bazaar_compute_node/app/config.py +121 -0
  7. bazaar_compute_node/app/registry.py +120 -0
  8. bazaar_compute_node/app/transport.py +264 -0
  9. bazaar_compute_node/app/windows_pipe.py +463 -0
  10. bazaar_compute_node/app/wrapper.py +63 -0
  11. bazaar_compute_node/bcc.py +524 -0
  12. bazaar_compute_node/cli.py +382 -0
  13. bazaar_compute_node/contrib/__init__.py +1 -0
  14. bazaar_compute_node/contrib/codex_app_server/__init__.py +63 -0
  15. bazaar_compute_node/contrib/codex_app_server/approval.py +168 -0
  16. bazaar_compute_node/contrib/codex_app_server/client.py +408 -0
  17. bazaar_compute_node/contrib/codex_app_server/events.py +431 -0
  18. bazaar_compute_node/contrib/codex_app_server/plugin.py +15 -0
  19. bazaar_compute_node/contrib/codex_app_server/process.py +583 -0
  20. bazaar_compute_node/contrib/codex_app_server/protocol.py +103 -0
  21. bazaar_compute_node/contrib/codex_app_server/runtime.py +513 -0
  22. bazaar_compute_node/contrib/logging/__init__.py +5 -0
  23. bazaar_compute_node/contrib/logging/audit.py +61 -0
  24. bazaar_compute_node/contrib/logging/plugin.py +11 -0
  25. bazaar_compute_node/contrib/sqlite/__init__.py +14 -0
  26. bazaar_compute_node/contrib/sqlite/codec.py +768 -0
  27. bazaar_compute_node/contrib/sqlite/database.py +282 -0
  28. bazaar_compute_node/contrib/sqlite/migrations.py +646 -0
  29. bazaar_compute_node/contrib/sqlite/plugin.py +11 -0
  30. bazaar_compute_node/contrib/sqlite/repository.py +1059 -0
  31. bazaar_compute_node/contrib/wecom/__init__.py +1 -0
  32. bazaar_compute_node/contrib/wecom/channel.py +960 -0
  33. bazaar_compute_node/contrib/wecom/markdown.py +146 -0
  34. bazaar_compute_node/contrib/wecom/plugin.py +29 -0
  35. bazaar_compute_node/core/__init__.py +5 -0
  36. bazaar_compute_node/core/approval.py +51 -0
  37. bazaar_compute_node/core/audit.py +101 -0
  38. bazaar_compute_node/core/channel.py +121 -0
  39. bazaar_compute_node/core/client.py +30 -0
  40. bazaar_compute_node/core/command.py +85 -0
  41. bazaar_compute_node/core/concurrency.py +29 -0
  42. bazaar_compute_node/core/correlation.py +48 -0
  43. bazaar_compute_node/core/instruction.py +224 -0
  44. bazaar_compute_node/core/lifecycle.py +48 -0
  45. bazaar_compute_node/core/models/__init__.py +63 -0
  46. bazaar_compute_node/core/models/entities.py +514 -0
  47. bazaar_compute_node/core/models/states.py +369 -0
  48. bazaar_compute_node/core/observability.py +47 -0
  49. bazaar_compute_node/core/orchestration/__init__.py +5 -0
  50. bazaar_compute_node/core/orchestration/command.py +614 -0
  51. bazaar_compute_node/core/orchestration/services.py +135 -0
  52. bazaar_compute_node/core/orchestration/session.py +891 -0
  53. bazaar_compute_node/core/orchestration/turn.py +451 -0
  54. bazaar_compute_node/core/outcomes.py +51 -0
  55. bazaar_compute_node/core/paths.py +19 -0
  56. bazaar_compute_node/core/runtime.py +118 -0
  57. bazaar_compute_node/core/storage.py +167 -0
  58. bazaar_compute_node-0.1.3.dist-info/METADATA +178 -0
  59. bazaar_compute_node-0.1.3.dist-info/RECORD +62 -0
  60. bazaar_compute_node-0.1.3.dist-info/WHEEL +4 -0
  61. bazaar_compute_node-0.1.3.dist-info/entry_points.txt +15 -0
  62. bazaar_compute_node-0.1.3.dist-info/licenses/LICENSE +613 -0
@@ -0,0 +1,513 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import json
5
+ import logging
6
+ import os
7
+ import shutil
8
+ from dataclasses import dataclass, replace
9
+ from pathlib import Path
10
+ from time import time_ns
11
+
12
+ from ...core.approval import IApprovalHandler
13
+ from ...core.instruction import DeveloperInstructionContext
14
+ from ...core.lifecycle import IAsyncLifecycle
15
+ from ...core.models import (
16
+ RuntimeEventState,
17
+ RuntimeSession,
18
+ RuntimeTurn,
19
+ )
20
+ from ...core.outcomes import ProviderCallResult, ProviderCallStatus
21
+ from ...core.paths import resolve_workspace_dir
22
+ from ...core.runtime import (
23
+ IRuntime,
24
+ IRuntimeTurnStream,
25
+ RuntimeCommandContext,
26
+ RuntimeSandboxMode,
27
+ RuntimeSessionUnavailable,
28
+ )
29
+ from .client import (
30
+ CodexAppServerClient,
31
+ parse_thread_response,
32
+ parse_turn_response,
33
+ )
34
+ from .events import CodexTurnEventStream
35
+ from .process import JsonlProcessSpec, JsonlProcessSupervisor
36
+ from .protocol import (
37
+ CodexAppServerProtocolError,
38
+ JsonlProcessExited,
39
+ JsonlProcessNotRunning,
40
+ JsonlProtocolError,
41
+ JsonlRemoteError,
42
+ JsonlRequestTimeout,
43
+ JsonlTransportError,
44
+ )
45
+
46
+ _INITIALIZE_ATTEMPTS = 2
47
+ _RESUME_ATTEMPTS = 2
48
+
49
+
50
+ @dataclass(slots=True)
51
+ class _CodexConnection:
52
+ supervisor: JsonlProcessSupervisor
53
+ client: CodexAppServerClient
54
+ workspace: Path
55
+ provider_thread_id: str
56
+ active_turn_id: str | None = None
57
+
58
+
59
+ class CodexAppServerRuntime(IRuntime, IAsyncLifecycle):
60
+ """Run one persistent Codex App Server process per bcn runtime session."""
61
+
62
+ @property
63
+ def name(self) -> str:
64
+ return "codex"
65
+
66
+ def environment_variable_names(self) -> tuple[str, ...]:
67
+ return (
68
+ "CODEX_HOME",
69
+ "CODEX_SQLITE_HOME",
70
+ "CODEX_CA_CERTIFICATE",
71
+ "SSL_CERT_FILE",
72
+ )
73
+
74
+ def __init__(
75
+ self,
76
+ context: RuntimeCommandContext,
77
+ *,
78
+ executable: str = "codex",
79
+ model: str | None = None,
80
+ effort: str | None = None,
81
+ ) -> None:
82
+ if not executable:
83
+ raise ValueError("executable must be a non-empty string")
84
+ if not context.node_id:
85
+ raise ValueError("runtime context node_id must be non-empty")
86
+ if model is not None and not model:
87
+ raise ValueError("model must be a non-empty string or None")
88
+ if effort is not None and not effort:
89
+ raise ValueError("effort must be a non-empty string or None")
90
+ self._context = context
91
+ self._executable = executable
92
+ self._model = model
93
+ self._effort = effort
94
+ self._connections: dict[str, _CodexConnection] = {}
95
+ self._logger = logging.getLogger("bazaar_compute_node.runtime.codex")
96
+ self._started = False
97
+ self._stopping = False
98
+
99
+ async def start(self, *, timeout: float) -> None:
100
+ del timeout
101
+ if self._stopping:
102
+ raise RuntimeError("Codex App Server runtime is stopping")
103
+ self._started = True
104
+
105
+ async def stop(self, *, timeout: float) -> None:
106
+ if self._stopping:
107
+ return
108
+ self._stopping = True
109
+ connections = tuple(self._connections.items())
110
+ self._connections.clear()
111
+ for _session_id, connection in connections:
112
+ try:
113
+ await connection.supervisor.stop(timeout=timeout)
114
+ except asyncio.CancelledError:
115
+ raise
116
+ except OSError, TimeoutError, JsonlTransportError:
117
+ continue
118
+ self._started = False
119
+
120
+ async def start_session(
121
+ self, session: RuntimeSession, *, timeout: float
122
+ ) -> ProviderCallResult[RuntimeSession]:
123
+ self._ensure_started()
124
+ existing = self._connections.pop(session.id, None)
125
+ if existing is not None:
126
+ await self._stop_connection(existing, timeout=timeout)
127
+ connection: _CodexConnection | None = None
128
+ try:
129
+ connection = await self._open_connection(session, timeout=timeout)
130
+ response = await connection.client.start_thread(
131
+ DeveloperInstructionContext(
132
+ node_id=self._context.node_id,
133
+ runtime_session_id=session.id,
134
+ runtime=session.runtime,
135
+ workspace=str(connection.workspace),
136
+ ).render(),
137
+ model=self._model,
138
+ cwd=connection.workspace,
139
+ timeout=timeout,
140
+ )
141
+ thread = parse_thread_response(response)
142
+ connection.provider_thread_id = thread.thread_id
143
+ self._connections[session.id] = connection
144
+ return ProviderCallResult(
145
+ status=ProviderCallStatus.CONFIRMED,
146
+ value=replace(
147
+ session,
148
+ provider_thread_id=thread.thread_id,
149
+ updated_at_ms=_now_ms(),
150
+ ),
151
+ receipt={"provider_thread_id": thread.thread_id},
152
+ )
153
+ except asyncio.CancelledError:
154
+ if connection is not None:
155
+ await self._stop_connection(connection, timeout=timeout)
156
+ raise
157
+ except Exception as error: # noqa: BLE001
158
+ if connection is not None:
159
+ await self._stop_connection(connection, timeout=timeout)
160
+ return _provider_result(error)
161
+
162
+ async def resume_session(
163
+ self, session: RuntimeSession, *, timeout: float
164
+ ) -> ProviderCallResult[RuntimeSession]:
165
+ self._ensure_started()
166
+ provider_thread_id = session.provider_thread_id
167
+ if provider_thread_id is None:
168
+ return ProviderCallResult(
169
+ status=ProviderCallStatus.FAILED,
170
+ error_kind="provider_failed",
171
+ error_message="cannot resume a runtime session without a provider thread",
172
+ )
173
+ connection = self._connections.pop(session.id, None)
174
+ if connection is not None and not connection.supervisor.is_running:
175
+ await self._stop_connection(connection, timeout=timeout)
176
+ connection = None
177
+ for attempt in range(_RESUME_ATTEMPTS):
178
+ if connection is None:
179
+ try:
180
+ connection = await self._open_connection(session, timeout=timeout)
181
+ except asyncio.CancelledError:
182
+ raise
183
+ except Exception as error: # noqa: BLE001
184
+ return _provider_result(error)
185
+ try:
186
+ response = await connection.client.resume_thread(
187
+ provider_thread_id,
188
+ model=self._model,
189
+ cwd=connection.workspace,
190
+ timeout=timeout,
191
+ )
192
+ thread = parse_thread_response(response)
193
+ if thread.thread_id != provider_thread_id:
194
+ raise CodexAppServerProtocolError(
195
+ "thread/resume returned a different provider thread"
196
+ )
197
+ connection.provider_thread_id = thread.thread_id
198
+ self._connections[session.id] = connection
199
+ if attempt > 0:
200
+ self._logger.info(
201
+ "%s",
202
+ json.dumps(
203
+ {
204
+ "event_name": "runtime.process.resume.retry_succeeded",
205
+ "metadata": {
206
+ "attempt": attempt + 1,
207
+ "session_id": session.bcn_session_id,
208
+ },
209
+ },
210
+ separators=(",", ":"),
211
+ sort_keys=True,
212
+ ),
213
+ )
214
+ return ProviderCallResult(
215
+ status=ProviderCallStatus.CONFIRMED,
216
+ value=replace(
217
+ session,
218
+ provider_thread_id=thread.thread_id,
219
+ updated_at_ms=_now_ms(),
220
+ ),
221
+ receipt={"provider_thread_id": thread.thread_id},
222
+ )
223
+ except asyncio.CancelledError:
224
+ await self._stop_connection(connection, timeout=timeout)
225
+ raise
226
+ except (
227
+ JsonlProcessExited,
228
+ JsonlProcessNotRunning,
229
+ JsonlRequestTimeout,
230
+ ) as error:
231
+ await self._stop_connection(connection, timeout=timeout)
232
+ connection = None
233
+ if attempt + 1 == _RESUME_ATTEMPTS:
234
+ self._logger.error(
235
+ "%s",
236
+ json.dumps(
237
+ {
238
+ "event_name": "runtime.process.resume.retry_exhausted",
239
+ "metadata": {
240
+ "attempt": attempt + 1,
241
+ "error_type": type(error).__name__,
242
+ "session_id": session.bcn_session_id,
243
+ },
244
+ },
245
+ separators=(",", ":"),
246
+ sort_keys=True,
247
+ ),
248
+ )
249
+ return _provider_result(error)
250
+ self._logger.warning(
251
+ "%s",
252
+ json.dumps(
253
+ {
254
+ "event_name": "runtime.process.resume.retrying",
255
+ "metadata": {
256
+ "attempt": attempt + 1,
257
+ "error_type": type(error).__name__,
258
+ "next_attempt": attempt + 2,
259
+ "session_id": session.bcn_session_id,
260
+ },
261
+ },
262
+ separators=(",", ":"),
263
+ sort_keys=True,
264
+ ),
265
+ )
266
+ except Exception as error: # noqa: BLE001
267
+ await self._stop_connection(connection, timeout=timeout)
268
+ return _provider_result(error)
269
+ raise AssertionError("Codex resume retry loop did not return")
270
+
271
+ async def start_turn(
272
+ self,
273
+ session: RuntimeSession,
274
+ turn: RuntimeTurn,
275
+ input_text: str,
276
+ approval_handler: IApprovalHandler,
277
+ *,
278
+ timeout: float,
279
+ ) -> IRuntimeTurnStream:
280
+ self._ensure_started()
281
+ connection = self._connections.get(session.id)
282
+ if connection is None or not connection.supervisor.is_running:
283
+ raise RuntimeSessionUnavailable("Codex App Server process is not running")
284
+ if connection.active_turn_id is not None:
285
+ raise RuntimeError(
286
+ f"runtime session already has an active turn: {connection.active_turn_id}"
287
+ )
288
+ provider_turn_id: str | None = None
289
+ initial_error: BaseException | None = None
290
+ initial_error_kind = "provider_unknown"
291
+ initial_error_state = RuntimeEventState.UNKNOWN
292
+ try:
293
+ if self._context.sandbox_mode is RuntimeSandboxMode.WORKSPACE_WRITE:
294
+ sandbox_policy: dict[str, object] = {
295
+ "type": "workspaceWrite",
296
+ "writableRoots": [str(connection.workspace)],
297
+ "networkAccess": self._context.network_access,
298
+ }
299
+ elif self._context.sandbox_mode is RuntimeSandboxMode.DANGER_FULL_ACCESS:
300
+ sandbox_policy = {"type": "dangerFullAccess"}
301
+ else:
302
+ raise AssertionError(
303
+ f"unsupported runtime sandbox mode: {self._context.sandbox_mode}"
304
+ )
305
+ response = await connection.client.start_turn(
306
+ connection.provider_thread_id,
307
+ input_text,
308
+ client_user_message_id=turn.client_user_message_id,
309
+ model=self._model,
310
+ effort=self._effort,
311
+ cwd=connection.workspace,
312
+ sandbox_policy=sandbox_policy,
313
+ timeout=timeout,
314
+ )
315
+ provider_turn = parse_turn_response(response)
316
+ provider_turn_id = provider_turn.turn_id
317
+ except asyncio.CancelledError:
318
+ raise
319
+ except Exception as error: # noqa: BLE001
320
+ initial_error = error
321
+ if isinstance(error, JsonlRemoteError):
322
+ initial_error_kind = "provider_failed"
323
+ initial_error_state = RuntimeEventState.FAILED
324
+ elif isinstance(error, CodexAppServerProtocolError):
325
+ initial_error_kind = "protocol"
326
+ initial_error_state = RuntimeEventState.FAILED
327
+ connection.active_turn_id = turn.turn_id
328
+ return CodexTurnEventStream(
329
+ connection.supervisor,
330
+ node_id=self._context.node_id,
331
+ runtime=session.runtime,
332
+ session_id=session.bcn_session_id,
333
+ runtime_session_id=session.id,
334
+ turn_id=turn.turn_id,
335
+ provider_thread_id=connection.provider_thread_id,
336
+ provider_turn_id=provider_turn_id,
337
+ approval_handler=approval_handler,
338
+ approval_timeout=timeout,
339
+ initial_error=initial_error,
340
+ initial_error_kind=initial_error_kind,
341
+ initial_error_state=initial_error_state,
342
+ on_closed=lambda: self._clear_active_turn(session.id, turn.turn_id),
343
+ )
344
+
345
+ async def interrupt_turn(
346
+ self,
347
+ session: RuntimeSession,
348
+ turn: RuntimeTurn,
349
+ *,
350
+ timeout: float,
351
+ ) -> ProviderCallResult[RuntimeTurn]:
352
+ self._ensure_started()
353
+ connection = self._connections.get(session.id)
354
+ provider_turn_id = turn.provider_turn_id
355
+ if connection is None or provider_turn_id is None:
356
+ return ProviderCallResult(
357
+ status=ProviderCallStatus.FAILED,
358
+ error_kind="provider_failed",
359
+ error_message="runtime turn has no active provider binding",
360
+ )
361
+ try:
362
+ await connection.client.interrupt_turn(
363
+ connection.provider_thread_id,
364
+ provider_turn_id,
365
+ timeout=timeout,
366
+ )
367
+ except asyncio.CancelledError:
368
+ raise
369
+ except Exception as error: # noqa: BLE001
370
+ result = _provider_result(error)
371
+ return ProviderCallResult(
372
+ status=result.status,
373
+ error_kind=result.error_kind,
374
+ error_message=result.error_message,
375
+ )
376
+ return ProviderCallResult(
377
+ status=ProviderCallStatus.QUEUED,
378
+ value=turn,
379
+ receipt={
380
+ "provider_thread_id": connection.provider_thread_id,
381
+ "provider_turn_id": provider_turn_id,
382
+ },
383
+ )
384
+
385
+ async def stop_session(
386
+ self, session: RuntimeSession, *, timeout: float
387
+ ) -> ProviderCallResult[RuntimeSession]:
388
+ connection = self._connections.pop(session.id, None)
389
+ if connection is None:
390
+ return ProviderCallResult(
391
+ status=ProviderCallStatus.CONFIRMED,
392
+ value=session,
393
+ )
394
+ try:
395
+ await connection.supervisor.stop(timeout=timeout)
396
+ except asyncio.CancelledError:
397
+ raise
398
+ except Exception as error: # noqa: BLE001
399
+ return _provider_result(error)
400
+ return ProviderCallResult(
401
+ status=ProviderCallStatus.CONFIRMED,
402
+ value=session,
403
+ )
404
+
405
+ async def _open_connection(
406
+ self,
407
+ session: RuntimeSession,
408
+ *,
409
+ timeout: float,
410
+ ) -> _CodexConnection:
411
+ executable = shutil.which(self._executable)
412
+ if executable is None:
413
+ raise FileNotFoundError(f"Codex executable not found: {self._executable}")
414
+ workspace = resolve_workspace_dir(session.workspace_id)
415
+ await asyncio.to_thread(
416
+ workspace.mkdir, parents=True, exist_ok=True, mode=0o700
417
+ )
418
+ if os.name != "nt":
419
+ await asyncio.to_thread(workspace.chmod, 0o700)
420
+ environment = dict(self._context.environment_for_session(session))
421
+ for attempt in range(_INITIALIZE_ATTEMPTS):
422
+ supervisor = JsonlProcessSupervisor(
423
+ JsonlProcessSpec(
424
+ executable=executable,
425
+ arguments=("app-server", "--stdio"),
426
+ cwd=workspace,
427
+ environment=environment,
428
+ )
429
+ )
430
+ client = CodexAppServerClient(supervisor)
431
+ await supervisor.start(timeout=self._context.startup_timeout_seconds)
432
+ try:
433
+ await client.initialize(
434
+ client_info=self._context.client_info,
435
+ timeout=self._context.startup_timeout_seconds,
436
+ )
437
+ except JsonlRequestTimeout:
438
+ await supervisor.stop(timeout=timeout)
439
+ if attempt + 1 == _INITIALIZE_ATTEMPTS:
440
+ raise
441
+ continue
442
+ except BaseException:
443
+ await supervisor.stop(timeout=timeout)
444
+ raise
445
+ return _CodexConnection(
446
+ supervisor=supervisor,
447
+ client=client,
448
+ workspace=workspace,
449
+ provider_thread_id=session.provider_thread_id or "",
450
+ )
451
+ raise AssertionError("Codex initialization retry loop did not return")
452
+
453
+ async def _stop_connection(
454
+ self,
455
+ connection: _CodexConnection,
456
+ *,
457
+ timeout: float,
458
+ ) -> None:
459
+ try:
460
+ await connection.supervisor.stop(timeout=timeout)
461
+ except asyncio.CancelledError:
462
+ raise
463
+ except OSError, TimeoutError, JsonlTransportError:
464
+ return
465
+
466
+ def _clear_active_turn(self, session_id: str, turn_id: str) -> None:
467
+ connection = self._connections.get(session_id)
468
+ if connection is not None and connection.active_turn_id == turn_id:
469
+ connection.active_turn_id = None
470
+
471
+ def _ensure_started(self) -> None:
472
+ if not self._started or self._stopping:
473
+ raise RuntimeError("Codex App Server runtime is not started")
474
+
475
+
476
+ def _provider_result(error: BaseException) -> ProviderCallResult[RuntimeSession]:
477
+ if isinstance(
478
+ error, (JsonlProcessExited, JsonlProcessNotRunning, JsonlRequestTimeout)
479
+ ):
480
+ return ProviderCallResult(
481
+ status=ProviderCallStatus.UNKNOWN,
482
+ error_kind="provider_unknown",
483
+ error_message=_safe_error_message(error),
484
+ )
485
+ if isinstance(error, (JsonlProtocolError, CodexAppServerProtocolError)):
486
+ return ProviderCallResult(
487
+ status=ProviderCallStatus.FAILED,
488
+ error_kind="protocol",
489
+ error_message=_safe_error_message(error),
490
+ )
491
+ if isinstance(error, JsonlRemoteError):
492
+ return ProviderCallResult(
493
+ status=ProviderCallStatus.FAILED,
494
+ error_kind="provider_failed",
495
+ error_message=_safe_error_message(error),
496
+ )
497
+ return ProviderCallResult(
498
+ status=ProviderCallStatus.FAILED,
499
+ error_kind="provider_failed",
500
+ error_message=_safe_error_message(error),
501
+ )
502
+
503
+
504
+ def _safe_error_message(error: BaseException) -> str:
505
+ message = str(error).strip()
506
+ return message or type(error).__name__
507
+
508
+
509
+ def _now_ms() -> int:
510
+ return time_ns() // 1_000_000
511
+
512
+
513
+ __all__ = ["CodexAppServerRuntime"]
@@ -0,0 +1,5 @@
1
+ from __future__ import annotations
2
+
3
+ from .audit import LoggingAudit
4
+
5
+ __all__ = ["LoggingAudit"]
@@ -0,0 +1,61 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import logging
5
+ from dataclasses import fields
6
+
7
+ from ...core.audit import AuditEvent
8
+ from ...core.observability import IAudit, LogLevel
9
+
10
+ _LOG_LEVELS = {
11
+ LogLevel.DEBUG: logging.DEBUG,
12
+ LogLevel.INFO: logging.INFO,
13
+ LogLevel.WARNING: logging.WARNING,
14
+ LogLevel.ERROR: logging.ERROR,
15
+ }
16
+
17
+
18
+ class LoggingAudit(IAudit):
19
+ """Emit sanitized audit events through the process logging pipeline."""
20
+
21
+ @property
22
+ def name(self) -> str:
23
+ return "logging"
24
+
25
+ def __init__(self, logger: logging.Logger | None = None) -> None:
26
+ if logger is None:
27
+ logger = logging.getLogger("bazaar_compute_node.audit")
28
+ if not logger.handlers:
29
+ logger.addHandler(logging.StreamHandler())
30
+ logger.setLevel(logging.INFO)
31
+ logger.propagate = False
32
+ self._logger = logger
33
+
34
+ async def append(self, event: AuditEvent, *, timeout: float) -> None:
35
+ del timeout
36
+ correlation = {
37
+ field.name: value
38
+ for field in fields(event.correlation)
39
+ if (value := getattr(event.correlation, field.name)) is not None
40
+ }
41
+ payload: dict[str, object] = {
42
+ "event_name": event.event_name,
43
+ "state": event.state.value,
44
+ "created_at_ms": event.created_at_ms,
45
+ "correlation": correlation,
46
+ "metadata": dict(event.metadata),
47
+ }
48
+ for key, value in (
49
+ ("duration_ms", event.duration_ms),
50
+ ("error_kind", event.error_kind.value if event.error_kind else None),
51
+ ("error_type", event.error_type),
52
+ ("error_message", event.error_message),
53
+ ("traceback_ref", event.traceback_ref),
54
+ ):
55
+ if value is not None:
56
+ payload[key] = value
57
+ self._logger.log(
58
+ _LOG_LEVELS[event.level],
59
+ "%s",
60
+ json.dumps(payload, separators=(",", ":"), sort_keys=True, default=str),
61
+ )
@@ -0,0 +1,11 @@
1
+ from __future__ import annotations
2
+
3
+ from ...core.observability import IAudit
4
+ from .audit import LoggingAudit
5
+
6
+
7
+ def create_audit() -> IAudit:
8
+ return LoggingAudit()
9
+
10
+
11
+ __all__ = ["create_audit"]
@@ -0,0 +1,14 @@
1
+ """SQLite storage foundation for persistent node state."""
2
+
3
+ from .database import NodeIdentityError, SqliteDatabase
4
+ from .migrations import (
5
+ MigrationChecksumError,
6
+ MigrationError,
7
+ )
8
+
9
+ __all__ = [
10
+ "MigrationChecksumError",
11
+ "MigrationError",
12
+ "NodeIdentityError",
13
+ "SqliteDatabase",
14
+ ]