agent-reliability 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. agent_reliability/__init__.py +14 -0
  2. agent_reliability/adapters/__init__.py +31 -0
  3. agent_reliability/adapters/event_sinks.py +82 -0
  4. agent_reliability/adapters/otel/__init__.py +13 -0
  5. agent_reliability/adapters/otel/run_context.py +109 -0
  6. agent_reliability/adapters/system_clock.py +14 -0
  7. agent_reliability/adapters/uuid_run_id_generator.py +15 -0
  8. agent_reliability/application/__init__.py +3 -0
  9. agent_reliability/domain/__init__.py +66 -0
  10. agent_reliability/domain/error_budget.py +153 -0
  11. agent_reliability/domain/evaluation.py +36 -0
  12. agent_reliability/domain/identity.py +43 -0
  13. agent_reliability/domain/runs.py +111 -0
  14. agent_reliability/domain/sli.py +221 -0
  15. agent_reliability/domain/slo.py +152 -0
  16. agent_reliability/evaluation/__init__.py +35 -0
  17. agent_reliability/evaluation/_validation.py +48 -0
  18. agent_reliability/evaluation/builtins.py +68 -0
  19. agent_reliability/evaluation/identity.py +32 -0
  20. agent_reliability/evaluation/protocols.py +46 -0
  21. agent_reliability/evaluation/result.py +110 -0
  22. agent_reliability/experimental/__init__.py +9 -0
  23. agent_reliability/ports/__init__.py +43 -0
  24. agent_reliability/ports/clock.py +26 -0
  25. agent_reliability/ports/event_sink.py +36 -0
  26. agent_reliability/ports/events.py +101 -0
  27. agent_reliability/ports/id_generator.py +24 -0
  28. agent_reliability/ports/run_context.py +28 -0
  29. agent_reliability/py.typed +1 -0
  30. agent_reliability/reliability/__init__.py +21 -0
  31. agent_reliability/reliability/engine.py +156 -0
  32. agent_reliability/reliability/model.py +191 -0
  33. agent_reliability/sdk/__init__.py +57 -0
  34. agent_reliability/sdk/client.py +476 -0
  35. agent_reliability/sdk/context.py +151 -0
  36. agent_reliability/sdk/diagnostics.py +85 -0
  37. agent_reliability/sdk/evaluator_runner.py +165 -0
  38. agent_reliability-1.0.0.dist-info/METADATA +210 -0
  39. agent_reliability-1.0.0.dist-info/RECORD +41 -0
  40. agent_reliability-1.0.0.dist-info/WHEEL +4 -0
  41. agent_reliability-1.0.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,14 @@
