activegraph 1.0.0rc2__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 (82) hide show
  1. activegraph/__init__.py +222 -0
  2. activegraph/__main__.py +5 -0
  3. activegraph/behaviors/__init__.py +1 -0
  4. activegraph/behaviors/base.py +153 -0
  5. activegraph/behaviors/decorators.py +218 -0
  6. activegraph/cli/__init__.py +17 -0
  7. activegraph/cli/main.py +793 -0
  8. activegraph/cli/quickstart.py +556 -0
  9. activegraph/core/__init__.py +1 -0
  10. activegraph/core/clock.py +46 -0
  11. activegraph/core/event.py +33 -0
  12. activegraph/core/graph.py +846 -0
  13. activegraph/core/ids.py +135 -0
  14. activegraph/core/patch.py +47 -0
  15. activegraph/core/view.py +59 -0
  16. activegraph/errors.py +342 -0
  17. activegraph/frame.py +15 -0
  18. activegraph/llm/__init__.py +51 -0
  19. activegraph/llm/anthropic.py +339 -0
  20. activegraph/llm/cache.py +180 -0
  21. activegraph/llm/errors.py +257 -0
  22. activegraph/llm/prompt.py +420 -0
  23. activegraph/llm/provider.py +66 -0
  24. activegraph/llm/recorded.py +270 -0
  25. activegraph/llm/types.py +130 -0
  26. activegraph/observability/__init__.py +61 -0
  27. activegraph/observability/logging.py +205 -0
  28. activegraph/observability/metrics.py +219 -0
  29. activegraph/observability/migration.py +404 -0
  30. activegraph/observability/prometheus.py +101 -0
  31. activegraph/observability/status.py +83 -0
  32. activegraph/packs/__init__.py +999 -0
  33. activegraph/packs/diligence/__init__.py +86 -0
  34. activegraph/packs/diligence/behaviors.py +447 -0
  35. activegraph/packs/diligence/fixtures/__init__.py +364 -0
  36. activegraph/packs/diligence/fixtures/companies.py +511 -0
  37. activegraph/packs/diligence/object_types.py +163 -0
  38. activegraph/packs/diligence/settings.py +60 -0
  39. activegraph/packs/diligence/tools.py +124 -0
  40. activegraph/packs/loader.py +709 -0
  41. activegraph/packs/scaffold.py +317 -0
  42. activegraph/policy.py +20 -0
  43. activegraph/runtime/__init__.py +1 -0
  44. activegraph/runtime/behavior_graph.py +151 -0
  45. activegraph/runtime/budget.py +120 -0
  46. activegraph/runtime/config_errors.py +124 -0
  47. activegraph/runtime/diff.py +145 -0
  48. activegraph/runtime/errors.py +216 -0
  49. activegraph/runtime/exec_errors.py +232 -0
  50. activegraph/runtime/patterns.py +946 -0
  51. activegraph/runtime/queue.py +27 -0
  52. activegraph/runtime/registration_errors.py +291 -0
  53. activegraph/runtime/registry.py +111 -0
  54. activegraph/runtime/runtime.py +2441 -0
  55. activegraph/runtime/scheduler.py +206 -0
  56. activegraph/runtime/view_builder.py +65 -0
  57. activegraph/store/__init__.py +41 -0
  58. activegraph/store/base.py +66 -0
  59. activegraph/store/conformance.py +161 -0
  60. activegraph/store/errors.py +84 -0
  61. activegraph/store/memory.py +118 -0
  62. activegraph/store/postgres.py +609 -0
  63. activegraph/store/serde.py +202 -0
  64. activegraph/store/sqlite.py +446 -0
  65. activegraph/store/url.py +230 -0
  66. activegraph/tools/__init__.py +64 -0
  67. activegraph/tools/base.py +57 -0
  68. activegraph/tools/cache.py +157 -0
  69. activegraph/tools/context.py +48 -0
  70. activegraph/tools/decorators.py +70 -0
  71. activegraph/tools/errors.py +320 -0
  72. activegraph/tools/graph_query.py +94 -0
  73. activegraph/tools/recorded.py +205 -0
  74. activegraph/tools/web_fetch.py +80 -0
  75. activegraph/trace/__init__.py +1 -0
  76. activegraph/trace/causal.py +123 -0
  77. activegraph/trace/printer.py +495 -0
  78. activegraph-1.0.0rc2.dist-info/METADATA +228 -0
  79. activegraph-1.0.0rc2.dist-info/RECORD +82 -0
  80. activegraph-1.0.0rc2.dist-info/WHEEL +5 -0
  81. activegraph-1.0.0rc2.dist-info/entry_points.txt +5 -0
  82. activegraph-1.0.0rc2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,66 @@
