agentloop-profiler 0.1.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 (166) hide show
  1. agentloop/__init__.py +136 -0
  2. agentloop/ablation_protocol.py +229 -0
  3. agentloop/ablations.py +564 -0
  4. agentloop/aggregate_cli.py +88 -0
  5. agentloop/aggregate_stats.py +175 -0
  6. agentloop/aggregates.py +532 -0
  7. agentloop/audit.py +39 -0
  8. agentloop/autoinstrument.py +145 -0
  9. agentloop/batch_experiments.py +295 -0
  10. agentloop/batching.py +95 -0
  11. agentloop/budget_types.py +335 -0
  12. agentloop/budgets.py +539 -0
  13. agentloop/calibration.py +872 -0
  14. agentloop/checkpoint_state.py +306 -0
  15. agentloop/checkpoint_store.py +86 -0
  16. agentloop/ci.py +202 -0
  17. agentloop/cli.py +1314 -0
  18. agentloop/client.py +228 -0
  19. agentloop/completion.py +345 -0
  20. agentloop/completion_types.py +199 -0
  21. agentloop/config.py +50 -0
  22. agentloop/context_controls.py +443 -0
  23. agentloop/context_experiments.py +337 -0
  24. agentloop/context_types.py +216 -0
  25. agentloop/costs.py +667 -0
  26. agentloop/cursor_validation.py +48 -0
  27. agentloop/decorators.py +445 -0
  28. agentloop/demo.py +300 -0
  29. agentloop/doctor.py +341 -0
  30. agentloop/drift.py +361 -0
  31. agentloop/drift_cli.py +147 -0
  32. agentloop/drift_types.py +110 -0
  33. agentloop/drift_windows.py +340 -0
  34. agentloop/entrypoint.py +201 -0
  35. agentloop/estimates.py +133 -0
  36. agentloop/events.py +75 -0
  37. agentloop/experiment_artifacts.py +80 -0
  38. agentloop/experiment_observations.py +169 -0
  39. agentloop/experiment_reports.py +545 -0
  40. agentloop/experiment_types.py +355 -0
  41. agentloop/experiments.py +392 -0
  42. agentloop/exporters.py +117 -0
  43. agentloop/finding_benchmark_types.py +131 -0
  44. agentloop/finding_benchmarks.py +424 -0
  45. agentloop/findings.py +419 -0
  46. agentloop/graph.py +464 -0
  47. agentloop/harness.py +825 -0
  48. agentloop/harness_bench/__init__.py +1 -0
  49. agentloop/harness_bench/cli.py +77 -0
  50. agentloop/harness_bench/drivers.py +96 -0
  51. agentloop/harness_bench/offline.py +507 -0
  52. agentloop/harness_evidence.py +585 -0
  53. agentloop/harness_usage.py +91 -0
  54. agentloop/html_report.py +860 -0
  55. agentloop/integrations/__init__.py +1 -0
  56. agentloop/integrations/crewai.py +144 -0
  57. agentloop/integrations/harbor/__init__.py +1 -0
  58. agentloop/integrations/harbor/atif.py +982 -0
  59. agentloop/integrations/harbor/atif_validation.py +329 -0
  60. agentloop/integrations/harbor/cli.py +207 -0
  61. agentloop/integrations/harbor/manifest.py +86 -0
  62. agentloop/integrations/harbor/trial_evidence.py +196 -0
  63. agentloop/integrations/harbor/trials.py +1109 -0
  64. agentloop/integrations/harbor/verifier.py +118 -0
  65. agentloop/integrations/langgraph.py +150 -0
  66. agentloop/integrations/langgraph_harness.py +466 -0
  67. agentloop/integrations/langgraph_recovery.py +366 -0
  68. agentloop/integrations/omnigent/__init__.py +1 -0
  69. agentloop/integrations/omnigent/cli.py +78 -0
  70. agentloop/integrations/omnigent/evidence.py +93 -0
  71. agentloop/integrations/omnigent/telemetry.py +422 -0
  72. agentloop/integrations/openai.py +415 -0
  73. agentloop/integrations/openai_agents.py +291 -0
  74. agentloop/integrations/sdk.py +82 -0
  75. agentloop/integrations/vercel_ai.py +83 -0
  76. agentloop/interoperability/__init__.py +5 -0
  77. agentloop/interoperability/artifacts.py +112 -0
  78. agentloop/interoperability/cli.py +59 -0
  79. agentloop/interoperability/cohort_evidence.py +175 -0
  80. agentloop/interoperability/cohorts.py +681 -0
  81. agentloop/interoperability/contracts.py +427 -0
  82. agentloop/interoperability/coordination.py +554 -0
  83. agentloop/interoperability/evidence.py +215 -0
  84. agentloop/interoperability/otlp_jsonl.py +775 -0
  85. agentloop/interoperability/otlp_validation.py +496 -0
  86. agentloop/interoperability/registry.py +561 -0
  87. agentloop/interoperability/validation.py +333 -0
  88. agentloop/intervention_service.py +110 -0
  89. agentloop/intervention_store.py +118 -0
  90. agentloop/interventions.py +190 -0
  91. agentloop/issues.py +90 -0
  92. agentloop/judgment_adapters.py +125 -0
  93. agentloop/judgment_benchmark_types.py +98 -0
  94. agentloop/judgment_benchmarks.py +606 -0
  95. agentloop/judgment_types.py +241 -0
  96. agentloop/judgment_views.py +38 -0
  97. agentloop/judgments.py +342 -0
  98. agentloop/loop_guards.py +274 -0
  99. agentloop/loop_types.py +66 -0
  100. agentloop/markdown.py +102 -0
  101. agentloop/metrics.py +342 -0
  102. agentloop/migrations.py +303 -0
  103. agentloop/model_routing.py +470 -0
  104. agentloop/onboarding.py +315 -0
  105. agentloop/operations.py +44 -0
  106. agentloop/optimizer.py +199 -0
  107. agentloop/otel.py +715 -0
  108. agentloop/otel_ids.py +81 -0
  109. agentloop/otel_semantics.py +134 -0
  110. agentloop/parallelism.py +141 -0
  111. agentloop/patches.py +603 -0
  112. agentloop/plan_export.py +126 -0
  113. agentloop/policy_promotion.py +325 -0
  114. agentloop/promotion_evidence.py +304 -0
  115. agentloop/promotion_types.py +209 -0
  116. agentloop/quality.py +446 -0
  117. agentloop/ranking.py +529 -0
  118. agentloop/recovery_evidence.py +96 -0
  119. agentloop/replay.py +753 -0
  120. agentloop/retention.py +375 -0
  121. agentloop/retention_types.py +113 -0
  122. agentloop/reuse_experiments.py +247 -0
  123. agentloop/routing_checks.py +116 -0
  124. agentloop/routing_types.py +241 -0
  125. agentloop/rules.py +396 -0
  126. agentloop/runtime.py +286 -0
  127. agentloop/savings.py +158 -0
  128. agentloop/scheduling_plan.py +99 -0
  129. agentloop/scheduling_types.py +169 -0
  130. agentloop/schema.py +257 -0
  131. agentloop/semantic_rules.py +200 -0
  132. agentloop/semantic_waste.py +345 -0
  133. agentloop/semantic_waste_types.py +88 -0
  134. agentloop/server.py +494 -0
  135. agentloop/store.py +1429 -0
  136. agentloop/structural_experiments.py +431 -0
  137. agentloop/structural_types.py +113 -0
  138. agentloop/structured_quality.py +1020 -0
  139. agentloop/studies.py +629 -0
  140. agentloop/study_statistics.py +58 -0
  141. agentloop/substitution_evidence.py +199 -0
  142. agentloop/substitution_export.py +190 -0
  143. agentloop/substitution_reports.py +407 -0
  144. agentloop/substitution_types.py +284 -0
  145. agentloop/substitutions.py +267 -0
  146. agentloop/timing.py +142 -0
  147. agentloop/tokens.py +188 -0
  148. agentloop/tool_scheduling.py +410 -0
  149. agentloop/trace_sources.py +31 -0
  150. agentloop/tracer.py +494 -0
  151. agentloop/value.py +306 -0
  152. agentloop/version.py +3 -0
  153. agentloop/workflow_types.py +264 -0
  154. agentloop/workflows.py +284 -0
  155. agentloop_profiler-0.1.0.dist-info/METADATA +513 -0
  156. agentloop_profiler-0.1.0.dist-info/RECORD +166 -0
  157. agentloop_profiler-0.1.0.dist-info/WHEEL +5 -0
  158. agentloop_profiler-0.1.0.dist-info/entry_points.txt +2 -0
  159. agentloop_profiler-0.1.0.dist-info/licenses/LICENSE +201 -0
  160. agentloop_profiler-0.1.0.dist-info/licenses/THIRD_PARTY_LICENSES.md +101 -0
  161. agentloop_profiler-0.1.0.dist-info/top_level.txt +2 -0
  162. dashboard/__init__.py +5 -0
  163. dashboard/app.py +922 -0
  164. dashboard/streamlit_compat.py +63 -0
  165. dashboard/trace_upload.py +57 -0
  166. dashboard/value_view.py +105 -0
