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,355 @@
1
+ """Versioned declarations for finding-linked, caller-run offline experiments."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from dataclasses import asdict, dataclass, field
7
+ from hashlib import sha256
8
+ from time import monotonic
9
+ from typing import Callable
10
+
11
+ from agentloop.ablation_protocol import number, timestamp
12
+ from agentloop.budget_types import Reservation, ResourceUsage
13
+ from agentloop.context_types import canonical as context_canonical
14
+ from agentloop.context_types import identifier, synchronous
15
+ from agentloop.events import utc_now_iso
16
+ from agentloop.findings import build_diagnosis
17
+ from agentloop.quality import validate_quality_fixtures
18
+ from agentloop.replay import ReplayGates
19
+ from agentloop.tracer import AgentTrace
20
+
21
+ EXPERIMENT_VERSION = "1.0"
22
+ EXPERIMENT_KEY = "agentloop.experiment"
23
+
24
+
25
+ def canonical(value):
26
+ # Validate finite JSON/string keys, then share the existing native evidence
27
+ # escaping convention so Unicode trace/output hashes bind without translation.
28
+ normalized = json.loads(context_canonical(value))
29
+ return json.dumps(normalized, sort_keys=True, separators=(",", ":"), allow_nan=False)
30
+
31
+
32
+ def fingerprint(value):
33
+ return sha256(canonical(value).encode("utf-8")).hexdigest()
34
+
35
+
36
+ def owned(value):
37
+ return json.loads(canonical(value))
38
+
39
+
40
+ def reference(value, name):
41
+ if not isinstance(value, str) or not value.strip() or len(value) > 1024:
42
+ raise ValueError(name + " must be a bounded nonempty reference")
43
+
44
+
45
+ @dataclass(frozen=True)
46
+ class ExperimentBudget:
47
+ max_invocations: int
48
+ timeout_s: float
49
+ max_tokens: int | None = None
50
+ max_cost_usd: float | None = None
51
+
52
+ def __post_init__(self):
53
+ number(self.max_invocations, "max_invocations", integer=True, maximum=10000)
54
+ number(self.timeout_s, "timeout_s", maximum=86400)
55
+ number(self.max_tokens, "max_tokens", integer=True, nullable=True)
56
+ number(self.max_cost_usd, "max_cost_usd", nullable=True)
57
+ if not self.max_invocations or not self.timeout_s:
58
+ raise ValueError("experiment invocation and time limits must be positive")
59
+
60
+
61
+ @dataclass(frozen=True)
62
+ class ExperimentRunner:
63
+ candidate_id: str
64
+ version: str
65
+ implementation_ref: str
66
+ configuration_ref: str
67
+ invoke: Callable = field(repr=False, compare=False)
68
+ reservation: Reservation = field(default_factory=Reservation)
69
+
70
+ def __post_init__(self):
71
+ identifier(self.candidate_id, "candidate_id")
72
+ identifier(self.version, "runner version")
73
+ reference(self.implementation_ref, "implementation_ref")
74
+ reference(self.configuration_ref, "configuration_ref")
75
+ if not synchronous(self.invoke) or type(self.reservation) is not Reservation:
76
+ raise ValueError("runner requires a trusted synchronous callback and reservation")
77
+
78
+ def declaration(self):
79
+ return owned(
80
+ {
81
+ "candidate_id": self.candidate_id,
82
+ "version": self.version,
83
+ "implementation_ref": self.implementation_ref,
84
+ "configuration_ref": self.configuration_ref,
85
+ "reservation": self.reservation.to_dict(),
86
+ }
87
+ )
88
+
89
+
90
+ @dataclass(frozen=True, init=False)
91
+ class ExperimentCase:
92
+ case_id: str
93
+ example_id: str
94
+ repetition: str
95
+ _json: str = field(repr=False)
96
+
97
+ def __init__(
98
+ self,
99
+ case_id,
100
+ baseline,
101
+ *,
102
+ target_finding_ids,
103
+ inputs,
104
+ expected,
105
+ baseline_output,
106
+ input_ref,
107
+ quality_ref,
108
+ baseline_implementation_ref,
109
+ baseline_configuration_ref,
110
+ example_id=None,
111
+ repetition="0",
112
+ ):
113
+ identifier(case_id, "case_id")
114
+ identifier(example_id or case_id, "example_id")
115
+ identifier(repetition, "repetition")
116
+ for name, value in (
117
+ ("input_ref", input_ref),
118
+ ("quality_ref", quality_ref),
119
+ ("baseline_implementation_ref", baseline_implementation_ref),
120
+ ("baseline_configuration_ref", baseline_configuration_ref),
121
+ ):
122
+ reference(value, name)
123
+ if not isinstance(baseline, AgentTrace):
124
+ raise ValueError("case requires an existing baseline trace")
125
+ trace = AgentTrace.from_dict(baseline.to_dict())
126
+ if trace.ended_at is None:
127
+ raise ValueError("baseline trace must be complete before planning")
128
+ if "output" in trace.metadata and fingerprint(trace.metadata["output"]) != fingerprint(
129
+ baseline_output
130
+ ):
131
+ raise ValueError("baseline output differs from the recorded workflow output")
132
+ diagnosis = build_diagnosis(trace)
133
+ if (
134
+ not isinstance(target_finding_ids, (list, tuple))
135
+ or not target_finding_ids
136
+ or len(target_finding_ids) > 64
137
+ ):
138
+ raise ValueError("explicit source finding IDs required")
139
+ ids = sorted(set(target_finding_ids))
140
+ available = {item["finding_id"]: item for item in diagnosis["findings"]}
141
+ if any(identity not in available for identity in ids):
142
+ raise ValueError("source finding must belong to this actual baseline")
143
+ data = {
144
+ "baseline": trace.to_dict(),
145
+ "diagnosis": diagnosis,
146
+ "target_finding_ids": ids,
147
+ "inputs": inputs,
148
+ "expected": expected,
149
+ "baseline_output": baseline_output,
150
+ "input_ref": input_ref,
151
+ "quality_ref": quality_ref,
152
+ "baseline_implementation_ref": baseline_implementation_ref,
153
+ "baseline_configuration_ref": baseline_configuration_ref,
154
+ }
155
+ encoded = canonical(data)
156
+ if len(encoded.encode("utf-8")) > 10_000_000:
157
+ raise ValueError("case exceeds ten million bytes")
158
+ for name, value in (
159
+ ("case_id", case_id),
160
+ ("example_id", example_id or case_id),
161
+ ("repetition", repetition),
162
+ ("_json", encoded),
163
+ ):
164
+ object.__setattr__(self, name, value)
165
+
166
+ def payload(self):
167
+ return json.loads(self._json)
168
+
169
+ def declaration(self):
170
+ data = self.payload()
171
+ return {
172
+ "case_id": self.case_id,
173
+ "example_id": self.example_id,
174
+ "repetition": self.repetition,
175
+ "baseline_run_id": data["baseline"]["run_id"],
176
+ "baseline_hash": fingerprint(data["baseline"]),
177
+ "source_finding_ids": data["target_finding_ids"],
178
+ "expected_effects": sorted(
179
+ (
180
+ item
181
+ for item in data["diagnosis"]["findings"]
182
+ if item["finding_id"] in data["target_finding_ids"]
183
+ ),
184
+ key=lambda item: item["finding_id"],
185
+ ),
186
+ "diagnosis_hash": fingerprint(data["diagnosis"]),
187
+ "input_ref": data["input_ref"],
188
+ "input_hash": fingerprint(data["inputs"]),
189
+ "quality_ref": data["quality_ref"],
190
+ "expected_hash": fingerprint(data["expected"]),
191
+ "baseline_output_hash": fingerprint(data["baseline_output"]),
192
+ "baseline_implementation_ref": data["baseline_implementation_ref"],
193
+ "baseline_configuration_ref": data["baseline_configuration_ref"],
194
+ "synthetic": data["baseline"]["metadata"].get("synthetic") is True,
195
+ }
196
+
197
+
198
+ @dataclass(frozen=True)
199
+ class ExperimentRequest:
200
+ experiment_id: str
201
+ case_id: str
202
+ candidate_id: str
203
+ input_ref: str
204
+ deadline: float
205
+ _input_json: str = field(repr=False)
206
+
207
+ @property
208
+ def inputs(self):
209
+ return json.loads(self._input_json)
210
+
211
+ @property
212
+ def remaining_s(self):
213
+ return max(0.0, self.deadline - monotonic())
214
+
215
+
216
+ @dataclass(frozen=True, init=False)
217
+ class ExperimentResult:
218
+ _json: str = field(repr=False)
219
+ usage: ResourceUsage
220
+
221
+ def __init__(self, output, *, usage=None):
222
+ usage = ResourceUsage() if usage is None else usage
223
+ if type(usage) is not ResourceUsage:
224
+ raise ValueError("experiment usage must be ResourceUsage")
225
+ encoded = canonical(output)
226
+ if len(encoded.encode("utf-8")) > 1_000_000:
227
+ raise ValueError("experiment output exceeds one million bytes")
228
+ object.__setattr__(self, "_json", encoded)
229
+ object.__setattr__(self, "usage", usage)
230
+
231
+ @property
232
+ def output(self):
233
+ return json.loads(self._json)
234
+
235
+
236
+ @dataclass(frozen=True, init=False)
237
+ class ExperimentPlan:
238
+ _json: str = field(repr=False)
239
+ experiment_id: str
240
+
241
+ def __init__(
242
+ self,
243
+ name,
244
+ *,
245
+ cases,
246
+ runners,
247
+ intervention_type,
248
+ intervention_version,
249
+ scorer,
250
+ gate_version,
251
+ gates,
252
+ budget,
253
+ permission_ref,
254
+ frozen_at=None,
255
+ synthetic=False,
256
+ ):
257
+ identifier(name, "experiment name")
258
+ identifier(intervention_type, "intervention_type")
259
+ identifier(intervention_version, "intervention_version")
260
+ identifier(gate_version, "gate_version")
261
+ reference(permission_ref, "permission_ref")
262
+ if (
263
+ not isinstance(cases, (tuple, list))
264
+ or not 1 <= len(cases) <= 1000
265
+ or any(type(case) is not ExperimentCase for case in cases)
266
+ ):
267
+ raise ValueError("experiment requires between one and one thousand frozen cases")
268
+ if (
269
+ not isinstance(runners, (tuple, list))
270
+ or not 1 <= len(runners) <= 16
271
+ or any(type(runner) is not ExperimentRunner for runner in runners)
272
+ ):
273
+ raise ValueError("experiment requires bounded explicit runner bindings")
274
+ if (
275
+ len({case.case_id for case in cases}) != len(cases)
276
+ or len({(case.example_id, case.repetition) for case in cases}) != len(cases)
277
+ or len({case.payload()["baseline"]["run_id"] for case in cases}) != len(cases)
278
+ or len({runner.candidate_id for runner in runners}) != len(runners)
279
+ ):
280
+ raise ValueError("case, baseline-run, pairing and candidate identities must be unique")
281
+ if (
282
+ type(budget) is not ExperimentBudget
283
+ or type(gates) is not ReplayGates
284
+ or type(synthetic) is not bool
285
+ ):
286
+ raise ValueError("typed budget/gates and explicit synthetic marker required")
287
+ if gates.min_quality_score is None:
288
+ raise ValueError("experiment gates require independent quality")
289
+ for key, value in asdict(gates).items():
290
+ if key.startswith("require_"):
291
+ if type(value) is not bool:
292
+ raise ValueError("gate flags must be booleans")
293
+ else:
294
+ number(value, key, maximum=1 if key == "min_quality_score" else None)
295
+ scorer = owned(scorer)
296
+ if not isinstance(scorer, dict) or scorer.get("type") == "custom":
297
+ raise ValueError(
298
+ "experiment scorer must be deterministic; executable imports are unsupported"
299
+ )
300
+ identifier(scorer.get("version"), "scorer version")
301
+ validate_quality_fixtures(
302
+ [
303
+ {
304
+ "schema_version": "2.0",
305
+ "id": case.case_id,
306
+ "expected": case.payload()["expected"],
307
+ "scorer": scorer,
308
+ }
309
+ for case in cases
310
+ ]
311
+ )
312
+ for runner in runners:
313
+ if (
314
+ budget.max_tokens is not None
315
+ and not runner.reservation.tokens_known
316
+ or budget.max_cost_usd is not None
317
+ and not runner.reservation.cost_known
318
+ ):
319
+ raise ValueError("resource limits require declared conservative reservation bounds")
320
+ when = frozen_at or utc_now_iso()
321
+ timestamp(when)
322
+ if any(
323
+ timestamp(case.payload()["baseline"]["ended_at"]) > timestamp(when) for case in cases
324
+ ):
325
+ raise ValueError("baseline outcomes must precede the frozen candidate plan")
326
+ data = {
327
+ "schema_version": EXPERIMENT_VERSION,
328
+ "name": name,
329
+ "frozen_at": when,
330
+ "permission_ref": permission_ref,
331
+ "intervention": {"type": intervention_type, "version": intervention_version},
332
+ "cases": [case.declaration() for case in cases],
333
+ "runners": [runner.declaration() for runner in runners],
334
+ "scorer": scorer,
335
+ "scorer_hash": fingerprint(scorer),
336
+ "gate_version": gate_version,
337
+ "gates": asdict(gates),
338
+ "budget": asdict(budget),
339
+ "budget_scope": "new_candidate_attempt_admissions_conservative_reservations_no_refunds",
340
+ "pairing_keys": ["experiment_id", "case_id"],
341
+ "artifact_layout": {
342
+ "baselines": "baselines/<case-sha256>.json",
343
+ "attempts": "attempts/<slot-sha256>/",
344
+ "reports": "reports/<snapshot-sha256>/",
345
+ },
346
+ "synthetic": synthetic or any(case.declaration()["synthetic"] for case in cases),
347
+ }
348
+ encoded = canonical(data)
349
+ if len(encoded.encode("utf-8")) > 20_000_000:
350
+ raise ValueError("experiment plan exceeds twenty million bytes")
351
+ object.__setattr__(self, "_json", encoded)
352
+ object.__setattr__(self, "experiment_id", "experiment_" + fingerprint(data))
353
+
354
+ def to_dict(self):
355
+ return json.loads(self._json)