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.
- agentloop/__init__.py +136 -0
- agentloop/ablation_protocol.py +229 -0
- agentloop/ablations.py +564 -0
- agentloop/aggregate_cli.py +88 -0
- agentloop/aggregate_stats.py +175 -0
- agentloop/aggregates.py +532 -0
- agentloop/audit.py +39 -0
- agentloop/autoinstrument.py +145 -0
- agentloop/batch_experiments.py +295 -0
- agentloop/batching.py +95 -0
- agentloop/budget_types.py +335 -0
- agentloop/budgets.py +539 -0
- agentloop/calibration.py +872 -0
- agentloop/checkpoint_state.py +306 -0
- agentloop/checkpoint_store.py +86 -0
- agentloop/ci.py +202 -0
- agentloop/cli.py +1314 -0
- agentloop/client.py +228 -0
- agentloop/completion.py +345 -0
- agentloop/completion_types.py +199 -0
- agentloop/config.py +50 -0
- agentloop/context_controls.py +443 -0
- agentloop/context_experiments.py +337 -0
- agentloop/context_types.py +216 -0
- agentloop/costs.py +667 -0
- agentloop/cursor_validation.py +48 -0
- agentloop/decorators.py +445 -0
- agentloop/demo.py +300 -0
- agentloop/doctor.py +341 -0
- agentloop/drift.py +361 -0
- agentloop/drift_cli.py +147 -0
- agentloop/drift_types.py +110 -0
- agentloop/drift_windows.py +340 -0
- agentloop/entrypoint.py +201 -0
- agentloop/estimates.py +133 -0
- agentloop/events.py +75 -0
- agentloop/experiment_artifacts.py +80 -0
- agentloop/experiment_observations.py +169 -0
- agentloop/experiment_reports.py +545 -0
- agentloop/experiment_types.py +355 -0
- agentloop/experiments.py +392 -0
- agentloop/exporters.py +117 -0
- agentloop/finding_benchmark_types.py +131 -0
- agentloop/finding_benchmarks.py +424 -0
- agentloop/findings.py +419 -0
- agentloop/graph.py +464 -0
- agentloop/harness.py +825 -0
- agentloop/harness_bench/__init__.py +1 -0
- agentloop/harness_bench/cli.py +77 -0
- agentloop/harness_bench/drivers.py +96 -0
- agentloop/harness_bench/offline.py +507 -0
- agentloop/harness_evidence.py +585 -0
- agentloop/harness_usage.py +91 -0
- agentloop/html_report.py +860 -0
- agentloop/integrations/__init__.py +1 -0
- agentloop/integrations/crewai.py +144 -0
- agentloop/integrations/harbor/__init__.py +1 -0
- agentloop/integrations/harbor/atif.py +982 -0
- agentloop/integrations/harbor/atif_validation.py +329 -0
- agentloop/integrations/harbor/cli.py +207 -0
- agentloop/integrations/harbor/manifest.py +86 -0
- agentloop/integrations/harbor/trial_evidence.py +196 -0
- agentloop/integrations/harbor/trials.py +1109 -0
- agentloop/integrations/harbor/verifier.py +118 -0
- agentloop/integrations/langgraph.py +150 -0
- agentloop/integrations/langgraph_harness.py +466 -0
- agentloop/integrations/langgraph_recovery.py +366 -0
- agentloop/integrations/omnigent/__init__.py +1 -0
- agentloop/integrations/omnigent/cli.py +78 -0
- agentloop/integrations/omnigent/evidence.py +93 -0
- agentloop/integrations/omnigent/telemetry.py +422 -0
- agentloop/integrations/openai.py +415 -0
- agentloop/integrations/openai_agents.py +291 -0
- agentloop/integrations/sdk.py +82 -0
- agentloop/integrations/vercel_ai.py +83 -0
- agentloop/interoperability/__init__.py +5 -0
- agentloop/interoperability/artifacts.py +112 -0
- agentloop/interoperability/cli.py +59 -0
- agentloop/interoperability/cohort_evidence.py +175 -0
- agentloop/interoperability/cohorts.py +681 -0
- agentloop/interoperability/contracts.py +427 -0
- agentloop/interoperability/coordination.py +554 -0
- agentloop/interoperability/evidence.py +215 -0
- agentloop/interoperability/otlp_jsonl.py +775 -0
- agentloop/interoperability/otlp_validation.py +496 -0
- agentloop/interoperability/registry.py +561 -0
- agentloop/interoperability/validation.py +333 -0
- agentloop/intervention_service.py +110 -0
- agentloop/intervention_store.py +118 -0
- agentloop/interventions.py +190 -0
- agentloop/issues.py +90 -0
- agentloop/judgment_adapters.py +125 -0
- agentloop/judgment_benchmark_types.py +98 -0
- agentloop/judgment_benchmarks.py +606 -0
- agentloop/judgment_types.py +241 -0
- agentloop/judgment_views.py +38 -0
- agentloop/judgments.py +342 -0
- agentloop/loop_guards.py +274 -0
- agentloop/loop_types.py +66 -0
- agentloop/markdown.py +102 -0
- agentloop/metrics.py +342 -0
- agentloop/migrations.py +303 -0
- agentloop/model_routing.py +470 -0
- agentloop/onboarding.py +315 -0
- agentloop/operations.py +44 -0
- agentloop/optimizer.py +199 -0
- agentloop/otel.py +715 -0
- agentloop/otel_ids.py +81 -0
- agentloop/otel_semantics.py +134 -0
- agentloop/parallelism.py +141 -0
- agentloop/patches.py +603 -0
- agentloop/plan_export.py +126 -0
- agentloop/policy_promotion.py +325 -0
- agentloop/promotion_evidence.py +304 -0
- agentloop/promotion_types.py +209 -0
- agentloop/quality.py +446 -0
- agentloop/ranking.py +529 -0
- agentloop/recovery_evidence.py +96 -0
- agentloop/replay.py +753 -0
- agentloop/retention.py +375 -0
- agentloop/retention_types.py +113 -0
- agentloop/reuse_experiments.py +247 -0
- agentloop/routing_checks.py +116 -0
- agentloop/routing_types.py +241 -0
- agentloop/rules.py +396 -0
- agentloop/runtime.py +286 -0
- agentloop/savings.py +158 -0
- agentloop/scheduling_plan.py +99 -0
- agentloop/scheduling_types.py +169 -0
- agentloop/schema.py +257 -0
- agentloop/semantic_rules.py +200 -0
- agentloop/semantic_waste.py +345 -0
- agentloop/semantic_waste_types.py +88 -0
- agentloop/server.py +494 -0
- agentloop/store.py +1429 -0
- agentloop/structural_experiments.py +431 -0
- agentloop/structural_types.py +113 -0
- agentloop/structured_quality.py +1020 -0
- agentloop/studies.py +629 -0
- agentloop/study_statistics.py +58 -0
- agentloop/substitution_evidence.py +199 -0
- agentloop/substitution_export.py +190 -0
- agentloop/substitution_reports.py +407 -0
- agentloop/substitution_types.py +284 -0
- agentloop/substitutions.py +267 -0
- agentloop/timing.py +142 -0
- agentloop/tokens.py +188 -0
- agentloop/tool_scheduling.py +410 -0
- agentloop/trace_sources.py +31 -0
- agentloop/tracer.py +494 -0
- agentloop/value.py +306 -0
- agentloop/version.py +3 -0
- agentloop/workflow_types.py +264 -0
- agentloop/workflows.py +284 -0
- agentloop_profiler-0.1.0.dist-info/METADATA +513 -0
- agentloop_profiler-0.1.0.dist-info/RECORD +166 -0
- agentloop_profiler-0.1.0.dist-info/WHEEL +5 -0
- agentloop_profiler-0.1.0.dist-info/entry_points.txt +2 -0
- agentloop_profiler-0.1.0.dist-info/licenses/LICENSE +201 -0
- agentloop_profiler-0.1.0.dist-info/licenses/THIRD_PARTY_LICENSES.md +101 -0
- agentloop_profiler-0.1.0.dist-info/top_level.txt +2 -0
- dashboard/__init__.py +5 -0
- dashboard/app.py +922 -0
- dashboard/streamlit_compat.py +63 -0
- dashboard/trace_upload.py +57 -0
- dashboard/value_view.py +105 -0
agentloop/ranking.py
ADDED
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
"""Deterministic investigation ordering; scores never authorize a change."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
import re
|
|
7
|
+
from collections import Counter
|
|
8
|
+
from copy import deepcopy
|
|
9
|
+
from enum import Enum
|
|
10
|
+
|
|
11
|
+
from agentloop.savings import SavingsItem, select_compatible
|
|
12
|
+
|
|
13
|
+
RANKING_KEY = "agentloop.ranking_inputs"
|
|
14
|
+
RANKING_VERSION = "1.0"
|
|
15
|
+
SORT_FIELDS = (
|
|
16
|
+
"priority",
|
|
17
|
+
"impact",
|
|
18
|
+
"frequency",
|
|
19
|
+
"latency",
|
|
20
|
+
"cost",
|
|
21
|
+
"tokens",
|
|
22
|
+
"confidence",
|
|
23
|
+
"evidence",
|
|
24
|
+
"quality_risk",
|
|
25
|
+
"reversible",
|
|
26
|
+
"validation_effort",
|
|
27
|
+
"validation_cost",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class RankingSort(str, Enum):
|
|
32
|
+
PRIORITY = "priority"
|
|
33
|
+
IMPACT = "impact"
|
|
34
|
+
FREQUENCY = "frequency"
|
|
35
|
+
LATENCY = "latency"
|
|
36
|
+
COST = "cost"
|
|
37
|
+
TOKENS = "tokens"
|
|
38
|
+
CONFIDENCE = "confidence"
|
|
39
|
+
EVIDENCE = "evidence"
|
|
40
|
+
QUALITY_RISK = "quality_risk"
|
|
41
|
+
REVERSIBLE = "reversible"
|
|
42
|
+
VALIDATION_EFFORT = "validation_effort"
|
|
43
|
+
VALIDATION_COST = "validation_cost"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
REQUIRED_INPUTS = ("quality_risk", "reversible", "validation_effort_minutes", "validation_cost_usd")
|
|
47
|
+
_CONFIDENCE = {"high": 3, "medium": 2, "low": 1}
|
|
48
|
+
_EVIDENCE = {
|
|
49
|
+
"observed": 3,
|
|
50
|
+
"declared": 3,
|
|
51
|
+
"deterministic_inference": 2,
|
|
52
|
+
"inferred": 1,
|
|
53
|
+
"semantic_judgment": 1,
|
|
54
|
+
}
|
|
55
|
+
_KEY = re.compile(r"[A-Za-z0-9_.:-]{1,96}\Z")
|
|
56
|
+
ORDER = (
|
|
57
|
+
"ready_to_test",
|
|
58
|
+
"confidence",
|
|
59
|
+
"evidence",
|
|
60
|
+
"quality_risk",
|
|
61
|
+
"frequency",
|
|
62
|
+
"impact",
|
|
63
|
+
"validation_effort",
|
|
64
|
+
"validation_cost",
|
|
65
|
+
"latency",
|
|
66
|
+
"cost",
|
|
67
|
+
"tokens",
|
|
68
|
+
"identity",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def requirements(values):
|
|
73
|
+
if (
|
|
74
|
+
not isinstance(values, (list, tuple))
|
|
75
|
+
or len(values) > 32
|
|
76
|
+
or any(not isinstance(value, str) or not _KEY.fullmatch(value) for value in values)
|
|
77
|
+
):
|
|
78
|
+
raise ValueError("ranking requirements must be bounded input identifiers")
|
|
79
|
+
result = tuple(sorted(set(REQUIRED_INPUTS) | set(values)))
|
|
80
|
+
if len(result) > 32:
|
|
81
|
+
raise ValueError("ranking requirements are limited to 32 including standard inputs")
|
|
82
|
+
return result
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _number(value, *, integer=False):
|
|
86
|
+
try:
|
|
87
|
+
return (
|
|
88
|
+
(type(value) is int if integer else type(value) in {int, float})
|
|
89
|
+
and math.isfinite(value)
|
|
90
|
+
and value >= 0
|
|
91
|
+
)
|
|
92
|
+
except (OverflowError, TypeError):
|
|
93
|
+
return False
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _input(inputs, name, validate, kind=None):
|
|
97
|
+
item = inputs.get(name)
|
|
98
|
+
if not isinstance(item, dict) or set(item) != {"value", "kind", "source_ref"}:
|
|
99
|
+
return None
|
|
100
|
+
if (
|
|
101
|
+
not isinstance(item["kind"], str)
|
|
102
|
+
or item["kind"] not in {"observed", "declared", "estimated"}
|
|
103
|
+
or (kind is not None and item["kind"] != kind)
|
|
104
|
+
or not isinstance(item["source_ref"], str)
|
|
105
|
+
or not item["source_ref"].strip()
|
|
106
|
+
or len(item["source_ref"]) > 512
|
|
107
|
+
):
|
|
108
|
+
return None
|
|
109
|
+
return deepcopy(item) if validate(item["value"]) else None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _component(value, kind, source):
|
|
113
|
+
return {"value": value, "kind": kind, "source_ref": source}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _mapping(value):
|
|
117
|
+
return value if isinstance(value, dict) else {}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _sum(values):
|
|
121
|
+
try:
|
|
122
|
+
result = math.fsum(values)
|
|
123
|
+
return result if _number(result) else None
|
|
124
|
+
except (OverflowError, ValueError):
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _units(components):
|
|
129
|
+
for name, unit in {
|
|
130
|
+
"impact": "ms_of_recorded_span_work",
|
|
131
|
+
"frequency": "included_runs",
|
|
132
|
+
"latency": "ms",
|
|
133
|
+
"cost": "USD",
|
|
134
|
+
"tokens": "tokens",
|
|
135
|
+
"validation_effort": "minutes",
|
|
136
|
+
"validation_cost": "USD",
|
|
137
|
+
}.items():
|
|
138
|
+
components[name]["unit"] = unit
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def finding_ranking(finding, *, inputs=None, frequency=1):
|
|
142
|
+
"""Use explicit annotations plus preserved canonical evidence/estimates.
|
|
143
|
+
|
|
144
|
+
Span work is cumulative observed duration, not elapsed savings. Confidence
|
|
145
|
+
categories and evidence tiers are ordinal labels, not probabilities.
|
|
146
|
+
"""
|
|
147
|
+
inputs = inputs if isinstance(inputs, dict) else {}
|
|
148
|
+
retained, invalid = {}, []
|
|
149
|
+
raw_spans = finding.get("affected_spans", ())
|
|
150
|
+
spans = (
|
|
151
|
+
{span for span in raw_spans if isinstance(span, str)}
|
|
152
|
+
if isinstance(raw_spans, (list, tuple))
|
|
153
|
+
else set()
|
|
154
|
+
)
|
|
155
|
+
evidence = finding.get("evidence", ())
|
|
156
|
+
evidence = evidence if isinstance(evidence, (list, tuple)) else ()
|
|
157
|
+
observed = {}
|
|
158
|
+
conflicts = False
|
|
159
|
+
for row in evidence:
|
|
160
|
+
if (
|
|
161
|
+
not isinstance(row, dict)
|
|
162
|
+
or not isinstance(row.get("span_id"), str)
|
|
163
|
+
or row.get("span_id") not in spans
|
|
164
|
+
or not _number(row.get("duration_ms"))
|
|
165
|
+
):
|
|
166
|
+
continue
|
|
167
|
+
identity = row["span_id"]
|
|
168
|
+
if identity in observed and observed[identity] != row["duration_ms"]:
|
|
169
|
+
conflicts = True
|
|
170
|
+
observed[identity] = row["duration_ms"]
|
|
171
|
+
complete = bool(spans) and set(observed) == spans and not conflicts
|
|
172
|
+
confidence = finding.get("confidence", "unknown")
|
|
173
|
+
confidence = confidence if isinstance(confidence, str) else "unknown"
|
|
174
|
+
level = finding.get("evidence_level", "unknown")
|
|
175
|
+
level = level if isinstance(level, str) else "unknown"
|
|
176
|
+
estimate = finding.get("estimate")
|
|
177
|
+
estimate = estimate if isinstance(estimate, dict) else {}
|
|
178
|
+
savings = finding.get("savings")
|
|
179
|
+
savings = savings if isinstance(savings, dict) else {}
|
|
180
|
+
unmodeled = estimate.get("unmodeled_metrics", ())
|
|
181
|
+
if not isinstance(unmodeled, (list, tuple)):
|
|
182
|
+
unmodeled = ("latency", "cost")
|
|
183
|
+
source = (
|
|
184
|
+
"estimator:"
|
|
185
|
+
+ str(estimate.get("estimator_id", "unavailable"))
|
|
186
|
+
+ ":"
|
|
187
|
+
+ str(estimate.get("estimator_version", "unknown"))
|
|
188
|
+
)
|
|
189
|
+
latency = savings.get("estimated_latency_savings_ms")
|
|
190
|
+
cost = savings.get("estimated_cost_savings_usd")
|
|
191
|
+
cost_status = _mapping(estimate.get("inputs")).get(
|
|
192
|
+
"cost_status", _mapping(finding.get("metadata")).get("cost_status")
|
|
193
|
+
)
|
|
194
|
+
modeled = all(
|
|
195
|
+
isinstance(estimate.get(key), str) and estimate[key]
|
|
196
|
+
for key in ("estimator_id", "estimator_version")
|
|
197
|
+
)
|
|
198
|
+
latency = latency if modeled and "latency" not in unmodeled and _number(latency) else None
|
|
199
|
+
cost = (
|
|
200
|
+
cost
|
|
201
|
+
if modeled
|
|
202
|
+
and "cost" not in unmodeled
|
|
203
|
+
and isinstance(cost_status, str)
|
|
204
|
+
and cost_status in {"complete", "empty"}
|
|
205
|
+
and _number(cost)
|
|
206
|
+
else None
|
|
207
|
+
)
|
|
208
|
+
components = {
|
|
209
|
+
"impact": _component(
|
|
210
|
+
_sum(observed.values()) if complete else None,
|
|
211
|
+
"observed",
|
|
212
|
+
"recorded_span_durations_nonadditive",
|
|
213
|
+
),
|
|
214
|
+
"frequency": _component(
|
|
215
|
+
frequency if _number(frequency, integer=True) else None,
|
|
216
|
+
"observed",
|
|
217
|
+
"included_distinct_runs",
|
|
218
|
+
),
|
|
219
|
+
"confidence": _component(
|
|
220
|
+
confidence if confidence in _CONFIDENCE else None,
|
|
221
|
+
"declared",
|
|
222
|
+
"rule:"
|
|
223
|
+
+ str(finding.get("rule_id", "unknown"))
|
|
224
|
+
+ ":"
|
|
225
|
+
+ str(finding.get("rule_version", "unknown")),
|
|
226
|
+
),
|
|
227
|
+
"evidence": _component(
|
|
228
|
+
level if complete and level in _EVIDENCE else None,
|
|
229
|
+
"declared",
|
|
230
|
+
"rule_evidence_level_and_span_coverage",
|
|
231
|
+
),
|
|
232
|
+
"latency": _component(latency, "estimated", source),
|
|
233
|
+
"cost": _component(cost, "estimated", source),
|
|
234
|
+
"tokens": _component(None, "estimated", "unavailable"),
|
|
235
|
+
}
|
|
236
|
+
for name, key, validate, kind in (
|
|
237
|
+
(
|
|
238
|
+
"quality_risk",
|
|
239
|
+
"quality_risk",
|
|
240
|
+
lambda value: value in {"low", "medium", "high"} if isinstance(value, str) else False,
|
|
241
|
+
"declared",
|
|
242
|
+
),
|
|
243
|
+
("reversible", "reversible", lambda value: type(value) is bool, "declared"),
|
|
244
|
+
(
|
|
245
|
+
"evidence_level",
|
|
246
|
+
"evidence",
|
|
247
|
+
lambda value: isinstance(value, str) and value in _EVIDENCE,
|
|
248
|
+
"declared",
|
|
249
|
+
),
|
|
250
|
+
("validation_effort_minutes", "validation_effort", _number, "declared"),
|
|
251
|
+
("validation_cost_usd", "validation_cost", _number, "declared"),
|
|
252
|
+
("estimated_cost_savings_usd", "cost", _number, "estimated"),
|
|
253
|
+
("estimated_latency_savings_ms", "latency", _number, "estimated"),
|
|
254
|
+
(
|
|
255
|
+
"estimated_token_savings",
|
|
256
|
+
"tokens",
|
|
257
|
+
lambda value: _number(value, integer=True),
|
|
258
|
+
"estimated",
|
|
259
|
+
),
|
|
260
|
+
):
|
|
261
|
+
supplied = _input(inputs, name, validate, kind)
|
|
262
|
+
if supplied is not None:
|
|
263
|
+
components[key] = supplied
|
|
264
|
+
retained[name] = deepcopy(supplied)
|
|
265
|
+
elif key not in components:
|
|
266
|
+
components[key] = _component(None, kind, "unavailable")
|
|
267
|
+
if name in inputs and supplied is None:
|
|
268
|
+
invalid.append(name)
|
|
269
|
+
try:
|
|
270
|
+
needed = requirements(finding.get("ranking_requirements", ()))
|
|
271
|
+
except ValueError:
|
|
272
|
+
needed = requirements(())
|
|
273
|
+
invalid.append("ranking_requirements")
|
|
274
|
+
aliases = {
|
|
275
|
+
"validation_effort_minutes": "validation_effort",
|
|
276
|
+
"validation_cost_usd": "validation_cost",
|
|
277
|
+
}
|
|
278
|
+
missing = []
|
|
279
|
+
for name in needed:
|
|
280
|
+
component = components.get(aliases.get(name, name))
|
|
281
|
+
if component is None:
|
|
282
|
+
component = _input(
|
|
283
|
+
inputs,
|
|
284
|
+
name,
|
|
285
|
+
lambda value: (
|
|
286
|
+
type(value) is bool
|
|
287
|
+
or _number(value)
|
|
288
|
+
or (isinstance(value, str) and 0 < len(value.strip()) <= 256)
|
|
289
|
+
),
|
|
290
|
+
)
|
|
291
|
+
components[name] = component or _component(None, "declared", "unavailable")
|
|
292
|
+
if component is not None:
|
|
293
|
+
retained[name] = deepcopy(component)
|
|
294
|
+
if component is None or component["value"] is None:
|
|
295
|
+
missing.append(name)
|
|
296
|
+
reasons = ["missing_input:" + name for name in sorted(missing)]
|
|
297
|
+
reasons.extend("invalid_input:" + name for name in sorted(invalid))
|
|
298
|
+
if cost is None and components["cost"]["value"] is None:
|
|
299
|
+
reasons.append("cost_opportunity_unavailable")
|
|
300
|
+
if not complete or components["impact"]["value"] is None:
|
|
301
|
+
reasons.append("span_evidence_incomplete")
|
|
302
|
+
if _CONFIDENCE.get(confidence, 0) < 2:
|
|
303
|
+
reasons.append("confidence_weak_or_unknown")
|
|
304
|
+
if not all(
|
|
305
|
+
isinstance(finding.get(key), str) and finding[key] for key in ("rule_id", "rule_version")
|
|
306
|
+
):
|
|
307
|
+
reasons.append("confidence_provenance_missing")
|
|
308
|
+
if _EVIDENCE.get(components["evidence"]["value"], 0) < 2:
|
|
309
|
+
reasons.append("evidence_weak_or_unknown")
|
|
310
|
+
if components["quality_risk"]["value"] == "high":
|
|
311
|
+
reasons.append("high_quality_risk_requires_review")
|
|
312
|
+
if components["reversible"]["value"] is False:
|
|
313
|
+
reasons.append("irreversible_change_requires_review")
|
|
314
|
+
if all(components[key]["value"] in {None, 0} for key in ("latency", "cost", "tokens")):
|
|
315
|
+
reasons.append("positive_opportunity_unavailable")
|
|
316
|
+
_units(components)
|
|
317
|
+
return {
|
|
318
|
+
"schema_version": RANKING_VERSION,
|
|
319
|
+
"method": "explicit_lexicographic",
|
|
320
|
+
"status": "ready_to_test" if not reasons else "needs_evidence_or_review",
|
|
321
|
+
"components": components,
|
|
322
|
+
"required_inputs": list(needed),
|
|
323
|
+
"reasons": sorted(set(reasons)),
|
|
324
|
+
"order": list(ORDER),
|
|
325
|
+
"estimate_provenance": deepcopy(estimate) or None,
|
|
326
|
+
"source_inputs": retained,
|
|
327
|
+
"invalid_inputs": sorted(invalid),
|
|
328
|
+
"automatic_application_allowed": False,
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _identity(item):
|
|
333
|
+
return item.get("finding_id", item.get("queue_id", ""))
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _priority(item):
|
|
337
|
+
rank = item["ranking"]
|
|
338
|
+
value = {key: part["value"] for key, part in rank["components"].items()}
|
|
339
|
+
return (
|
|
340
|
+
rank["status"] != "ready_to_test",
|
|
341
|
+
-_CONFIDENCE.get(value["confidence"], 0),
|
|
342
|
+
-_EVIDENCE.get(value["evidence"], 0),
|
|
343
|
+
{"low": 0, "medium": 1, "high": 2}.get(value["quality_risk"], 3),
|
|
344
|
+
-(value["frequency"] or 0),
|
|
345
|
+
-(value["impact"] or 0),
|
|
346
|
+
math.inf if value["validation_effort"] is None else value["validation_effort"],
|
|
347
|
+
math.inf if value["validation_cost"] is None else value["validation_cost"],
|
|
348
|
+
-(value["latency"] or 0),
|
|
349
|
+
-(value["cost"] or 0),
|
|
350
|
+
-(value["tokens"] or 0),
|
|
351
|
+
_identity(item),
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def sort_ranked(items, sort_by="priority"):
|
|
356
|
+
if sort_by not in SORT_FIELDS:
|
|
357
|
+
raise ValueError("unsupported finding ranking dimension")
|
|
358
|
+
if sort_by == "priority":
|
|
359
|
+
return sorted(items, key=_priority)
|
|
360
|
+
|
|
361
|
+
def key(item):
|
|
362
|
+
value = item["ranking"]["components"][sort_by]["value"]
|
|
363
|
+
if sort_by == "confidence":
|
|
364
|
+
value = _CONFIDENCE.get(value)
|
|
365
|
+
elif sort_by == "evidence":
|
|
366
|
+
value = _EVIDENCE.get(value)
|
|
367
|
+
elif sort_by == "quality_risk":
|
|
368
|
+
value = {"low": 0, "medium": 1, "high": 2}.get(value)
|
|
369
|
+
return (
|
|
370
|
+
value is None,
|
|
371
|
+
0
|
|
372
|
+
if value is None
|
|
373
|
+
else value
|
|
374
|
+
if sort_by in {"validation_effort", "validation_cost", "quality_risk"}
|
|
375
|
+
else -value,
|
|
376
|
+
_identity(item),
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
return sorted(items, key=key)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def rank_findings(findings, *, inputs=None, sort_by="priority"):
|
|
383
|
+
inputs = inputs if isinstance(inputs, dict) else {}
|
|
384
|
+
result = deepcopy(findings)
|
|
385
|
+
for finding in result:
|
|
386
|
+
declared = _mapping(inputs.get(finding.get("rule_id")))
|
|
387
|
+
if finding["finding_id"] in inputs:
|
|
388
|
+
selected = inputs[finding["finding_id"]]
|
|
389
|
+
declared = {**declared, **selected} if isinstance(selected, dict) else {}
|
|
390
|
+
finding["ranking"] = finding_ranking(finding, inputs=declared)
|
|
391
|
+
return finalize_ranking(result, sort_by=sort_by)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def finalize_ranking(items, *, sort_by="priority"):
|
|
395
|
+
prioritized = sort_ranked(items)
|
|
396
|
+
ready_count = sum(item["ranking"]["status"] == "ready_to_test" for item in prioritized)
|
|
397
|
+
for index, item in enumerate(prioritized):
|
|
398
|
+
item["ranking"]["priority_rank"] = index + 1
|
|
399
|
+
# Compatibility numeric field: an ordinal among ready investigations,
|
|
400
|
+
# never dollars, saved time, utility, or an auto-application threshold.
|
|
401
|
+
item["priority_score"] = max(0, ready_count - index)
|
|
402
|
+
item["ranking"]["score_meaning"] = "ordinal_among_ready_findings_zero_when_ineligible"
|
|
403
|
+
return sort_ranked(prioritized, sort_by)
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def cluster_ranking(members, *, selection_cache=None):
|
|
407
|
+
"""Aggregate conservatively and reuse compatible-span opportunity selection."""
|
|
408
|
+
members = sorted(
|
|
409
|
+
members,
|
|
410
|
+
key=lambda item: (
|
|
411
|
+
str(item.get("project_id", "")),
|
|
412
|
+
item["run_id"],
|
|
413
|
+
item.get("finding_id", ""),
|
|
414
|
+
),
|
|
415
|
+
)
|
|
416
|
+
ranks, per_run, observed = [], {}, {}
|
|
417
|
+
conflicting_spans = False
|
|
418
|
+
for member in members:
|
|
419
|
+
finding = member.get("finding")
|
|
420
|
+
finding = finding if isinstance(finding, dict) else {}
|
|
421
|
+
previous = _mapping(finding.get("ranking"))
|
|
422
|
+
rank = finding_ranking(finding, inputs=previous.get("source_inputs", {}))
|
|
423
|
+
old_invalid = previous.get("invalid_inputs", ())
|
|
424
|
+
if isinstance(old_invalid, (list, tuple)):
|
|
425
|
+
for name in old_invalid:
|
|
426
|
+
if isinstance(name, str) and _KEY.fullmatch(name):
|
|
427
|
+
rank["reasons"].append("invalid_input:" + name)
|
|
428
|
+
rank["status"] = "needs_evidence_or_review"
|
|
429
|
+
ranks.append(rank)
|
|
430
|
+
key = (member.get("project_id"), member["run_id"])
|
|
431
|
+
per_run.setdefault(key, []).append(
|
|
432
|
+
SavingsItem(
|
|
433
|
+
frozenset(finding.get("affected_spans") or ()),
|
|
434
|
+
rank["components"]["latency"]["value"] or 0,
|
|
435
|
+
rank["components"]["cost"]["value"] or 0,
|
|
436
|
+
)
|
|
437
|
+
)
|
|
438
|
+
for row in finding.get("evidence", ()):
|
|
439
|
+
if (
|
|
440
|
+
isinstance(row, dict)
|
|
441
|
+
and row.get("span_id") in (finding.get("affected_spans") or ())
|
|
442
|
+
and _number(row.get("duration_ms"))
|
|
443
|
+
):
|
|
444
|
+
span_key = (key, row["span_id"])
|
|
445
|
+
if span_key in observed and observed[span_key] != row["duration_ms"]:
|
|
446
|
+
conflicting_spans = True
|
|
447
|
+
observed[span_key] = row["duration_ms"]
|
|
448
|
+
result = deepcopy(ranks[0])
|
|
449
|
+
components = result["components"]
|
|
450
|
+
reasons = set(reason for rank in ranks for reason in rank["reasons"])
|
|
451
|
+
if conflicting_spans:
|
|
452
|
+
reasons.add("conflicting_span_observations")
|
|
453
|
+
required = set(name for rank in ranks for name in rank["required_inputs"])
|
|
454
|
+
for key in (
|
|
455
|
+
"quality_risk",
|
|
456
|
+
"reversible",
|
|
457
|
+
"validation_effort",
|
|
458
|
+
"validation_cost",
|
|
459
|
+
"confidence",
|
|
460
|
+
"evidence",
|
|
461
|
+
):
|
|
462
|
+
values = [rank["components"][key]["value"] for rank in ranks]
|
|
463
|
+
value = None
|
|
464
|
+
if all(item is not None for item in values):
|
|
465
|
+
if key == "quality_risk":
|
|
466
|
+
value = max(values, key={"low": 0, "medium": 1, "high": 2}.get)
|
|
467
|
+
elif key == "confidence":
|
|
468
|
+
value = min(values, key=_CONFIDENCE.get)
|
|
469
|
+
elif key == "evidence":
|
|
470
|
+
value = min(values, key=_EVIDENCE.get)
|
|
471
|
+
elif key == "reversible":
|
|
472
|
+
value = all(values)
|
|
473
|
+
else:
|
|
474
|
+
value = max(values)
|
|
475
|
+
components[key] = _component(value, "declared", "conservative_member_aggregation")
|
|
476
|
+
totals = []
|
|
477
|
+
for key, items in per_run.items():
|
|
478
|
+
cached = (selection_cache or {}).get(key)
|
|
479
|
+
totals.append(
|
|
480
|
+
cached[1]
|
|
481
|
+
if cached is not None and Counter(cached[0]) == Counter(items)
|
|
482
|
+
else select_compatible(items)
|
|
483
|
+
)
|
|
484
|
+
for key, attribute in (("latency", "latency_ms"), ("cost", "cost_usd")):
|
|
485
|
+
components[key] = _component(
|
|
486
|
+
_sum(getattr(selection, attribute) for selection in totals)
|
|
487
|
+
if all(rank["components"][key]["value"] is not None for rank in ranks)
|
|
488
|
+
else None,
|
|
489
|
+
"estimated",
|
|
490
|
+
"compatible_span_selection_per_run",
|
|
491
|
+
)
|
|
492
|
+
components["tokens"] = _component(None, "estimated", "see_member_estimates")
|
|
493
|
+
components["impact"] = _component(
|
|
494
|
+
_sum(observed.values())
|
|
495
|
+
if not conflicting_spans
|
|
496
|
+
and all(rank["components"]["impact"]["value"] is not None for rank in ranks)
|
|
497
|
+
else None,
|
|
498
|
+
"observed",
|
|
499
|
+
"distinct_recorded_spans_per_run_nonadditive",
|
|
500
|
+
)
|
|
501
|
+
components["frequency"] = _component(len(per_run), "observed", "included_distinct_project_runs")
|
|
502
|
+
if components["cost"]["value"] is None:
|
|
503
|
+
reasons.add("cost_opportunity_unavailable")
|
|
504
|
+
if components["impact"]["value"] is None:
|
|
505
|
+
reasons.add("span_evidence_incomplete")
|
|
506
|
+
if not all(selection.optimal for selection in totals):
|
|
507
|
+
reasons.add("opportunity_selection_approximate")
|
|
508
|
+
_units(components)
|
|
509
|
+
result.update(
|
|
510
|
+
status="ready_to_test" if not reasons else "needs_evidence_or_review",
|
|
511
|
+
reasons=sorted(reasons),
|
|
512
|
+
required_inputs=sorted(required),
|
|
513
|
+
members=[
|
|
514
|
+
{
|
|
515
|
+
"run_id": member["run_id"],
|
|
516
|
+
"project_id": member.get("project_id"),
|
|
517
|
+
"finding_id": member.get("finding_id"),
|
|
518
|
+
"ranking": rank,
|
|
519
|
+
}
|
|
520
|
+
for member, rank in zip(members, ranks)
|
|
521
|
+
],
|
|
522
|
+
estimate_provenance=[rank["estimate_provenance"] for rank in ranks],
|
|
523
|
+
source_inputs={},
|
|
524
|
+
selection={
|
|
525
|
+
"optimal": all(selection.optimal for selection in totals),
|
|
526
|
+
"algorithms": sorted({selection.algorithm for selection in totals}),
|
|
527
|
+
},
|
|
528
|
+
)
|
|
529
|
+
return result
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Payload-free checkpoint decisions joined to native harness provenance."""
|
|
2
|
+
|
|
3
|
+
from copy import deepcopy
|
|
4
|
+
from threading import RLock
|
|
5
|
+
from uuid import uuid4
|
|
6
|
+
|
|
7
|
+
from agentloop.checkpoint_state import _require
|
|
8
|
+
from agentloop.context_types import fingerprint
|
|
9
|
+
from agentloop.harness import Decision, Harness, HarnessConfig, HarnessControlError, Hook, Policy
|
|
10
|
+
from agentloop.tracer import current_trace
|
|
11
|
+
|
|
12
|
+
RECOVERY_KEY = "agentloop.recovery"
|
|
13
|
+
_LOCK = RLock()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def record_recovery(
|
|
17
|
+
action,
|
|
18
|
+
reason,
|
|
19
|
+
*,
|
|
20
|
+
reference=None,
|
|
21
|
+
parent_run_id=None,
|
|
22
|
+
attempt_run_id=None,
|
|
23
|
+
status="incomplete",
|
|
24
|
+
store_durable=False,
|
|
25
|
+
):
|
|
26
|
+
"""Record a separate admission audit, never a fresh execution budget."""
|
|
27
|
+
identity = "recovery_" + uuid4().hex
|
|
28
|
+
entry = {
|
|
29
|
+
"recovery_id": identity,
|
|
30
|
+
"action": action,
|
|
31
|
+
"reason": reason,
|
|
32
|
+
"checkpoint_ref_hash": None if reference is None else fingerprint(reference),
|
|
33
|
+
"parent_run_id": parent_run_id,
|
|
34
|
+
"attempt_run_id": attempt_run_id,
|
|
35
|
+
"status": status,
|
|
36
|
+
"store_durable": store_durable,
|
|
37
|
+
"scope": "recovery_admission_only",
|
|
38
|
+
}
|
|
39
|
+
trace = current_trace()
|
|
40
|
+
envelope = None
|
|
41
|
+
if trace is not None:
|
|
42
|
+
with _LOCK:
|
|
43
|
+
envelope = trace.metadata.setdefault(
|
|
44
|
+
RECOVERY_KEY, {"schema_version": "1.0", "records": {}}
|
|
45
|
+
)
|
|
46
|
+
_require(
|
|
47
|
+
type(envelope) is dict
|
|
48
|
+
and envelope.get("schema_version") == "1.0"
|
|
49
|
+
and type(envelope.get("records")) is dict
|
|
50
|
+
and len(envelope["records"]) < 10000,
|
|
51
|
+
"invalid_recovery_evidence",
|
|
52
|
+
)
|
|
53
|
+
envelope["records"][identity] = deepcopy(entry)
|
|
54
|
+
policy = Policy(
|
|
55
|
+
"agentloop.recovery",
|
|
56
|
+
"1.0",
|
|
57
|
+
lambda context: Decision(action, reason, ("recovery:" + identity,)),
|
|
58
|
+
hooks=frozenset({Hook("completion")}),
|
|
59
|
+
actions=frozenset({"continue", "escalate"}),
|
|
60
|
+
)
|
|
61
|
+
audit = Harness(HarnessConfig("enforce", (policy,))).start_run("audit_" + uuid4().hex)
|
|
62
|
+
try:
|
|
63
|
+
audit.wrap(lambda: None, boundary="completion", branch_id="recovery")()
|
|
64
|
+
except HarnessControlError:
|
|
65
|
+
# This audit has no task work or task quota. Its escalation is re-raised
|
|
66
|
+
# by the caller as a typed CheckpointError before host invocation.
|
|
67
|
+
pass
|
|
68
|
+
entry["audit_run_id"] = audit.run_id
|
|
69
|
+
entry["audit_call_id"] = audit.results[0].call_id
|
|
70
|
+
if trace is not None:
|
|
71
|
+
with _LOCK:
|
|
72
|
+
_require(trace.metadata.get(RECOVERY_KEY) is envelope, "invalid_recovery_evidence")
|
|
73
|
+
envelope["records"][identity] = deepcopy(entry)
|
|
74
|
+
return entry
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def recovery_status(trace):
|
|
78
|
+
"""Return the latest declared attempt status for study adapters, if present."""
|
|
79
|
+
envelope = trace.metadata.get(RECOVERY_KEY)
|
|
80
|
+
if envelope is None:
|
|
81
|
+
return None
|
|
82
|
+
_require(
|
|
83
|
+
type(envelope) is dict
|
|
84
|
+
and envelope.get("schema_version") == "1.0"
|
|
85
|
+
and type(envelope.get("records")) is dict
|
|
86
|
+
and bool(envelope["records"]),
|
|
87
|
+
"invalid_recovery_evidence",
|
|
88
|
+
)
|
|
89
|
+
latest = list(envelope["records"].values())[-1]
|
|
90
|
+
_require(type(latest) is dict, "invalid_recovery_evidence")
|
|
91
|
+
status = latest.get("status")
|
|
92
|
+
_require(
|
|
93
|
+
status in {"completed", "incomplete", "failed", "cancelled", "stopped"},
|
|
94
|
+
"invalid_recovery_evidence",
|
|
95
|
+
)
|
|
96
|
+
return status
|