@@ -0,0 +1,190 @@
1
+ """Immutable evidence linking finding predictions to an observed replay."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ from dataclasses import dataclass
8
+ from typing import Any
9
+
10
+ from agentloop.findings import build_diagnosis
11
+ from agentloop.harness_evidence import HarnessEvidenceError, comparison_evidence
12
+ from agentloop.replay import ReplayGates, build_replay_report
13
+
14
+ INTERVENTION_SCHEMA_VERSION = "1.0"
15
+ _IDENTITY_FIELDS = (
16
+ "schema_version",
17
+ "baseline_run_id",
18
+ "candidate_run_id",
19
+ "target_finding_ids",
20
+ "intervention_type",
21
+ "configuration",
22
+ )
23
+
24
+
25
+ class InterventionValidationError(ValueError):
26
+ """An intervention does not satisfy the evidence contract."""
27
+
28
+
29
+ class InterventionReferenceError(LookupError):
30
+ """A required trace or baseline finding is not available in this project."""
31
+
32
+
33
+ class InterventionConflictError(ValueError):
34
+ """The same intervention identity already has different evidence."""
35
+
36
+
37
+ def canonical_json(value: Any) -> str:
38
+ try:
39
+ return json.dumps(value, sort_keys=True, separators=(",", ":"), allow_nan=False)
40
+ except (TypeError, ValueError) as exc:
41
+ raise InterventionValidationError("evidence must contain finite JSON values") from exc
42
+
43
+
44
+ def trace_fingerprint(trace: Any) -> str:
45
+ return hashlib.sha256(canonical_json(trace.to_dict()).encode("utf-8")).hexdigest()
46
+
47
+
48
+ def _identity(payload: dict[str, Any]) -> str:
49
+ identity = {key: payload[key] for key in _IDENTITY_FIELDS}
50
+ return "int_" + hashlib.sha256(canonical_json(identity).encode("utf-8")).hexdigest()
51
+
52
+
53
+ @dataclass(frozen=True)
54
+ class InterventionRecord:
55
+ """Canonical JSON owns the snapshot; callers receive independent copies."""
56
+
57
+ _json: str
58
+
59
+ @property
60
+ def intervention_id(self) -> str:
61
+ return self.to_dict()["intervention_id"]
62
+
63
+ def to_dict(self) -> dict[str, Any]:
64
+ return json.loads(self._json)
65
+
66
+ @classmethod
67
+ def from_dict(cls, payload: dict[str, Any]) -> InterventionRecord:
68
+ if (
69
+ not isinstance(payload, dict)
70
+ or payload.get("schema_version") != INTERVENTION_SCHEMA_VERSION
71
+ ):
72
+ raise InterventionValidationError("unsupported intervention schema_version")
73
+ for key in ("baseline_run_id", "candidate_run_id", "intervention_type"):
74
+ if not isinstance(payload.get(key), str) or not payload[key].strip():
75
+ raise InterventionValidationError(f"{key} must be a nonempty string")
76
+ if payload["baseline_run_id"] == payload["candidate_run_id"]:
77
+ raise InterventionValidationError("baseline and candidate must have distinct run IDs")
78
+ targets = payload.get("target_finding_ids")
79
+ if (
80
+ not isinstance(targets, list)
81
+ or not targets
82
+ or any(not isinstance(item, str) or not item for item in targets)
83
+ ):
84
+ raise InterventionValidationError("target_finding_ids must be a nonempty list of IDs")
85
+ if targets != sorted(set(targets)):
86
+ raise InterventionValidationError("target_finding_ids must be sorted and unique")
87
+ for key in ("configuration", "metadata", "predicted", "measured", "trace_fingerprints"):
88
+ if not isinstance(payload.get(key), dict):
89
+ raise InterventionValidationError(f"{key} must be an object")
90
+ findings = payload["predicted"].get("findings")
91
+ if not isinstance(findings, list) or any(not isinstance(item, dict) for item in findings):
92
+ raise InterventionValidationError("predicted.findings must contain finding snapshots")
93
+ if [item.get("finding_id") for item in findings] != targets:
94
+ raise InterventionValidationError("prediction snapshots must match target finding IDs")
95
+ measured = payload["measured"]
96
+ for condition in ("baseline", "candidate"):
97
+ if (
98
+ not isinstance(measured.get(condition), dict)
99
+ or measured[condition].get("run_id") != payload[f"{condition}_run_id"]
100
+ ):
101
+ raise InterventionValidationError("replay run IDs must match the intervention")
102
+ fingerprint = payload["trace_fingerprints"].get(condition)
103
+ if (
104
+ not isinstance(fingerprint, str)
105
+ or len(fingerprint) != 64
106
+ or any(char not in "0123456789abcdef" for char in fingerprint)
107
+ ):
108
+ raise InterventionValidationError("trace fingerprints must be SHA-256 hex digests")
109
+ if not isinstance(measured.get("deltas"), dict) or not isinstance(
110
+ measured.get("gates"), dict
111
+ ):
112
+ raise InterventionValidationError("measured must retain replay deltas and gates")
113
+ if (
114
+ type(payload.get("gates_passed")) is not bool
115
+ or measured["gates"].get("passed") is not payload["gates_passed"]
116
+ ):
117
+ raise InterventionValidationError("gates_passed must match the replay decision")
118
+ if payload.get("intervention_id") != _identity(payload):
119
+ raise InterventionValidationError("intervention_id does not match its identity fields")
120
+ return cls(canonical_json(payload))
121
+
122
+
123
+ def build_intervention(
124
+ baseline: Any,
125
+ candidate: Any,
126
+ *,
127
+ target_finding_ids: list[str],
128
+ intervention_type: str,
129
+ configuration: dict[str, Any] | None = None,
130
+ metadata: dict[str, Any] | None = None,
131
+ diagnosis: dict[str, Any] | None = None,
132
+ gates: ReplayGates | None = None,
133
+ quality_report: dict[str, Any] | None = None,
134
+ replay_report: dict[str, Any] | None = None,
135
+ ) -> InterventionRecord:
136
+ """Capture prediction snapshots, then attach the observed pair comparison."""
137
+ if (
138
+ not isinstance(target_finding_ids, list)
139
+ or not target_finding_ids
140
+ or any(not isinstance(item, str) or not item for item in target_finding_ids)
141
+ ):
142
+ raise InterventionValidationError("at least one target finding ID is required")
143
+ diagnosis = build_diagnosis(baseline) if diagnosis is None else diagnosis
144
+ if diagnosis.get("run_id") != baseline.run_id:
145
+ raise InterventionValidationError("diagnosis must belong to the baseline run")
146
+ targets = sorted(set(target_finding_ids))
147
+ available = {finding["finding_id"]: finding for finding in diagnosis.get("findings", [])}
148
+ if any(target not in available for target in targets):
149
+ raise InterventionReferenceError("target baseline finding not found")
150
+ replay = (
151
+ replay_report
152
+ if replay_report is not None
153
+ else build_replay_report(
154
+ baseline,
155
+ candidate,
156
+ gates=gates,
157
+ quality_report=quality_report,
158
+ )
159
+ )
160
+ if metadata is not None and not isinstance(metadata, dict):
161
+ raise InterventionValidationError("metadata must be an object")
162
+ record_metadata = dict(metadata or {})
163
+ if "agentloop.harness_evidence" in record_metadata:
164
+ raise InterventionValidationError(
165
+ "agentloop.harness_evidence is derived from the compared traces"
166
+ )
167
+ try:
168
+ harness_evidence = comparison_evidence(baseline, candidate)
169
+ except HarnessEvidenceError:
170
+ raise InterventionValidationError("invalid harness evidence in compared traces") from None
171
+ if harness_evidence is not None:
172
+ record_metadata["agentloop.harness_evidence"] = harness_evidence
173
+ payload = {
174
+ "schema_version": INTERVENTION_SCHEMA_VERSION,
175
+ "baseline_run_id": baseline.run_id,
176
+ "candidate_run_id": candidate.run_id,
177
+ "target_finding_ids": targets,
178
+ "intervention_type": intervention_type,
179
+ "configuration": {} if configuration is None else configuration,
180
+ "predicted": {"findings": [available[target] for target in targets]},
181
+ "measured": replay,
182
+ "gates_passed": replay["gates"]["passed"],
183
+ "trace_fingerprints": {
184
+ "baseline": trace_fingerprint(baseline),
185
+ "candidate": trace_fingerprint(candidate),
186
+ },
187
+ "metadata": record_metadata,
188
+ }
189
+ payload["intervention_id"] = _identity(payload)
190
+ return InterventionRecord.from_dict(payload)
agentloop/issues.py ADDED
@@ -0,0 +1,90 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from agentloop.costs import format_cost_usd
6
+ from agentloop.markdown import markdown_code_span, markdown_heading, markdown_text
7
+ from agentloop.timing import format_duration_ms
8
+
9
+ # Generated drafts target this repository, so use only deliberately maintained
10
+ # labels that already exist. Finding type and severity remain structured fields
11
+ # in the draft body instead of creating an unbounded dynamic label taxonomy.
12
+ ISSUE_DRAFT_LABELS = ("enhancement",)
13
+
14
+
15
+ def build_issue_drafts(queue: list[dict[str, Any]], *, limit: int = 5) -> list[dict[str, Any]]:
16
+ drafts = []
17
+ for item in queue:
18
+ if item.get("patchable_count", 0) <= 0:
19
+ continue
20
+ drafts.append(_issue_for_queue_item(item))
21
+ if len(drafts) >= limit:
22
+ break
23
+ return drafts
24
+
25
+
26
+ def issue_drafts_to_markdown(drafts: list[dict[str, Any]]) -> str:
27
+ lines = ["# AgentLoop GitHub Issue Drafts", ""]
28
+ if not drafts:
29
+ lines.append("No patchable optimization queue items found.")
30
+ return "\n".join(lines).rstrip() + "\n"
31
+
32
+ for draft in drafts:
33
+ lines.extend(
34
+ [
35
+ f"## {markdown_heading(draft['title'])}",
36
+ "",
37
+ f"- Labels: {markdown_text(', '.join(map(str, draft['labels'])))}",
38
+ "",
39
+ draft["body"],
40
+ "",
41
+ ]
42
+ )
43
+ return "\n".join(lines).rstrip() + "\n"
44
+
45
+
46
+ def _issue_for_queue_item(item: dict[str, Any]) -> dict[str, Any]:
47
+ title = f"Optimize agent workflow: {item['title']}"
48
+ labels = list(ISSUE_DRAFT_LABELS)
49
+ body = "\n".join(
50
+ [
51
+ "AgentLoop detected a recurring patchable optimization opportunity.",
52
+ "",
53
+ "## Finding",
54
+ "",
55
+ f"- Type: {markdown_code_span(item['type'])}",
56
+ f"- Severity: {markdown_code_span(item['severity'])}",
57
+ f"- Occurrences: {item['occurrence_count']}",
58
+ f"- Affected runs: {item['run_count']}",
59
+ f"- Patchable findings: {item['patchable_count']}",
60
+ f"- Quality risk: {markdown_code_span(item.get('quality_risk', 'unknown'))}",
61
+ f"- Requires scorer: {item.get('requires_scorer', True)}",
62
+ f"- Safe to auto-patch: {item.get('safe_to_auto_patch', False)}",
63
+ f"- Estimated latency savings: {format_duration_ms(item['estimated_latency_savings_ms'])}",
64
+ "- Estimated cost savings: " + format_cost_usd(item.get("estimated_cost_savings_usd")),
65
+ f"- Priority score: {item['priority_score']:.1f}",
66
+ "",
67
+ "## Affected Runs",
68
+ "",
69
+ "\n".join(
70
+ f"- {markdown_code_span(run_id)}" for run_id in item.get("affected_runs", [])[:20]
71
+ ),
72
+ "",
73
+ "## Acceptance Criteria",
74
+ "",
75
+ "- Generate or inspect the AgentLoop patch plan for one affected run.",
76
+ "- Apply a constrained workflow rewrite.",
77
+ "- Run `agentloop replay` against baseline and candidate traces.",
78
+ "- Attach a quality fixture or scorer when quality risk is medium or high.",
79
+ "- Run `agentloop ci` and confirm cost, latency, retry, schema, and quality gates pass.",
80
+ ]
81
+ )
82
+ return {
83
+ "title": title,
84
+ "body": body,
85
+ "labels": labels,
86
+ "queue_id": item["queue_id"],
87
+ "type": item["type"],
88
+ "severity": item["severity"],
89
+ "priority_score": item["priority_score"],
90
+ }
@@ -0,0 +1,125 @@
1
+ """Replaceable local prediction and typed-service bridges for judgment contract 1.0."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ import json
7
+ from dataclasses import dataclass
8
+ from typing import Callable
9
+
10
+ from agentloop.judgment_types import (
11
+ JudgeIdentity,
12
+ JudgeUsage,
13
+ JudgmentAnswer,
14
+ JudgmentUncertainty,
15
+ canonical,
16
+ )
17
+
18
+
19
+ def _callback(value, name):
20
+ if (
21
+ not callable(value)
22
+ or inspect.iscoroutinefunction(value)
23
+ or inspect.isasyncgenfunction(value)
24
+ ):
25
+ raise ValueError(f"{name} must be a synchronous callable")
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class LocalPredictorJudge:
30
+ """Adapt a local scalar predictor without assuming its model framework or labels.
31
+
32
+ The predictor accepts a JudgmentRequest and keyword timeout_s. The session
33
+ validates its output against the request's type/domain. Usage is caller-owned
34
+ metadata; no zero token/cost claim is inferred from the word 'local'.
35
+ """
36
+
37
+ identity: JudgeIdentity
38
+ predict: Callable
39
+ usage: JudgeUsage = JudgeUsage()
40
+
41
+ def __post_init__(self):
42
+ if type(self.identity) is not JudgeIdentity or type(self.usage) is not JudgeUsage:
43
+ raise ValueError("predictor requires a typed identity and usage")
44
+ _callback(self.predict, "predict")
45
+
46
+ def judge(self, request, *, timeout_s=None):
47
+ value = self.predict(request, timeout_s=timeout_s)
48
+ if type(value) is JudgmentAnswer:
49
+ return value
50
+ if inspect.iscoroutine(value) or inspect.isgenerator(value):
51
+ value.close()
52
+ raise ValueError("predictors must return a scalar, not deferred work")
53
+ return JudgmentAnswer(value, usage=self.usage)
54
+
55
+
56
+ def parse_typed_answer(response):
57
+ """Accept a finite structured response, never free-form prose or extra fields."""
58
+ if not isinstance(response, dict) or set(response) - {
59
+ "value",
60
+ "status",
61
+ "reason",
62
+ "usage",
63
+ "uncertainty",
64
+ }:
65
+ raise ValueError("service response must be a typed answer object")
66
+ encoded = canonical(response)
67
+ if len(encoded.encode("utf-8")) > 65536:
68
+ raise ValueError("service answer exceeds the 64 KiB contract limit")
69
+ owned = json.loads(encoded)
70
+ return JudgmentAnswer(
71
+ value=owned.get("value"),
72
+ status=owned.get("status", "known"),
73
+ reason=owned.get("reason"),
74
+ usage=JudgeUsage(**owned.get("usage", {})),
75
+ uncertainty=JudgmentUncertainty(**owned.get("uncertainty", {})),
76
+ )
77
+
78
+
79
+ @dataclass(frozen=True)
80
+ class TypedServiceJudge:
81
+ """Adapt an optional host-owned transport using AgentLoop's structured schema.
82
+
83
+ Credentials and client SDKs belong to the host transport. This adapter never
84
+ reads environment secrets, imports a provider, or opens a network connection
85
+ itself. A configured transport may perform remote work only during explicit
86
+ session execution. The transport owns connection/response byte limits and
87
+ cooperative cancellation before returning a decoded JSON object.
88
+ """
89
+
90
+ identity: JudgeIdentity
91
+ transport: Callable | None = None
92
+ credentials_required: bool = True
93
+ credentials_available: bool = False
94
+
95
+ def __post_init__(self):
96
+ if type(self.identity) is not JudgeIdentity:
97
+ raise ValueError("service requires JudgeIdentity")
98
+ if (
99
+ type(self.credentials_required) is not bool
100
+ or type(self.credentials_available) is not bool
101
+ ):
102
+ raise ValueError("credential availability must be a boolean declaration")
103
+ if self.transport is not None:
104
+ _callback(self.transport, "transport")
105
+
106
+ @property
107
+ def availability(self):
108
+ if self.transport is None:
109
+ return "not_configured"
110
+ if self.credentials_required and not self.credentials_available:
111
+ return "missing_credentials"
112
+ return "ready"
113
+
114
+ def judge(self, request, *, timeout_s=None):
115
+ if self.availability != "ready":
116
+ return JudgmentAnswer(
117
+ status="unknown",
118
+ reason="abstained",
119
+ usage=JudgeUsage(0, 0, 0, "not_dispatched", "not_dispatched"),
120
+ )
121
+ response = self.transport(request.to_dict(), timeout_s=timeout_s)
122
+ if inspect.iscoroutine(response) or inspect.isgenerator(response):
123
+ response.close()
124
+ raise ValueError("transport must return a decoded JSON answer")
125
+ return parse_typed_answer(response)
@@ -0,0 +1,98 @@
1
+ """Frozen examples and execution declarations for backend comparisons."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from dataclasses import asdict, dataclass
7
+ from typing import Any
8
+
9
+ from agentloop.judgment_types import JudgmentRequest, canonical, fingerprint, finite, text
10
+
11
+ BENCHMARK_VERSION = "1.0"
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class JudgmentExample:
16
+ example_id: str
17
+ request: JudgmentRequest
18
+ expected: Any = None
19
+ label_status: str = "unlabelled"
20
+ label_ref: str | None = None
21
+ label_version: str | None = None
22
+
23
+ def __post_init__(self):
24
+ text(self.example_id, "example_id")
25
+ if type(self.request) is not JudgmentRequest:
26
+ raise ValueError("example requires JudgmentRequest")
27
+ if self.label_status not in {"independent", "unlabelled"}:
28
+ raise ValueError("labels must be independently sourced or unavailable")
29
+ if self.label_status == "independent":
30
+ text(self.label_ref, "label_ref")
31
+ text(self.label_version, "label_version")
32
+ valid = (
33
+ type(self.expected) is bool
34
+ if self.request.spec.kind == "probability"
35
+ else self.request.spec.accepts(self.expected)
36
+ )
37
+ if not valid:
38
+ raise ValueError("reference label violates the example's type/domain")
39
+ elif (
40
+ self.expected is not None
41
+ or self.label_ref is not None
42
+ or self.label_version is not None
43
+ ):
44
+ raise ValueError("unlabelled examples cannot carry an expected answer")
45
+
46
+ def to_dict(self):
47
+ return json.loads(canonical(asdict(self)))
48
+
49
+ @classmethod
50
+ def from_dict(cls, value):
51
+ owned = dict(value)
52
+ owned["request"] = JudgmentRequest.from_dict(owned["request"])
53
+ return cls(**owned)
54
+
55
+
56
+ @dataclass(frozen=True)
57
+ class JudgmentBenchmark:
58
+ name: str
59
+ version: str
60
+ examples: tuple[JudgmentExample, ...]
61
+ repetitions: int = 1
62
+ seed: int = 0
63
+ timeout_s: float | None = None
64
+ synthetic: bool = False
65
+
66
+ def __post_init__(self):
67
+ text(self.name, "name")
68
+ text(self.version, "version")
69
+ if (
70
+ not isinstance(self.examples, (list, tuple))
71
+ or not self.examples
72
+ or any(type(item) is not JudgmentExample for item in self.examples)
73
+ ):
74
+ raise ValueError("benchmark requires a nonempty sequence of JudgmentExample")
75
+ object.__setattr__(self, "examples", tuple(self.examples))
76
+ if len({item.example_id for item in self.examples}) != len(self.examples):
77
+ raise ValueError("example IDs must be unique")
78
+ if type(self.repetitions) is not int or self.repetitions < 1:
79
+ raise ValueError("repetitions must be a positive integer")
80
+ if type(self.seed) is not int or type(self.synthetic) is not bool:
81
+ raise ValueError("seed and synthetic require an integer and boolean")
82
+ if self.timeout_s is not None and (not finite(self.timeout_s) or self.timeout_s <= 0):
83
+ raise ValueError("timeout_s must be finite and positive")
84
+
85
+ def to_dict(self):
86
+ return {"schema_version": BENCHMARK_VERSION, **json.loads(canonical(asdict(self)))}
87
+
88
+ @property
89
+ def protocol_hash(self):
90
+ return fingerprint(self.to_dict())
91
+
92
+ @classmethod
93
+ def from_dict(cls, value):
94
+ owned = dict(value)
95
+ if owned.pop("schema_version") != BENCHMARK_VERSION:
96
+ raise ValueError("unsupported judgment benchmark version")
97
+ owned["examples"] = tuple(JudgmentExample.from_dict(item) for item in owned["examples"])
98
+ return cls(**owned)