1
+ """Agent Reliability SDK.
2
+
3
+ Vendor-neutral reliability primitives for operating autonomous AI agents.
4
+
5
+ See docs/GA_CONTRACT.md and docs/COMPATIBILITY.md for the stability policy.
6
+ Public APIs live in focused subpackages; the root package intentionally
7
+ exposes only the distribution version.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ __version__ = "1.0.0"
13
+
14
+ __all__ = ["__version__"]
@@ -0,0 +1,31 @@
1
+ """Adapters: concrete implementations of ports.
2
+
3
+ The default clock, id generator, and event sinks arrived in M2. M3's optional
4
+ OpenTelemetry adapter lives in ``agent_reliability.adapters.otel`` and is not
5
+ imported here, preserving base-install import safety.
6
+
7
+ This is the only layer permitted to depend on a specific vendor SDK,
8
+ transport, or agent framework. Adapters implement ports; they are never
9
+ imported by ``domain`` or ``application``.
10
+
11
+ The exports of this subpackage are part of the stable 1.0 contract documented
12
+ in docs/GA_CONTRACT.md.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from agent_reliability.adapters.event_sinks import (
18
+ CompositeEventSink,
19
+ InMemoryEventSink,
20
+ NoOpEventSink,
21
+ )
22
+ from agent_reliability.adapters.system_clock import SystemClock
23
+ from agent_reliability.adapters.uuid_run_id_generator import UuidRunIdGenerator
24
+
25
+ __all__ = [
26
+ "CompositeEventSink",
27
+ "InMemoryEventSink",
28
+ "NoOpEventSink",
29
+ "SystemClock",
30
+ "UuidRunIdGenerator",
31
+ ]
@@ -0,0 +1,82 @@
1
+ """Default ``EventSink`` implementations.
2
+
3
+ ``NoOpEventSink`` is what ``AgentReliability()`` uses when no sink is
4
+ given — a library should not silently print to the console by default,
5
+ and doing nothing observable until a caller opts in is the least
6
+ surprising default for production embedding (docs/SDK_DESIGN.md).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import threading
12
+
13
+ from agent_reliability.ports.event_sink import EventSink
14
+ from agent_reliability.ports.events import InstrumentationEvent
15
+
16
+ __all__ = ["CompositeEventSink", "InMemoryEventSink", "NoOpEventSink"]
17
+
18
+
19
+ class NoOpEventSink:
20
+ """Discards every event. The default sink."""
21
+
22
+ def emit(self, event: InstrumentationEvent) -> None:
23
+ return None
24
+
25
+
26
+ class InMemoryEventSink:
27
+ """Appends every event to a list, held in memory for the sink's
28
+ lifetime.
29
+
30
+ Intended for tests and local examples — **not** a production
31
+ default. Unbounded accumulation of every event for the life of the
32
+ process is exactly the kind of memory growth this project's
33
+ engineering principles warn against as a default (docs/SDK_DESIGN.md,
34
+ "Memory safety"). ``events`` is safe to read/append from multiple
35
+ threads (protected by an internal lock); it is still an unbounded,
36
+ ever-growing list, by design, for this sink.
37
+ """
38
+
39
+ def __init__(self) -> None:
40
+ self._lock = threading.Lock()
41
+ self._events: list[InstrumentationEvent] = []
42
+
43
+ def emit(self, event: InstrumentationEvent) -> None:
44
+ with self._lock:
45
+ self._events.append(event)
46
+
47
+ @property
48
+ def events(self) -> list[InstrumentationEvent]:
49
+ """A snapshot copy of the events received so far."""
50
+ with self._lock:
51
+ return list(self._events)
52
+
53
+ def clear(self) -> None:
54
+ with self._lock:
55
+ self._events.clear()
56
+
57
+
58
+ class CompositeEventSink:
59
+ """Fans one event out to multiple sinks.
60
+
61
+ Each child's ``emit`` is called independently; one child raising
62
+ does not stop delivery to the others (the exception is allowed to
63
+ propagate to the SDK's own failure-isolation wrapper around this
64
+ composite's ``emit`` call, which reports it via diagnostics exactly
65
+ as it would for a single misbehaving sink — see
66
+ docs/adr/0004-instrumentation-failure-isolation.md). The *first*
67
+ child to raise is what gets reported; later children still run.
68
+ """
69
+
70
+ def __init__(self, sinks: list[EventSink]) -> None:
71
+ self._sinks = list(sinks)
72
+
73
+ def emit(self, event: InstrumentationEvent) -> None:
74
+ first_error: Exception | None = None
75
+ for sink in self._sinks:
76
+ try:
77
+ sink.emit(event)
78
+ except Exception as exc:
79
+ if first_error is None:
80
+ first_error = exc
81
+ if first_error is not None:
82
+ raise first_error
@@ -0,0 +1,13 @@
1
+ """Optional OpenTelemetry interoperability adapter.
2
+
3
+ Install ``agent-reliability[otel]`` before importing this module. The adapter
4
+ uses the OpenTelemetry API but never configures a provider or exporter.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from agent_reliability.adapters.otel.run_context import (
10
+ OpenTelemetryRunContextBridge,
11
+ )
12
+
13
+ __all__ = ["OpenTelemetryRunContextBridge"]
@@ -0,0 +1,109 @@
1
+ """OpenTelemetry implementation of the run-context bridge port."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from contextlib import AbstractContextManager, suppress
6
+
7
+ from opentelemetry import trace
8
+ from opentelemetry.trace import Span, SpanKind, Status, StatusCode, Tracer
9
+
10
+ from agent_reliability import __version__
11
+ from agent_reliability.domain import AgentRun, RunStatus
12
+ from agent_reliability.ports.run_context import RunContextScope
13
+
14
+ __all__ = ["OpenTelemetryRunContextBridge"]
15
+
16
+ _INSTRUMENTATION_SCOPE_NAME = "agent_reliability"
17
+ _SPAN_NAME = "invoke_agent"
18
+ _SCHEMA_VERSION = "1"
19
+
20
+
21
+ class OpenTelemetryRunContextBridge:
22
+ """Creates a current internal span for each initialized SDK run.
23
+
24
+ Passing a tracer is useful for explicit provider ownership and tests. If
25
+ omitted, the bridge obtains a tracer from the host-configured global
26
+ provider; it never installs or changes that provider.
27
+ """
28
+
29
+ def __init__(self, tracer: Tracer | None = None) -> None:
30
+ if tracer is not None and not isinstance(tracer, Tracer):
31
+ raise TypeError("tracer must implement opentelemetry.trace.Tracer")
32
+ self._tracer = (
33
+ tracer
34
+ if tracer is not None
35
+ else trace.get_tracer(_INSTRUMENTATION_SCOPE_NAME, __version__)
36
+ )
37
+
38
+ def start(self, run: AgentRun) -> RunContextScope:
39
+ attributes: dict[str, str] = {
40
+ "gen_ai.operation.name": "invoke_agent",
41
+ "gen_ai.agent.name": run.agent.name,
42
+ "agent_reliability.schema.version": _SCHEMA_VERSION,
43
+ "agent_reliability.agent.id": run.agent.agent_id,
44
+ "agent_reliability.agent.version": run.agent.version,
45
+ "agent_reliability.run.id": run.run_id,
46
+ }
47
+ if run.agent.environment is not None:
48
+ attributes["agent_reliability.agent.environment"] = run.agent.environment
49
+ if run.parent_run_id is not None:
50
+ attributes["agent_reliability.run.parent_id"] = run.parent_run_id
51
+
52
+ span = self._tracer.start_span(
53
+ _SPAN_NAME, kind=SpanKind.INTERNAL, attributes=attributes
54
+ )
55
+ activation = trace.use_span(
56
+ span,
57
+ end_on_exit=False,
58
+ record_exception=False,
59
+ set_status_on_exception=False,
60
+ )
61
+ try:
62
+ activation.__enter__()
63
+ except BaseException:
64
+ # Preserve the activation failure. There is no active scope for
65
+ # the SDK to finish, and the host runtime owns any additional
66
+ # OpenTelemetry diagnostics.
67
+ with suppress(BaseException):
68
+ span.end()
69
+ raise
70
+ return _OpenTelemetryRunContextScope(span=span, activation=activation)
71
+
72
+
73
+ class _OpenTelemetryRunContextScope:
74
+ def __init__(self, *, span: Span, activation: AbstractContextManager[Span]) -> None:
75
+ self._span = span
76
+ self._activation = activation
77
+ self._closed = False
78
+
79
+ def finish(self, *, status: RunStatus, exception_type: str | None) -> None:
80
+ if self._closed:
81
+ return
82
+
83
+ first_error: BaseException | None = None
84
+ try:
85
+ try:
86
+ self._span.set_attribute("agent_reliability.run.status", status.value)
87
+ if status is RunStatus.FAILED:
88
+ if exception_type is not None:
89
+ self._span.set_attribute("error.type", exception_type)
90
+ self._span.set_status(Status(StatusCode.ERROR))
91
+ except BaseException as exc:
92
+ first_error = exc
93
+
94
+ try:
95
+ self._activation.__exit__(None, None, None)
96
+ except BaseException as exc:
97
+ if first_error is None:
98
+ first_error = exc
99
+
100
+ try:
101
+ self._span.end()
102
+ except BaseException as exc:
103
+ if first_error is None:
104
+ first_error = exc
105
+ finally:
106
+ self._closed = True
107
+
108
+ if first_error is not None:
109
+ raise first_error
@@ -0,0 +1,14 @@
1
+ """The default ``Clock`` implementation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import UTC, datetime
6
+
7
+ __all__ = ["SystemClock"]
8
+
9
+
10
+ class SystemClock:
11
+ """Reads the real system clock, normalized to UTC."""
12
+
13
+ def now(self) -> datetime:
14
+ return datetime.now(UTC)
@@ -0,0 +1,15 @@
1
+ """The default ``RunIdGenerator`` implementation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import uuid
6
+
7
+ __all__ = ["UuidRunIdGenerator"]
8
+
9
+
10
+ class UuidRunIdGenerator:
11
+ """Generates run ids via ``uuid.uuid4()`` — random, not sequential,
12
+ globally-unique-compatible with no coordination required."""
13
+
14
+ def generate(self) -> str:
15
+ return str(uuid.uuid4())
@@ -0,0 +1,3 @@
1
+ """Reserved internal application-layer namespace; exports no public API."""
2
+
3
+ from __future__ import annotations
@@ -0,0 +1,66 @@
1
+ """Domain layer: pure reliability concepts (Run, Evaluation, SLI, SLO,
2
+ Error Budget). See docs/DOMAIN_MODEL.md, docs/SLO_SEMANTICS.md, and
3
+ ADR-0002 for the specification these types implement.
4
+
5
+ Rules for this package, enforced by review (not yet by tooling):
6
+
7
+ - No imports from ``agent_reliability.adapters``.
8
+ - No imports of network, filesystem, or database libraries.
9
+ - No dependency on any specific LLM provider or agent framework.
10
+ - Values are immutable.
11
+
12
+ The exports of this subpackage are part of the stable 1.0 contract documented
13
+ in docs/GA_CONTRACT.md. They are not re-exported from the
14
+ ``agent_reliability`` package root, which exposes only ``__version__``.
15
+
16
+ M4 evaluator execution/provenance lives in the separate
17
+ ``agent_reliability.evaluation`` namespace and depends on this domain, never
18
+ the reverse; see docs/ARCHITECTURE.md.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from agent_reliability.domain.error_budget import (
24
+ BudgetStatus,
25
+ BurnRate,
26
+ ErrorBudget,
27
+ compute_burn_rate,
28
+ compute_error_budget,
29
+ )
30
+ from agent_reliability.domain.evaluation import EvaluationOutcome
31
+ from agent_reliability.domain.identity import AgentIdentity
32
+ from agent_reliability.domain.runs import AgentRun, RunStatus
33
+ from agent_reliability.domain.sli import (
34
+ ObservationCounts,
35
+ RatioResult,
36
+ UnknownPolicy,
37
+ compute_ratio,
38
+ )
39
+ from agent_reliability.domain.slo import (
40
+ ObjectiveDirection,
41
+ Slo,
42
+ SloEvaluation,
43
+ SloStatus,
44
+ evaluate_slo,
45
+ )
46
+
47
+ __all__ = [
48
+ "AgentIdentity",
49
+ "AgentRun",
50
+ "BudgetStatus",
51
+ "BurnRate",
52
+ "ErrorBudget",
53
+ "EvaluationOutcome",
54
+ "ObjectiveDirection",
55
+ "ObservationCounts",
56
+ "RatioResult",
57
+ "RunStatus",
58
+ "Slo",
59
+ "SloEvaluation",
60
+ "SloStatus",
61
+ "UnknownPolicy",
62
+ "compute_burn_rate",
63
+ "compute_error_budget",
64
+ "compute_ratio",
65
+ "evaluate_slo",
66
+ ]
@@ -0,0 +1,153 @@
1
+ """Error budget and burn rate.
2
+
3
+ Both are, mechanically, the same computation —
4
+ ``observed_bad_fraction / allowed_bad_fraction`` — applied to different
5
+ observation windows: a full window for the cumulative error budget, an
6
+ arbitrary shorter lookback for burn rate (docs/SLO_SEMANTICS.md,
7
+ ADR-0002). ``_consumption`` below is the single shared implementation;
8
+ ``compute_error_budget`` and ``compute_burn_rate`` are thin, differently
9
+ -shaped public wrappers around it, so the two can never silently drift
10
+ apart.
11
+
12
+ Three states are distinguished (``BudgetStatus``), because two
13
+ different situations both prevent an ordinary finite number from being
14
+ produced, and they are not the same situation:
15
+
16
+ - ``NO_DATA``: no considered observations at all (division is
17
+ undefined because there is nothing to divide).
18
+ - ``ZERO_TOLERANCE_INTACT`` / ``ZERO_TOLERANCE_EXCEEDED``: data exists,
19
+ but the SLO's target is 100% (``AT_LEAST``) or 0% (``AT_MOST``), so
20
+ ``allowed_bad_fraction == 0``. Zero observed bad events against a
21
+ zero-tolerance budget is well-defined (fully intact); any observed
22
+ bad event makes the true consumption unbounded, which
23
+ ``fractions.Fraction`` cannot represent (it has no infinity) — this
24
+ is reported as its own status with the numeric value left ``None``,
25
+ rather than smuggled in as a ``float('inf')``.
26
+
27
+ See ADR-0002 for the full reasoning and alternatives considered.
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import enum
33
+ from dataclasses import dataclass
34
+ from fractions import Fraction
35
+
36
+ from agent_reliability.domain.sli import RatioResult
37
+ from agent_reliability.domain.slo import Slo
38
+
39
+ __all__ = [
40
+ "BudgetStatus",
41
+ "BurnRate",
42
+ "ErrorBudget",
43
+ "compute_burn_rate",
44
+ "compute_error_budget",
45
+ ]
46
+
47
+
48
+ class BudgetStatus(enum.Enum):
49
+ """Shared status for any bad-fraction-over-allowed-fraction division.
50
+
51
+ Used by both ``ErrorBudget`` (cumulative, full-window) and
52
+ ``BurnRate`` (a lookback window) because they are the same
53
+ underlying division — see module docstring.
54
+ """
55
+
56
+ MEASURED = "measured"
57
+ NO_DATA = "no_data"
58
+ ZERO_TOLERANCE_INTACT = "zero_tolerance_intact"
59
+ ZERO_TOLERANCE_EXCEEDED = "zero_tolerance_exceeded"
60
+
61
+
62
+ def _consumption(
63
+ allowed_bad_fraction: Fraction, ratio: RatioResult
64
+ ) -> tuple[BudgetStatus, Fraction | None]:
65
+ """The single shared computation behind both public functions below.
66
+
67
+ Returns ``(status, value)`` where ``value`` is
68
+ ``observed_bad_fraction / allowed_bad_fraction`` when that division
69
+ is well-defined, and ``None`` otherwise (with ``status`` explaining
70
+ why — see module docstring).
71
+ """
72
+ observed_bad_fraction = ratio.fail_ratio
73
+ if observed_bad_fraction is None:
74
+ return BudgetStatus.NO_DATA, None
75
+ if allowed_bad_fraction == 0:
76
+ if observed_bad_fraction == 0:
77
+ return BudgetStatus.ZERO_TOLERANCE_INTACT, Fraction(0)
78
+ return BudgetStatus.ZERO_TOLERANCE_EXCEEDED, None
79
+ return BudgetStatus.MEASURED, observed_bad_fraction / allowed_bad_fraction
80
+
81
+
82
+ @dataclass(frozen=True)
83
+ class ErrorBudget:
84
+ """The error budget implied by an ``Slo`` over one observation window.
85
+
86
+ ``allowed_bad_events`` may be non-integral (e.g. ``4.995`` for a
87
+ 0.5% allowance over 999 considered events) — this is kept exact via
88
+ ``Fraction`` rather than rounded, per docs/SLO_SEMANTICS.md.
89
+ ``observed_bad_events`` is always a plain, exact integer count (it is
90
+ never reconstructed by multiplying a ratio back out).
91
+
92
+ ``consumption_ratio`` and ``remaining_fraction`` are ``None`` iff
93
+ ``status`` is ``NO_DATA`` or ``ZERO_TOLERANCE_EXCEEDED``.
94
+ ``remaining_fraction`` is not clamped to ``[0, 1]``: a negative value
95
+ means the budget is exhausted and exceeded, and the magnitude is the
96
+ size of the breach (docs/SLO_SEMANTICS.md).
97
+ """
98
+
99
+ slo: Slo
100
+ ratio: RatioResult
101
+ status: BudgetStatus
102
+ allowed_bad_fraction: Fraction
103
+ allowed_bad_events: Fraction
104
+ observed_bad_events: int
105
+ consumption_ratio: Fraction | None
106
+ remaining_fraction: Fraction | None
107
+
108
+
109
+ def compute_error_budget(slo: Slo, ratio: RatioResult) -> ErrorBudget:
110
+ """Compute the error budget implied by ``slo`` over ``ratio``'s window."""
111
+ allowed_bad_fraction = slo.allowed_bad_fraction
112
+ status, consumption_ratio = _consumption(allowed_bad_fraction, ratio)
113
+ remaining_fraction = (
114
+ None if consumption_ratio is None else Fraction(1) - consumption_ratio
115
+ )
116
+ return ErrorBudget(
117
+ slo=slo,
118
+ ratio=ratio,
119
+ status=status,
120
+ allowed_bad_fraction=allowed_bad_fraction,
121
+ allowed_bad_events=allowed_bad_fraction * ratio.considered_count,
122
+ observed_bad_events=ratio.considered_fail_count,
123
+ consumption_ratio=consumption_ratio,
124
+ remaining_fraction=remaining_fraction,
125
+ )
126
+
127
+
128
+ @dataclass(frozen=True)
129
+ class BurnRate:
130
+ """How fast a budget is being consumed over one (typically short)
131
+ lookback window, relative to the rate that would exactly exhaust it
132
+ over the SLO's full observation window.
133
+
134
+ ``value`` of ``1`` means consuming budget at exactly the sustainable
135
+ rate; ``> 1`` means faster than sustainable. ``None`` iff ``status``
136
+ is ``NO_DATA`` or ``ZERO_TOLERANCE_EXCEEDED`` — see module docstring.
137
+
138
+ ``ratio`` represents whatever lookback period the caller chose; this
139
+ function has no notion of window length or time itself (see
140
+ docs/SLO_SEMANTICS.md — multi-window/time-aware alerting is deferred).
141
+ """
142
+
143
+ slo: Slo
144
+ ratio: RatioResult
145
+ status: BudgetStatus
146
+ value: Fraction | None
147
+
148
+
149
+ def compute_burn_rate(slo: Slo, ratio: RatioResult) -> BurnRate:
150
+ """Compute the burn rate implied by ``slo`` over ``ratio``'s (lookback)
151
+ window."""
152
+ status, value = _consumption(slo.allowed_bad_fraction, ratio)
153
+ return BurnRate(slo=slo, ratio=ratio, status=status, value=value)
@@ -0,0 +1,36 @@
1
+ """Evaluation outcomes.
2
+
3
+ An ``EvaluationOutcome`` is the categorical result of assessing some
4
+ property of an agent run (task success, correctness, policy compliance,
5
+ ...). It is deliberately a three-valued enum, not a ``bool``: missing or
6
+ inconclusive evidence (``UNKNOWN``) is a distinct, first-class outcome,
7
+ never collapsed into ``PASS`` or ``FAIL`` and never represented as
8
+ ``None`` (see docs/DOMAIN_MODEL.md).
9
+
10
+ This module intentionally does not define the richer M4 ``EvaluationResult``
11
+ or a separate "reliability observation" wrapper type. For the ratio
12
+ mathematics in ``sli.py``, an eligible observation *is* its outcome — see
13
+ ADR-0002. Evaluator execution and provenance live one layer above in
14
+ ``agent_reliability.evaluation``; the mathematical domain does not depend on
15
+ them.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import enum
21
+
22
+ __all__ = ["EvaluationOutcome"]
23
+
24
+
25
+ class EvaluationOutcome(enum.Enum):
26
+ """The categorical result of one evaluation.
27
+
28
+ ``PASS`` and ``FAIL`` are never inferred from a quantitative score;
29
+ thresholding a score into one of these outcomes is evaluator/policy
30
+ behavior that lives outside this domain kernel (see
31
+ docs/DOMAIN_MODEL.md, "Evaluation score" vs. "Evaluation result").
32
+ """
33
+
34
+ PASS = "pass"
35
+ FAIL = "fail"
36
+ UNKNOWN = "unknown"
@@ -0,0 +1,43 @@
1
+ """Agent identity.
2
+
3
+ Deliberately minimal: models only what M1 needs, per docs/DOMAIN_MODEL.md.
4
+ The library intentionally makes no cross-version identity inference:
5
+ ``AgentIdentity`` has ordinary structural (dataclass) equality only.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+
12
+ __all__ = ["AgentIdentity"]
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class AgentIdentity:
17
+ """Identifies what agent, at what version, produced a run.
18
+
19
+ ``agent_id``, ``name``, and ``version`` are required: without a
20
+ version an agent's runs cannot be attributed to a specific build for
21
+ reliability comparison, and without a stable ``agent_id`` there is
22
+ no key to compare versions of "the same" agent against later.
23
+
24
+ ``environment`` is optional metadata (e.g. "production", "staging").
25
+ It intentionally has no default — a missing value stays missing
26
+ rather than silently defaulting to a guess like "production", which
27
+ could mislabel data whose environment the caller genuinely did not
28
+ specify. Whether/how ``environment`` should scope or filter an SLI's
29
+ population of runs is not decided at M1.
30
+ """
31
+
32
+ agent_id: str
33
+ name: str
34
+ version: str
35
+ environment: str | None = None
36
+
37
+ def __post_init__(self) -> None:
38
+ if not self.agent_id:
39
+ raise ValueError("AgentIdentity.agent_id must not be empty")
40
+ if not self.name:
41
+ raise ValueError("AgentIdentity.name must not be empty")
42
+ if not self.version:
43
+ raise ValueError("AgentIdentity.version must not be empty")