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
agentloop/events.py ADDED
@@ -0,0 +1,75 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from datetime import datetime, timezone
5
+ from typing import Any
6
+ from uuid import uuid4
7
+
8
+ from agentloop.operations import operation_kind
9
+
10
+
11
+ def utc_now_iso() -> str:
12
+ return datetime.now(timezone.utc).isoformat()
13
+
14
+
15
+ def new_event_id() -> str:
16
+ return "evt_" + uuid4().hex[:16]
17
+
18
+
19
+ def new_run_id() -> str:
20
+ return "run_" + uuid4().hex[:16]
21
+
22
+
23
+ def format_exception_detail(exc: BaseException) -> str:
24
+ """Return useful telemetry text even when an exception has no message."""
25
+
26
+ message = str(exc)
27
+ return message if message.strip() else type(exc).__name__
28
+
29
+
30
+ @dataclass
31
+ class AgentEvent:
32
+ event_id: str
33
+ run_id: str
34
+ event_type: str
35
+ name: str
36
+ started_at: str
37
+ ended_at: str
38
+ duration_ms: float
39
+ parent_id: str | None = None
40
+ model: str | None = None
41
+ input_tokens: int = 0
42
+ output_tokens: int = 0
43
+ # Where input_tokens/output_tokens came from. ``None`` means the trace was
44
+ # written before this field existed, which reads back as "unspecified" — not
45
+ # as exact. See agentloop.tokens and docs/TRACE_SCHEMA.md.
46
+ token_provenance: str | None = None
47
+ input_text: str | None = None
48
+ output_text: str | None = None
49
+ status: str = "ok"
50
+ error: str | None = None
51
+ metadata: dict[str, Any] = field(default_factory=dict)
52
+
53
+ @property
54
+ def operation_kind(self) -> str:
55
+ return operation_kind(self)
56
+
57
+ @property
58
+ def total_tokens(self) -> int:
59
+ return self.input_tokens + self.output_tokens
60
+
61
+ def to_dict(self) -> dict[str, Any]:
62
+ return self.__dict__.copy()
63
+
64
+ @classmethod
65
+ def from_dict(cls, data: dict[str, Any], *, index: int = 0) -> "AgentEvent":
66
+ """Build an event from a serialized dict, validating the schema contract.
67
+
68
+ Raises :class:`agentloop.schema.TraceValidationError` (a ``ValueError``)
69
+ for missing, wrongly typed, or out-of-range fields; unknown fields are
70
+ ignored per the documented forward-compatibility policy.
71
+ """
72
+
73
+ from agentloop.schema import coerce_event_dict
74
+
75
+ return cls(**coerce_event_dict(data, index=index))
@@ -0,0 +1,80 @@
1
+ """Owned local artifacts; exclusive journal lock and atomic immutable writes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ from contextlib import contextmanager
8
+ from pathlib import Path
9
+ from uuid import uuid4
10
+
11
+ from agentloop.experiment_types import canonical
12
+
13
+
14
+ def local_path(root, relative):
15
+ root = Path(root).resolve()
16
+ target = (root / relative).resolve()
17
+ if root not in target.parents:
18
+ raise ValueError("experiment artifact path escapes its root")
19
+ return target
20
+
21
+
22
+ def read_json(path):
23
+ return json.loads(Path(path).read_text(encoding="utf-8"))
24
+
25
+
26
+ def write_once(root, relative, value):
27
+ return write_bytes_once(root, relative, (canonical(value) + "\n").encode("utf-8"))
28
+
29
+
30
+ def write_bytes_once(root, relative, encoded):
31
+ path = local_path(root, relative)
32
+ if path.exists():
33
+ if path.read_bytes() != encoded:
34
+ raise ValueError("existing experiment artifact has conflicting content")
35
+ return path
36
+ path.parent.mkdir(parents=True, exist_ok=True)
37
+ temporary = local_path(
38
+ root, str(path.relative_to(Path(root).resolve())) + "." + uuid4().hex + ".tmp"
39
+ )
40
+ try:
41
+ with temporary.open("xb") as handle:
42
+ handle.write(encoded)
43
+ handle.flush()
44
+ os.fsync(handle.fileno())
45
+ # All journal writers hold the same exclusive lock. Existing artifacts
46
+ # are compared above; an interrupted temporary is never a completed record.
47
+ os.replace(temporary, path)
48
+ finally:
49
+ temporary.unlink(missing_ok=True)
50
+ return path
51
+
52
+
53
+ @contextmanager
54
+ def journal_lock(root):
55
+ root = Path(root).resolve()
56
+ root.mkdir(parents=True, exist_ok=True)
57
+ path = local_path(root, "journal.lock")
58
+ nonce = uuid4().hex
59
+ try:
60
+ with path.open("x", encoding="utf-8") as handle:
61
+ handle.write(nonce)
62
+ handle.flush()
63
+ os.fsync(handle.fileno())
64
+ except FileExistsError:
65
+ raise ValueError(
66
+ "experiment journal is busy or requires host lock reconciliation"
67
+ ) from None
68
+ failed = False
69
+ try:
70
+ yield
71
+ except BaseException:
72
+ failed = True
73
+ raise
74
+ finally:
75
+ try:
76
+ if path.read_text(encoding="utf-8") == nonce:
77
+ path.unlink()
78
+ except BaseException:
79
+ if not failed:
80
+ raise
@@ -0,0 +1,169 @@
1
+ """Bounded scalar observations attached to native experiment traces."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from collections import Counter
7
+
8
+ from agentloop.context_types import identifier
9
+ from agentloop.experiment_types import owned, reference
10
+ from agentloop.studies import summarize_values
11
+ from agentloop.tracer import current_trace
12
+
13
+ OBSERVATIONS_KEY = "agentloop.experiment_observations"
14
+
15
+
16
+ def observation(value, *, kind="observed", unit="count", source_ref):
17
+ if value is not None and not (
18
+ type(value) in {bool, int, float, str} and (not isinstance(value, str) or len(value) <= 256)
19
+ ):
20
+ raise ValueError("experiment observations must be bounded scalars")
21
+ if type(value) in {int, float}:
22
+ try:
23
+ if not math.isfinite(value):
24
+ raise ValueError("experiment observations must be finite")
25
+ except OverflowError:
26
+ raise ValueError("experiment observation is outside the numeric range") from None
27
+ if kind not in {"observed", "estimated", "declared"}:
28
+ raise ValueError("unknown observation kind")
29
+ identifier(unit, "observation unit")
30
+ reference(source_ref, "observation source")
31
+ return {"value": value, "kind": kind, "unit": unit, "source_ref": source_ref}
32
+
33
+
34
+ def record_experiment_observations(values):
35
+ trace = current_trace()
36
+ if trace is None or not isinstance(trace.metadata.get("experiment_id"), str):
37
+ raise ValueError("observations require an active experiment trace")
38
+ if not isinstance(values, dict) or len(values) > 64:
39
+ raise ValueError("observations require a bounded mapping")
40
+ copied = {}
41
+ for key, value in values.items():
42
+ identifier(key, "observation name")
43
+ if not isinstance(value, dict) or set(value) != {"value", "kind", "unit", "source_ref"}:
44
+ raise ValueError("invalid observation fields")
45
+ copied[key] = observation(**value)
46
+ envelope = trace.metadata.setdefault(OBSERVATIONS_KEY, {"schema_version": "1.0", "values": {}})
47
+ if (
48
+ not isinstance(envelope, dict)
49
+ or envelope.get("schema_version") != "1.0"
50
+ or not isinstance(envelope.get("values"), dict)
51
+ ):
52
+ raise ValueError("invalid experiment observation envelope")
53
+ if len(set(envelope["values"]) | set(copied)) > 64:
54
+ raise ValueError("experiment observation limit exceeded")
55
+ if any(
56
+ key in envelope["values"] and envelope["values"][key] != value
57
+ for key, value in copied.items()
58
+ ):
59
+ raise ValueError("experiment observation cannot be rewritten")
60
+ envelope["values"].update(owned(copied))
61
+
62
+
63
+ def summarize_observations(rows):
64
+ sets = []
65
+ for row in rows:
66
+ raw = (row["trace"] or {}).get("metadata", {}).get(OBSERVATIONS_KEY)
67
+ if raw is None:
68
+ sets.append({})
69
+ continue
70
+ if (
71
+ not isinstance(raw, dict)
72
+ or raw.get("schema_version") != "1.0"
73
+ or not isinstance(raw.get("values"), dict)
74
+ or len(raw["values"]) > 64
75
+ ):
76
+ raise ValueError("invalid saved experiment observations")
77
+ values = {}
78
+ for name, item in raw["values"].items():
79
+ identifier(name, "observation name")
80
+ values[name] = observation(**item)
81
+ sets.append(values)
82
+ result = {}
83
+ for name in sorted({key for values in sets for key in values}):
84
+ available = [
85
+ values[name] for values in sets if name in values and values[name]["value"] is not None
86
+ ]
87
+ kinds = sorted({item["kind"] for item in available})
88
+ units = sorted({item["unit"] for item in available})
89
+ sources = sorted({item["source_ref"] for item in available})
90
+ row_values = [values.get(name, {}).get("value") for values in sets]
91
+ compatible = len(kinds) == len(units) == len(sources) == 1
92
+ summary = {
93
+ "planned_count": len(rows),
94
+ "known_count": len(available),
95
+ "missing_count": len(rows) - len(available),
96
+ "kinds": kinds,
97
+ "units": units,
98
+ "source_refs": sources,
99
+ "aggregation_status": "compatible" if compatible else "unknown_or_mixed_basis",
100
+ }
101
+ if compatible and all(type(value) is bool for value in row_values if value is not None):
102
+ true_count = sum(value is True for value in row_values)
103
+ quality_true = sum(
104
+ value is True and row["receipt"] is not None and row["receipt"]["quality_passed"]
105
+ for value, row in zip(row_values, rows)
106
+ )
107
+ summary.update(
108
+ true_count=true_count,
109
+ false_count=sum(value is False for value in row_values),
110
+ true_fraction_of_planned=true_count / len(rows),
111
+ quality_preserving_true_count=quality_true,
112
+ quality_preserving_true_fraction_of_planned=quality_true / len(rows),
113
+ )
114
+ elif compatible and all(
115
+ type(value) in {int, float} for value in row_values if value is not None
116
+ ):
117
+ summary["values"] = summarize_values(row_values)
118
+ summary["quality_preserving_values"] = summarize_values(
119
+ [
120
+ value
121
+ if row["receipt"] is not None and row["receipt"]["quality_passed"]
122
+ else None
123
+ for value, row in zip(row_values, rows)
124
+ ]
125
+ )
126
+ if units == ["completed_items"] and kinds == ["observed"]:
127
+ rates = []
128
+ for value, row in zip(row_values, rows):
129
+ elapsed = (
130
+ None
131
+ if row["intervention"] is None
132
+ else row["intervention"]["measured"]["candidate"]["runtime_ms"]
133
+ )
134
+ rate = (
135
+ value * 1000 / elapsed
136
+ if value is not None and elapsed is not None and elapsed > 0
137
+ else None
138
+ )
139
+ rates.append(rate if rate is not None and math.isfinite(rate) else None)
140
+ summary["per_second_of_end_to_end_candidate_runtime"] = summarize_values(rates)
141
+ summary["quality_preserving_per_second"] = summarize_values(
142
+ [
143
+ value
144
+ if row["receipt"] is not None and row["receipt"]["quality_passed"]
145
+ else None
146
+ for value, row in zip(rates, rows)
147
+ ]
148
+ )
149
+ elif compatible:
150
+ summary["value_counts"] = dict(
151
+ sorted(Counter(str(value) for value in row_values if value is not None).items())
152
+ )
153
+ summary["quality_pass_counts_by_value"] = dict(
154
+ sorted(
155
+ Counter(
156
+ str(value)
157
+ for value, row in zip(row_values, rows)
158
+ if value is not None
159
+ and row["receipt"] is not None
160
+ and row["receipt"]["quality_passed"]
161
+ ).items()
162
+ )
163
+ )
164
+ summary["quality_fraction_by_value"] = {
165
+ key: summary["quality_pass_counts_by_value"].get(key, 0) / count
166
+ for key, count in summary["value_counts"].items()
167
+ }
168
+ result[name] = summary
169
+ return result