1
+ """The `LLMProvider` Protocol every provider implements.
2
+
3
+ CONTRACT v0.6 #3, extended in v0.7. Narrow, explicit, keyword-only.
4
+ Shipped reference implementation is `AnthropicProvider`. Tests use
5
+ `RecordedLLMProvider` + `RecordingLLMProvider`. The demo ships its
6
+ own scripted provider.
7
+
8
+ A provider does three things:
9
+ * `complete()`: run a single non-streaming completion. v0.7 adds
10
+ an optional `tools=` parameter; when non-empty, the model is
11
+ allowed to return tool_use blocks in the response.
12
+ * `estimate_cost()`: turn token counts into USD (Decimal).
13
+ * `count_tokens()`: provider-official input token count for the
14
+ prompt that's about to be sent. Used for pre-call budget gating
15
+ when `budget.max_cost_usd` is set; otherwise skipped (see
16
+ CONTRACT v0.6 #4 / decision 10).
17
+
18
+ No streaming, no multi-model orchestration — those are deferred to
19
+ v0.8+. Tool use IS in v0.7, but the loop is orchestrated by the
20
+ runtime, not the provider: provider returns `tool_calls`, runtime
21
+ invokes the tool, runtime re-calls `complete()` with the result
22
+ echoed back as a `role="tool"` message.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ from decimal import Decimal
28
+ from typing import Any, Optional, Protocol, runtime_checkable
29
+
30
+ from activegraph.llm.types import LLMMessage, LLMResponse
31
+
32
+
33
+ @runtime_checkable
34
+ class LLMProvider(Protocol):
35
+ def complete(
36
+ self,
37
+ *,
38
+ system: str,
39
+ messages: list[LLMMessage],
40
+ model: str,
41
+ max_tokens: int,
42
+ temperature: float,
43
+ top_p: float,
44
+ output_schema: Optional[type],
45
+ timeout_seconds: float,
46
+ tools: Optional[list[dict[str, Any]]] = None,
47
+ ) -> LLMResponse:
48
+ ...
49
+
50
+ def estimate_cost(
51
+ self,
52
+ *,
53
+ input_tokens: int,
54
+ output_tokens: int,
55
+ model: str,
56
+ ) -> Decimal:
57
+ ...
58
+
59
+ def count_tokens(
60
+ self,
61
+ *,
62
+ system: str,
63
+ messages: list[LLMMessage],
64
+ model: str,
65
+ ) -> int:
66
+ ...
@@ -0,0 +1,270 @@
1
+ """Fixture-based LLM providers for tests.
2
+
3
+ CONTRACT v0.6 #12, plus decision-3 adjustment for `recorded_at`:
4
+
5
+ RecordedLLMProvider — looks up fixtures by prompt hash. Tests run
6
+ against this. Raises if a fixture is
7
+ missing (so tests fail loud rather than
8
+ regressing into live calls).
9
+
10
+ RecordingLLMProvider — wraps another provider, mirrors every call
11
+ to disk as a fixture file. Use once with
12
+ `--record` to seed fixtures, then commit
13
+ them. Marked with `@pytest.mark.records_llm`
14
+ so they don't run in CI without explicit
15
+ opt-in.
16
+
17
+ Fixture file layout (`tests/fixtures/llm/<sha256_hex>.json`):
18
+
19
+ {
20
+ "prompt_hash": "<sha256_hex>",
21
+ "recorded_at": "2026-05-15T10:32:01Z", # outside the hash
22
+ "model": "claude-sonnet-4-5",
23
+ "prompt": { # only this hashes
24
+ "model": ..., "system": ..., "messages": [...],
25
+ "output_schema_name": ..., "output_schema_json": {...},
26
+ "max_tokens": ..., "temperature": ..., "top_p": ...,
27
+ "deterministic": ...
28
+ },
29
+ "response": {
30
+ "raw_text": "...", "parsed": {...},
31
+ "input_tokens": 0, "output_tokens": 0,
32
+ "cost_usd": "0.001", "latency_seconds": 0.0,
33
+ "model": "...", "finish_reason": "end_turn",
34
+ "seed": null, "provider_meta": {}
35
+ }
36
+ }
37
+
38
+ `recorded_at` is intentionally OUTSIDE the hashed `prompt` payload so
39
+ it doesn't perturb lookups but stays available for future debugging
40
+ when fixtures drift.
41
+ """
42
+
43
+ from __future__ import annotations
44
+
45
+ import hashlib
46
+ import json
47
+ import os
48
+ from datetime import datetime, timezone
49
+ from decimal import Decimal
50
+ from typing import Any, Optional
51
+
52
+ from activegraph.llm.errors import LLMBehaviorError
53
+ from activegraph.llm.provider import LLMProvider
54
+ from activegraph.llm.types import LLMMessage, LLMResponse
55
+
56
+
57
+ def _now_iso() -> str:
58
+ return (
59
+ datetime.now(tz=timezone.utc)
60
+ .isoformat(timespec="seconds")
61
+ .replace("+00:00", "Z")
62
+ )
63
+
64
+
65
+ def _canonical_prompt_payload(
66
+ *,
67
+ model: str,
68
+ system: str,
69
+ messages: list[LLMMessage],
70
+ output_schema: Optional[type],
71
+ max_tokens: int,
72
+ temperature: float,
73
+ top_p: float,
74
+ deterministic: bool,
75
+ tools: Optional[list[dict[str, Any]]] = None,
76
+ ) -> dict[str, Any]:
77
+ from activegraph.llm.prompt import schema_to_json
78
+
79
+ return {
80
+ "model": model,
81
+ "system": system,
82
+ "messages": [m.to_dict() for m in messages],
83
+ "output_schema_name": (
84
+ getattr(output_schema, "__name__", None) if output_schema else None
85
+ ),
86
+ "output_schema_json": schema_to_json(output_schema),
87
+ "max_tokens": int(max_tokens),
88
+ "temperature": float(temperature),
89
+ "top_p": float(top_p),
90
+ "deterministic": bool(deterministic),
91
+ # v0.7: tool definitions contribute to the prompt hash so a
92
+ # behavior gaining or losing a tool produces a different key.
93
+ "tools": list(tools) if tools else None,
94
+ }
95
+
96
+
97
+ def _hash_payload(payload: dict[str, Any]) -> str:
98
+ canonical = json.dumps(payload, sort_keys=True, separators=(",", ":"))
99
+ return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
100
+
101
+
102
+ # ---------- RecordedLLMProvider ---------------------------------------------
103
+
104
+
105
+ class RecordedLLMProvider(LLMProvider):
106
+ """Reads fixtures from a directory keyed by prompt hash.
107
+
108
+ Tests construct one of these instead of an `AnthropicProvider`.
109
+ Missing fixtures raise so the test fails loud — there is no
110
+ silent fallthrough to a real call.
111
+ """
112
+
113
+ def __init__(self, fixtures_dir: str) -> None:
114
+ self._dir = fixtures_dir
115
+
116
+ # ---- LLMProvider methods ----
117
+
118
+ def complete(
119
+ self,
120
+ *,
121
+ system: str,
122
+ messages: list[LLMMessage],
123
+ model: str,
124
+ max_tokens: int,
125
+ temperature: float,
126
+ top_p: float,
127
+ output_schema: Optional[type],
128
+ timeout_seconds: float,
129
+ tools: Optional[list[dict[str, Any]]] = None,
130
+ ) -> LLMResponse:
131
+ payload = _canonical_prompt_payload(
132
+ model=model,
133
+ system=system,
134
+ messages=messages,
135
+ output_schema=output_schema,
136
+ max_tokens=max_tokens,
137
+ temperature=temperature,
138
+ top_p=top_p,
139
+ deterministic=(temperature == 0.0 and top_p == 1.0),
140
+ tools=tools,
141
+ )
142
+ prompt_hash = _hash_payload(payload)
143
+ path = os.path.join(self._dir, f"{prompt_hash}.json")
144
+ if not os.path.exists(path):
145
+ raise LLMBehaviorError(
146
+ "llm.fixture_missing",
147
+ f"no recorded fixture for prompt_hash={prompt_hash} in {self._dir}",
148
+ payload_extras={"prompt_hash": prompt_hash, "fixtures_dir": self._dir},
149
+ )
150
+ with open(path, "r") as f:
151
+ data = json.load(f)
152
+ return _response_from_fixture(data["response"], output_schema)
153
+
154
+ def estimate_cost(
155
+ self, *, input_tokens: int, output_tokens: int, model: str
156
+ ) -> Decimal:
157
+ # Tests don't care about real pricing; just return zero.
158
+ return Decimal("0")
159
+
160
+ def count_tokens(
161
+ self, *, system: str, messages: list[LLMMessage], model: str
162
+ ) -> int:
163
+ total = len(system) + sum(len(m.content) for m in messages)
164
+ return max(1, total // 4)
165
+
166
+
167
+ def _response_from_fixture(
168
+ rdata: dict[str, Any], output_schema: Optional[type]
169
+ ) -> LLMResponse:
170
+ parsed_raw = rdata.get("parsed")
171
+ parsed: Any = None
172
+ if parsed_raw is not None and output_schema is not None:
173
+ parsed = output_schema.model_validate(parsed_raw)
174
+ elif parsed_raw is not None:
175
+ parsed = parsed_raw
176
+ cost = rdata.get("cost_usd", "0")
177
+ if not isinstance(cost, Decimal):
178
+ cost = Decimal(str(cost))
179
+ return LLMResponse(
180
+ raw_text=rdata.get("raw_text", ""),
181
+ parsed=parsed,
182
+ input_tokens=int(rdata.get("input_tokens", 0) or 0),
183
+ output_tokens=int(rdata.get("output_tokens", 0) or 0),
184
+ cost_usd=cost,
185
+ latency_seconds=float(rdata.get("latency_seconds", 0.0) or 0.0),
186
+ model=rdata.get("model", "?"),
187
+ finish_reason=rdata.get("finish_reason", "end_turn"),
188
+ seed=rdata.get("seed"),
189
+ cache_hit=False,
190
+ provider_meta=dict(rdata.get("provider_meta", {}) or {}),
191
+ )
192
+
193
+
194
+ # ---------- RecordingLLMProvider --------------------------------------------
195
+
196
+
197
+ class RecordingLLMProvider(LLMProvider):
198
+ """Wraps a real provider and persists responses to fixtures.
199
+
200
+ Use this once (with `--record` opt-in) to seed `tests/fixtures/llm`
201
+ against the live Anthropic API, then commit the fixtures and run
202
+ tests against `RecordedLLMProvider` thereafter.
203
+ """
204
+
205
+ def __init__(self, inner: LLMProvider, fixtures_dir: str) -> None:
206
+ self._inner = inner
207
+ self._dir = fixtures_dir
208
+ os.makedirs(self._dir, exist_ok=True)
209
+
210
+ def complete(
211
+ self,
212
+ *,
213
+ system: str,
214
+ messages: list[LLMMessage],
215
+ model: str,
216
+ max_tokens: int,
217
+ temperature: float,
218
+ top_p: float,
219
+ output_schema: Optional[type],
220
+ timeout_seconds: float,
221
+ tools: Optional[list[dict[str, Any]]] = None,
222
+ ) -> LLMResponse:
223
+ response = self._inner.complete(
224
+ system=system,
225
+ messages=messages,
226
+ model=model,
227
+ max_tokens=max_tokens,
228
+ temperature=temperature,
229
+ top_p=top_p,
230
+ output_schema=output_schema,
231
+ timeout_seconds=timeout_seconds,
232
+ tools=tools,
233
+ )
234
+ payload = _canonical_prompt_payload(
235
+ model=model,
236
+ system=system,
237
+ messages=messages,
238
+ output_schema=output_schema,
239
+ max_tokens=max_tokens,
240
+ temperature=temperature,
241
+ top_p=top_p,
242
+ deterministic=(temperature == 0.0 and top_p == 1.0),
243
+ tools=tools,
244
+ )
245
+ prompt_hash = _hash_payload(payload)
246
+ fixture = {
247
+ "prompt_hash": prompt_hash,
248
+ "recorded_at": _now_iso(),
249
+ "model": model,
250
+ "prompt": payload,
251
+ "response": response.to_dict(),
252
+ }
253
+ path = os.path.join(self._dir, f"{prompt_hash}.json")
254
+ with open(path, "w") as f:
255
+ json.dump(fixture, f, indent=2, sort_keys=True)
256
+ return response
257
+
258
+ def estimate_cost(
259
+ self, *, input_tokens: int, output_tokens: int, model: str
260
+ ) -> Decimal:
261
+ return self._inner.estimate_cost(
262
+ input_tokens=input_tokens, output_tokens=output_tokens, model=model
263
+ )
264
+
265
+ def count_tokens(
266
+ self, *, system: str, messages: list[LLMMessage], model: str
267
+ ) -> int:
268
+ return self._inner.count_tokens(
269
+ system=system, messages=messages, model=model
270
+ )
@@ -0,0 +1,130 @@
1
+ """LLM data types. Locked in v0.6, extended in v0.7.
2
+
3
+ These shapes are part of the public contract:
4
+
5
+ LLMMessage — single role-tagged message in the conversation history.
6
+ v0.7 adds the "tool" role and `tool_use_id` so the
7
+ LLM ↔ tool turn loop can echo results back.
8
+ ToolCall — a single tool-call request returned by the model
9
+ inside an `LLMResponse.tool_calls`. v0.7 addition.
10
+ LLMResponse — what every provider's `complete()` returns. Carries
11
+ raw text, parsed structured output (if a schema was
12
+ requested), token counts, cost, latency, model id,
13
+ finish reason, a `cache_hit` flag, and (v0.7) an
14
+ optional list of `tool_calls`.
15
+
16
+ Anything provider-specific (Anthropic stop reasons, retry-after
17
+ seconds, etc.) goes into the `provider_meta` dict so the contract
18
+ stays narrow.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from dataclasses import dataclass, field
24
+ from decimal import Decimal
25
+ from typing import Any, Literal, Optional
26
+
27
+
28
+ # v0.7: the assistant can return tool_use blocks; the user echoes results
29
+ # back as a "tool" message. Anthropic uses content blocks with a
30
+ # `tool_use_id`; we flatten to a string content + carry the id alongside.
31
+ Role = Literal["user", "assistant", "tool"]
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class LLMMessage:
36
+ """One message in a chat-style prompt.
37
+
38
+ Anthropic's `system` prompt is conventionally separate from the
39
+ `messages` list, so we keep `system` out of this dataclass and pass
40
+ it as its own argument to `LLMProvider.complete()`. That keeps the
41
+ interface aligned with what the SDK actually wants.
42
+
43
+ CONTRACT v0.7: a `role="tool"` message echoes a tool result back to
44
+ the model. `tool_use_id` ties it to the originating tool_use block
45
+ from the previous assistant turn.
46
+ """
47
+
48
+ role: Role
49
+ content: str
50
+ tool_use_id: Optional[str] = None
51
+ tool_name: Optional[str] = None
52
+
53
+ def to_dict(self) -> dict[str, Any]:
54
+ out: dict[str, Any] = {"role": self.role, "content": self.content}
55
+ if self.tool_use_id is not None:
56
+ out["tool_use_id"] = self.tool_use_id
57
+ if self.tool_name is not None:
58
+ out["tool_name"] = self.tool_name
59
+ return out
60
+
61
+
62
+ @dataclass(frozen=True)
63
+ class ToolCall:
64
+ """A single tool-call request returned inside `LLMResponse.tool_calls`.
65
+
66
+ `id` is the provider-assigned identifier the assistant uses to
67
+ match up its tool_use block with the following tool result; the
68
+ runtime forwards it back as `LLMMessage.tool_use_id`. `name`
69
+ matches the tool's `@tool(name=...)`. `args` is the JSON-shaped
70
+ argument payload.
71
+ """
72
+
73
+ id: str
74
+ name: str
75
+ args: dict[str, Any]
76
+
77
+ def to_dict(self) -> dict[str, Any]:
78
+ return {"id": self.id, "name": self.name, "args": dict(self.args)}
79
+
80
+
81
+ @dataclass
82
+ class LLMResponse:
83
+ raw_text: str
84
+ parsed: Optional[Any]
85
+ input_tokens: int
86
+ output_tokens: int
87
+ cost_usd: Decimal
88
+ latency_seconds: float
89
+ model: str
90
+ finish_reason: str
91
+ seed: Optional[int] = None
92
+ cache_hit: bool = False
93
+ provider_meta: dict[str, Any] = field(default_factory=dict)
94
+ # v0.7: when finish_reason indicates the model wants to call tools,
95
+ # `tool_calls` is non-empty and the runtime enters the turn loop
96
+ # instead of handing `parsed` to the handler.
97
+ tool_calls: Optional[list[ToolCall]] = None
98
+
99
+ def to_dict(self) -> dict[str, Any]:
100
+ return {
101
+ "raw_text": self.raw_text,
102
+ "parsed": _parsed_to_jsonable(self.parsed),
103
+ "input_tokens": int(self.input_tokens),
104
+ "output_tokens": int(self.output_tokens),
105
+ "cost_usd": str(self.cost_usd),
106
+ "latency_seconds": float(self.latency_seconds),
107
+ "model": self.model,
108
+ "finish_reason": self.finish_reason,
109
+ "seed": self.seed,
110
+ "cache_hit": bool(self.cache_hit),
111
+ "provider_meta": dict(self.provider_meta),
112
+ "tool_calls": (
113
+ [tc.to_dict() for tc in self.tool_calls]
114
+ if self.tool_calls
115
+ else None
116
+ ),
117
+ }
118
+
119
+
120
+ def _parsed_to_jsonable(parsed: Any) -> Any:
121
+ """Make a Pydantic model JSON-safe for event payloads."""
122
+ if parsed is None:
123
+ return None
124
+ dump = getattr(parsed, "model_dump", None)
125
+ if callable(dump):
126
+ try:
127
+ return dump(mode="json")
128
+ except TypeError:
129
+ return dump()
130
+ return parsed
@@ -0,0 +1,61 @@
1
+ """Operator-facing observability surface. v0.8.
2
+
3
+ Three pillars, all opt-in:
4
+
5
+ - Structured logging (``configure_logging``) — JSON-line logger setup
6
+ matching the documented schema. Off by default; users with existing
7
+ ``logging`` config keep theirs.
8
+ - Metrics (``Metrics`` protocol + ``NoOpMetrics`` + ``PrometheusMetrics``)
9
+ — three methods, fixed metric names. NoOp is the default.
10
+ - Runtime introspection (``RuntimeStatus``) — frozen snapshot returned
11
+ by ``runtime.status()``.
12
+
13
+ The framework never auto-configures any of these. A library that does
14
+ is hostile to operators who already have their own config.
15
+ """
16
+
17
+ from activegraph.observability.logging import (
18
+ LOG_FIELDS,
19
+ configure_logging,
20
+ get_logger,
21
+ runtime_log_extra,
22
+ )
23
+ from activegraph.observability.metrics import (
24
+ METRIC_NAMES,
25
+ Metrics,
26
+ NoOpMetrics,
27
+ )
28
+ from activegraph.observability.migration import (
29
+ MigrationReport,
30
+ MigrationRunReport,
31
+ migrate,
32
+ )
33
+ from activegraph.observability.prometheus import PrometheusMetrics
34
+ from activegraph.observability.status import (
35
+ BehaviorInfo,
36
+ BudgetSnapshot,
37
+ EventSummary,
38
+ FrameSnapshot,
39
+ RuntimeStatus,
40
+ status_to_dict,
41
+ )
42
+
43
+ __all__ = [
44
+ "BehaviorInfo",
45
+ "BudgetSnapshot",
46
+ "EventSummary",
47
+ "FrameSnapshot",
48
+ "LOG_FIELDS",
49
+ "METRIC_NAMES",
50
+ "Metrics",
51
+ "MigrationReport",
52
+ "MigrationRunReport",
53
+ "NoOpMetrics",
54
+ "PrometheusMetrics",
55
+ "RuntimeStatus",
56
+ "configure_logging",
57
+ "get_logger",
58
+ "migrate",
59
+ "runtime_log_extra",
60
+ "status_to_dict",
61
+ ]