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,306 @@
1
+ """Private, versioned JSON codecs for quiescent built-in policy state.
2
+
3
+ This is operational state supplied by a trusted host store, never trace replay
4
+ or a general-purpose object deserializer. Unknown policies are not resumable.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import math
11
+ from collections import deque
12
+ from dataclasses import fields
13
+ from fractions import Fraction
14
+
15
+ from agentloop.budget_types import COUNT_KEYS, RETRY_SOURCES
16
+ from agentloop.budgets import _BudgetEvaluator, _State
17
+ from agentloop.context_types import canonical, fingerprint
18
+ from agentloop.harness_evidence import valid_decision_id
19
+ from agentloop.loop_guards import _LoopGuard, _StepState
20
+
21
+ _REASONS = frozenset(
22
+ "invalid_policy_state in_flight_work enforcement_required budget_policy_required "
23
+ "unsupported_policy_state invalid_clock_domain stopped_run state_size_limit "
24
+ "incompatible_schema incompatible_configuration invalid_run_lineage "
25
+ "incompatible_clock_domain invalid_store_limit invalid_owner stale_checkpoint "
26
+ "claim_required missing_checkpoint checkpoint_owner_mismatch invalid_request_id "
27
+ "checkpoint_changed invalid_recovery_evidence unsupported_recovery_adapter "
28
+ "checkpoint_provider_required unsupported_recovery_store invalid_retry_safety "
29
+ "invalid_checkpoint subgraph_recovery_unsupported recovery_already_running "
30
+ "missing_run run_lineage_changed run_checkpoint_mismatch completed_checkpoint "
31
+ "invalid_checkpoint_reference dynamic_interrupt_requires_host "
32
+ "side_effect_reconciliation_required new_trace_required duplicate_or_stale_resume "
33
+ "synchronous_durability_required".split()
34
+ )
35
+
36
+
37
+ class CheckpointError(ValueError):
38
+ def __init__(self, reason):
39
+ if not isinstance(reason, str) or reason not in _REASONS:
40
+ reason = "checkpoint_error"
41
+ self.reason_code = reason
42
+ super().__init__("Checkpoint recovery failed: " + reason)
43
+
44
+
45
+ def _require(condition, reason="invalid_policy_state"):
46
+ if not condition:
47
+ raise CheckpointError(reason)
48
+
49
+
50
+ def _counts(value, keys):
51
+ return (
52
+ type(value) is dict
53
+ and set(value) == set(keys)
54
+ and all(type(item) is int and item >= 0 for item in value.values())
55
+ )
56
+
57
+
58
+ def _text(value):
59
+ return isinstance(value, str) and 0 < len(value) <= 4096
60
+
61
+
62
+ def _digest(value):
63
+ return (
64
+ isinstance(value, str)
65
+ and len(value) == 64
66
+ and all(char in "0123456789abcdef" for char in value)
67
+ )
68
+
69
+
70
+ def _budget_state(state):
71
+ _require(type(state) is _State and _text(state.scope_id))
72
+ _require(type(state.pending) is dict and not state.pending, "in_flight_work")
73
+ for name in ("committed", "refunded", "reserved_counts"):
74
+ _require(_counts(getattr(state, name), COUNT_KEYS))
75
+ _require(_counts(state.retries, RETRY_SOURCES))
76
+ _require(not any(state.reserved_counts.values()), "in_flight_work")
77
+ for name in (
78
+ "metered_calls",
79
+ "tokens_known",
80
+ "tokens_held",
81
+ "unknown_calls",
82
+ "unknown_tokens",
83
+ "unknown_cost",
84
+ "tokens_refunded",
85
+ "reserved_metered",
86
+ "reserved_tokens",
87
+ "reserved_unknown_tokens",
88
+ "reserved_unknown_cost",
89
+ ):
90
+ value = getattr(state, name)
91
+ _require(type(value) is int and value >= 0)
92
+ if name.startswith("reserved_"):
93
+ _require(value == 0, "in_flight_work")
94
+ for name in ("cost_known", "cost_held", "cost_refunded", "reserved_cost"):
95
+ _require(type(getattr(state, name)) is Fraction and getattr(state, name) >= 0)
96
+ _require(state.reserved_cost == 0, "in_flight_work")
97
+ _require(
98
+ state.last_clock is None
99
+ or (
100
+ type(state.last_clock) in {int, float}
101
+ and math.isfinite(state.last_clock)
102
+ and state.last_clock >= 0
103
+ )
104
+ )
105
+ _require(
106
+ type(state.seen_usage) is dict
107
+ and all(_text(key) and isinstance(value, str) for key, value in state.seen_usage.items())
108
+ )
109
+
110
+
111
+ def _step_state(state, window):
112
+ _require(type(state) is _StepState)
113
+ _require(type(state.pending) is dict and not state.pending, "in_flight_work")
114
+ for name in (
115
+ "retries",
116
+ "retries_in_flight",
117
+ "active_calls",
118
+ "mutating_in_flight",
119
+ "semantic_epoch",
120
+ ):
121
+ value = getattr(state, name)
122
+ _require(type(value) is int and value >= 0)
123
+ _require(
124
+ state.retries_in_flight == state.active_calls == state.mutating_in_flight == 0,
125
+ "in_flight_work",
126
+ )
127
+ _require(_counts(state.retry_sources, RETRY_SOURCES))
128
+ _require(
129
+ type(state.history) is deque
130
+ and state.history.maxlen == window
131
+ and len(state.history) <= window
132
+ )
133
+ _require(
134
+ all(
135
+ type(pair) is tuple and len(pair) == 2 and all(_digest(value) for value in pair)
136
+ for pair in state.history
137
+ )
138
+ )
139
+ _require(
140
+ state.last_status is None
141
+ or state.last_status in {"ok", "error", "cancelled", "closed", "denied"}
142
+ )
143
+ _require(state.last_decision_id is None or valid_decision_id(state.last_decision_id))
144
+ _require(state.last_mutating is None or type(state.last_mutating) is bool)
145
+ _require(state.last_arguments is None or _digest(state.last_arguments))
146
+
147
+
148
+ def validate_states(harness, states):
149
+ _require(harness.config.mode == "enforce", "enforcement_required")
150
+ policies = {policy.policy_id: policy for policy in harness.config.policies}
151
+ _require(type(states) is dict and set(states) <= set(policies))
152
+ _require(
153
+ any(type(policy.evaluate) is _BudgetEvaluator for policy in policies.values()),
154
+ "budget_policy_required",
155
+ )
156
+ for policy in policies.values():
157
+ evaluator = policy.evaluate
158
+ _require(type(evaluator) in {_BudgetEvaluator, _LoopGuard}, "unsupported_policy_state")
159
+ value = states.get(policy.policy_id, {})
160
+ _require(type(value) is dict)
161
+ if not value:
162
+ continue
163
+ if type(evaluator) is _BudgetEvaluator:
164
+ _require(set(value) == {"budget"})
165
+ _budget_state(value["budget"])
166
+ else:
167
+ _require(set(value) == {"steps"} and type(value["steps"]) is dict)
168
+ for key, state in value["steps"].items():
169
+ _require(type(key) is tuple and len(key) == 2 and all(_text(item) for item in key))
170
+ _step_state(state, evaluator.window)
171
+
172
+
173
+ def _encode(value):
174
+ if value is None or type(value) in {bool, int, float, str}:
175
+ return value
176
+ if type(value) is Fraction:
177
+ return {"fraction": [value.numerator, value.denominator]}
178
+ if type(value) is dict:
179
+ return {"map": [[_encode(key), _encode(item)] for key, item in value.items()]}
180
+ if type(value) is tuple:
181
+ return {"tuple": [_encode(item) for item in value]}
182
+ if type(value) is deque:
183
+ return {"deque": [_encode(item) for item in value], "maxlen": value.maxlen}
184
+ if type(value) in {_State, _StepState}:
185
+ return {
186
+ "type": "budget" if type(value) is _State else "loop",
187
+ "fields": {field.name: _encode(getattr(value, field.name)) for field in fields(value)},
188
+ }
189
+ raise CheckpointError("unsupported_policy_state")
190
+
191
+
192
+ def _decode(value):
193
+ if value is None or type(value) in {bool, int, float, str}:
194
+ return value
195
+ _require(type(value) is dict)
196
+ if set(value) == {"fraction"}:
197
+ pair = value["fraction"]
198
+ _require(
199
+ type(pair) is list
200
+ and len(pair) == 2
201
+ and all(type(item) is int for item in pair)
202
+ and pair[1] > 0
203
+ )
204
+ return Fraction(*pair)
205
+ if set(value) == {"map"}:
206
+ _require(type(value["map"]) is list)
207
+ result = {}
208
+ for pair in value["map"]:
209
+ _require(type(pair) is list and len(pair) == 2)
210
+ key = _decode(pair[0])
211
+ _require(type(key) in {str, tuple} and key not in result)
212
+ result[key] = _decode(pair[1])
213
+ return result
214
+ if set(value) == {"tuple"}:
215
+ _require(type(value["tuple"]) is list)
216
+ return tuple(_decode(item) for item in value["tuple"])
217
+ if set(value) == {"deque", "maxlen"}:
218
+ _require(
219
+ type(value["deque"]) is list
220
+ and type(value["maxlen"]) is int
221
+ and 1 <= value["maxlen"] <= 256
222
+ and len(value["deque"]) <= value["maxlen"]
223
+ )
224
+ return deque((_decode(item) for item in value["deque"]), maxlen=value["maxlen"])
225
+ if set(value) == {"type", "fields"}:
226
+ cls = {"budget": _State, "loop": _StepState}.get(value["type"])
227
+ _require(
228
+ cls is not None
229
+ and type(value["fields"]) is dict
230
+ and set(value["fields"]) == {field.name for field in fields(cls)}
231
+ )
232
+ return cls(**{name: _decode(item) for name, item in value["fields"].items()})
233
+ raise CheckpointError("invalid_policy_state")
234
+
235
+
236
+ def _capabilities(harness):
237
+ capabilities = harness.capabilities
238
+ return fingerprint(
239
+ {
240
+ "name": capabilities.name,
241
+ "hooks": sorted((hook.boundary, hook.phase) for hook in capabilities.hooks),
242
+ "actions": sorted(capabilities.actions),
243
+ "execution_kinds": sorted(capabilities.execution_kinds),
244
+ }
245
+ )
246
+
247
+
248
+ def seal_run(run, *, clock_domain):
249
+ """Atomically retire a quiescent attempt and export its actual policy state."""
250
+ _require(_text(clock_domain), "invalid_clock_domain")
251
+ with run._lock:
252
+ _require(not run._stopped, "stopped_run")
253
+ before = {result.call_id for result in run._results if result.hook.phase == "before"}
254
+ after = {result.call_id for result in run._results if result.hook.phase == "after"}
255
+ _require(before == after, "in_flight_work")
256
+ validate_states(run.harness, run._states)
257
+ payload = {
258
+ "schema_version": "1.0",
259
+ "config_hash": run.harness.config.config_hash,
260
+ "adapter": _capabilities(run.harness),
261
+ "run_id": run.run_id,
262
+ "clock_domain": clock_domain,
263
+ "states": _encode(run._states),
264
+ }
265
+ encoded = canonical(payload)
266
+ _require(len(encoded.encode("utf-8")) <= 4_000_000, "state_size_limit")
267
+ run._stopped = True
268
+ return json.loads(encoded)
269
+
270
+
271
+ def restore_run(harness, payload, *, clock_domain, run_id):
272
+ """Restore only a trusted, claimed host record under identical configuration."""
273
+ try:
274
+ _require(
275
+ type(payload) is dict
276
+ and set(payload)
277
+ == {"schema_version", "config_hash", "adapter", "run_id", "clock_domain", "states"}
278
+ )
279
+ _require(payload["schema_version"] == "1.0", "incompatible_schema")
280
+ _require(_text(payload["clock_domain"]) and _text(clock_domain), "invalid_clock_domain")
281
+ _require(
282
+ payload["config_hash"] == harness.config.config_hash
283
+ and payload["adapter"] == _capabilities(harness),
284
+ "incompatible_configuration",
285
+ )
286
+ _require(
287
+ _text(payload["run_id"]) and _text(run_id) and payload["run_id"] != run_id,
288
+ "invalid_run_lineage",
289
+ )
290
+ _require(len(canonical(payload).encode("utf-8")) <= 4_000_000, "state_size_limit")
291
+ # Absolute monotonic deadlines cannot safely move between clock epochs.
292
+ if any(
293
+ type(policy.evaluate) is _BudgetEvaluator
294
+ and policy.evaluate.limits.deadline_at is not None
295
+ for policy in harness.config.policies
296
+ ):
297
+ _require(payload["clock_domain"] == clock_domain, "incompatible_clock_domain")
298
+ states = _decode(payload["states"])
299
+ validate_states(harness, states)
300
+ except CheckpointError:
301
+ raise
302
+ except (TypeError, ValueError, KeyError, RecursionError):
303
+ raise CheckpointError("invalid_policy_state") from None
304
+ run = harness.start_run(run_id)
305
+ run._states = states
306
+ return run
@@ -0,0 +1,86 @@
1
+ """Trusted host-side recovery journal contract and a bounded, non-durable example."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from threading import RLock
7
+ from typing import Protocol
8
+ from uuid import uuid4
9
+
10
+ from agentloop.checkpoint_state import CheckpointError, _require, _text
11
+ from agentloop.context_types import canonical, fingerprint
12
+
13
+
14
+ class RecoveryStore(Protocol):
15
+ """The host must authenticate owners and atomically claim the latest record.
16
+
17
+ Production implementations must persist claims and records together. A claim
18
+ is never automatically released: an interrupted/uncertain resume needs host
19
+ reconciliation. Graph state remains in the framework's own checkpointer.
20
+ """
21
+
22
+ atomic_claim: bool
23
+ durable: bool
24
+ schema_version: str
25
+
26
+ def save(self, owner, stream, record, *, previous_ref=None, request_id=None) -> str: ...
27
+
28
+ def load(self, owner, stream, reference) -> dict: ...
29
+
30
+ def claim(self, owner, stream, reference, request_id, record_hash) -> bool: ...
31
+
32
+
33
+ class MemoryRecoveryStore:
34
+ """Thread-safe bounded fake journal; loses records and claims on restart."""
35
+
36
+ atomic_claim = True
37
+ durable = False
38
+ schema_version = "1.0"
39
+
40
+ def __init__(self, *, max_records=256):
41
+ _require(type(max_records) is int and 1 <= max_records <= 10000, "invalid_store_limit")
42
+ self._maximum = max_records
43
+ self._lock = RLock()
44
+ self._records, self._latest, self._claims, self._requests = {}, {}, {}, set()
45
+
46
+ def save(self, owner, stream, record, *, previous_ref=None, request_id=None):
47
+ _require(_text(owner) and _text(stream), "invalid_owner")
48
+ encoded = canonical(record)
49
+ _require(len(encoded.encode("utf-8")) <= 4_100_000, "state_size_limit")
50
+ key = (owner, stream)
51
+ with self._lock:
52
+ _require(len(self._records) < self._maximum, "store_limit")
53
+ _require(self._latest.get(key) == previous_ref, "stale_checkpoint")
54
+ if previous_ref is not None:
55
+ _require(
56
+ _text(request_id) and self._claims.get(previous_ref) == request_id,
57
+ "claim_required",
58
+ )
59
+ reference = "checkpoint_" + uuid4().hex
60
+ self._records[reference] = (key, encoded)
61
+ self._latest[key] = reference
62
+ return reference
63
+
64
+ def load(self, owner, stream, reference):
65
+ with self._lock:
66
+ entry = self._records.get(reference)
67
+ if entry is None:
68
+ raise CheckpointError("missing_checkpoint")
69
+ _require(entry[0] == (owner, stream), "checkpoint_owner_mismatch")
70
+ return json.loads(entry[1])
71
+
72
+ def claim(self, owner, stream, reference, request_id, record_hash):
73
+ _require(_text(request_id), "invalid_request_id")
74
+ key = (owner, stream)
75
+ with self._lock:
76
+ record = self.load(owner, stream, reference)
77
+ _require(fingerprint(record) == record_hash, "checkpoint_changed")
78
+ if (
79
+ self._latest.get(key) != reference
80
+ or reference in self._claims
81
+ or (key, request_id) in self._requests
82
+ ):
83
+ return False
84
+ self._claims[reference] = request_id
85
+ self._requests.add((key, request_id))
86
+ return True
agentloop/ci.py ADDED
@@ -0,0 +1,202 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from agentloop.findings import build_diagnosis
6
+ from agentloop.markdown import markdown_table_cell, markdown_text
7
+ from agentloop.replay import ReplayGates, build_replay_report
8
+
9
+
10
+ def build_ci_report(
11
+ baseline_trace: Any,
12
+ candidate_trace: Any,
13
+ *,
14
+ gates: ReplayGates | None = None,
15
+ quality_report: dict[str, Any] | None = None,
16
+ ) -> dict[str, Any]:
17
+ """Build the PR-facing AgentLoop performance report."""
18
+ replay = build_replay_report(
19
+ baseline_trace, candidate_trace, gates=gates, quality_report=quality_report
20
+ )
21
+ diagnosis = build_diagnosis(candidate_trace)
22
+ summary = _summary(replay, diagnosis)
23
+ trace_inputs = {
24
+ "baseline": _trace_input(baseline_trace),
25
+ "candidate": _trace_input(candidate_trace),
26
+ }
27
+ synthetic = any(item["synthetic"] is True for item in trace_inputs.values())
28
+ analysis_complete = diagnosis.get("analysis_complete", True)
29
+ if synthetic:
30
+ summary["merge_recommendation"] = (
31
+ "synthetic self-test only; assess application traces before merge"
32
+ )
33
+ if not analysis_complete:
34
+ summary["merge_recommendation"] = (
35
+ "analysis incomplete; inspect failed finding rules before merge"
36
+ )
37
+ passed = replay["gates"]["passed"] and analysis_complete
38
+ return {
39
+ "passed": passed,
40
+ "status": "passed" if passed else "failed",
41
+ "summary": summary,
42
+ "replay": replay,
43
+ "diagnosis": diagnosis,
44
+ "trace_inputs": trace_inputs,
45
+ "evidence_scope": "includes_synthetic_data" if synthetic else "supplied_trace_comparison",
46
+ "analysis_complete": analysis_complete,
47
+ "rule_errors": diagnosis.get("rule_errors", []),
48
+ }
49
+
50
+
51
+ def ci_report_to_markdown(report: dict[str, Any]) -> str:
52
+ summary = report["summary"]
53
+ replay = report["replay"]
54
+ diagnosis = report["diagnosis"]
55
+ gate_status = report.get("status", "passed" if replay["gates"]["passed"] else "failed")
56
+
57
+ lines = [
58
+ "# AgentLoop CI Report",
59
+ "",
60
+ f"- Status: {gate_status}",
61
+ f"- Replay: {markdown_text(replay['summary'])}",
62
+ f"- Merge recommendation: {markdown_text(summary['merge_recommendation'])}",
63
+ (
64
+ f"- Findings: {summary['finding_count']} total, "
65
+ f"{summary['high_severity_count']} high severity, "
66
+ f"{summary['patchable_count']} patchable"
67
+ ),
68
+ "",
69
+ "## Performance Gates",
70
+ "",
71
+ "| Gate | Status | Detail |",
72
+ "|---|---|---|",
73
+ ]
74
+ for gate in replay["gates"]["results"]:
75
+ status = "pass" if gate["passed"] else "fail"
76
+ lines.append(
77
+ f"| {markdown_table_cell(gate['name'])} | {status} | "
78
+ f"{markdown_table_cell(gate['detail'])} |"
79
+ )
80
+ if report.get("rule_errors"):
81
+ lines.extend(["", "## Incomplete analysis", ""])
82
+ for error in report["rule_errors"]:
83
+ lines.append(f"- {markdown_text(error['rule_id'])}: {markdown_text(error['message'])}")
84
+
85
+ if report.get("trace_inputs"):
86
+ lines.extend(["", "## Trace inputs", ""])
87
+ if report.get("evidence_scope") == "includes_synthetic_data":
88
+ lines.append(
89
+ "This comparison includes synthetic data. It does not establish application performance gains from a pull request."
90
+ )
91
+ else:
92
+ lines.append(
93
+ "Results apply to the supplied traces and configured gates. Trace provenance is supplied by the producer."
94
+ )
95
+ lines.extend(
96
+ ["", "| Side | Run ID | Path | Synthetic marker | Source |", "|---|---|---|---|---|"]
97
+ )
98
+ for side, item in report["trace_inputs"].items():
99
+ marker = (
100
+ "yes"
101
+ if item["synthetic"] is True
102
+ else ("no" if item["synthetic"] is False else "not supplied")
103
+ )
104
+ cells = (
105
+ side,
106
+ item["run_id"],
107
+ item.get("path", "in-memory trace"),
108
+ marker,
109
+ item["source"],
110
+ )
111
+ lines.append("| " + " | ".join(markdown_table_cell(value) for value in cells) + " |")
112
+
113
+ lines.extend(
114
+ [
115
+ "",
116
+ "## Metric Impact",
117
+ "",
118
+ "| Metric | Impact |",
119
+ "|---|---:|",
120
+ f"| Latency improvement | {summary['latency_improvement_pct']:.2f}% |",
121
+ "| Cost improvement | "
122
+ + (
123
+ "unavailable"
124
+ if summary["cost_improvement_pct"] is None
125
+ else f"{summary['cost_improvement_pct']:.2f}%"
126
+ )
127
+ + " |",
128
+ f"| Retry delta | {summary['retry_count_delta']} |",
129
+ f"| Candidate schema validity | {summary['candidate_schema_validity']} |",
130
+ f"| Candidate quality score | {summary['candidate_quality_score']} |",
131
+ "",
132
+ "## Top Findings",
133
+ "",
134
+ ]
135
+ )
136
+
137
+ findings = diagnosis.get("findings", [])
138
+ if not findings:
139
+ lines.append("No machine-actionable optimization findings detected.")
140
+ else:
141
+ lines.extend(
142
+ [
143
+ "| Finding | Severity | Type | Rewrite |",
144
+ "|---|---|---|---|",
145
+ ]
146
+ )
147
+ for finding in findings[:5]:
148
+ lines.append(
149
+ "| "
150
+ f"{markdown_table_cell(finding['finding_id'])}: "
151
+ f"{markdown_table_cell(finding['title'])} | "
152
+ f"{markdown_table_cell(finding['severity'])} | "
153
+ f"{markdown_table_cell(finding['type'])} | "
154
+ f"{markdown_table_cell(finding['rewrite']['hint'])} |"
155
+ )
156
+
157
+ return "\n".join(lines).rstrip() + "\n"
158
+
159
+
160
+ def _summary(replay: dict[str, Any], diagnosis: dict[str, Any]) -> dict[str, Any]:
161
+ deltas = replay["deltas"]
162
+ diagnosis_summary = diagnosis["summary"]
163
+ candidate = replay["candidate"]
164
+ gates_passed = replay["gates"]["passed"]
165
+ merge_recommendation = (
166
+ "safe to merge by configured AgentLoop gates"
167
+ if gates_passed and diagnosis_summary["high_severity_count"] == 0
168
+ else "review before merge; configured gates or high-severity findings need attention"
169
+ )
170
+ return {
171
+ "latency_improvement_pct": deltas["latency_improvement_pct"],
172
+ "cost_improvement_pct": deltas["cost_improvement_pct"],
173
+ "retry_count_delta": deltas["retry_count_delta"],
174
+ "candidate_schema_validity": _format_optional(
175
+ candidate.get("schema_validity_pct"), suffix="%"
176
+ )
177
+ if candidate.get("schema_validity_pct") is not None
178
+ else _format_optional(candidate.get("schema_valid")),
179
+ "candidate_quality_score": _format_optional(candidate.get("quality_score")),
180
+ "finding_count": diagnosis_summary["finding_count"],
181
+ "high_severity_count": diagnosis_summary["high_severity_count"],
182
+ "patchable_count": diagnosis_summary["patchable_count"],
183
+ "merge_recommendation": merge_recommendation,
184
+ }
185
+
186
+
187
+ def _format_optional(value: Any, suffix: str = "") -> str:
188
+ if value is None:
189
+ return "not provided"
190
+ if isinstance(value, float):
191
+ return f"{value:.4f}{suffix}"
192
+ return f"{value}{suffix}"
193
+
194
+
195
+ def _trace_input(trace: Any) -> dict[str, Any]:
196
+ metadata = getattr(trace, "metadata", {}) or {}
197
+ marker = metadata.get("synthetic")
198
+ return {
199
+ "run_id": str(trace.run_id),
200
+ "synthetic": marker if isinstance(marker, bool) else None,
201
+ "source": str(metadata.get("source") or "not supplied"),
202
+ }