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
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""Canonical finding candidates from validated, explicitly scoped investigations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from fractions import Fraction
|
|
6
|
+
from types import SimpleNamespace
|
|
7
|
+
|
|
8
|
+
from agentloop.costs import is_cost_evaluable
|
|
9
|
+
from agentloop.semantic_waste_types import SemanticInvestigation
|
|
10
|
+
from agentloop.timing import event_interval_ms
|
|
11
|
+
from agentloop.tokens import token_status
|
|
12
|
+
|
|
13
|
+
_TITLES = {
|
|
14
|
+
"semantic_redundancy": "Investigate semantically redundant work",
|
|
15
|
+
"low_contribution": "Investigate low-contribution work",
|
|
16
|
+
"semantic_no_progress": "Investigate a sequence with no task progress",
|
|
17
|
+
"retry_usefulness": "Investigate retries without useful new evidence",
|
|
18
|
+
"context_relevance": "Investigate context unrelated to the decision",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _total(values):
|
|
23
|
+
try:
|
|
24
|
+
return float(sum((Fraction(str(value)) for value in values), Fraction()))
|
|
25
|
+
except (ValueError, OverflowError):
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _inputs(context):
|
|
30
|
+
events = {item["event_id"]: item for item in context.report["events"]}
|
|
31
|
+
children = {item.get("parent_id") for item in events.values() if item.get("parent_id")}
|
|
32
|
+
model_events = [item for item in events.values() if item["event_type"] == "model_call"]
|
|
33
|
+
model_costs = context.report.get("cost_breakdown", {}).get("model_calls", [])
|
|
34
|
+
return {
|
|
35
|
+
"events": events,
|
|
36
|
+
"children": children,
|
|
37
|
+
"model_costs": {event["event_id"]: cost for event, cost in zip(model_events, model_costs)}
|
|
38
|
+
if len(model_events) == len(model_costs)
|
|
39
|
+
else {},
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _estimate(context, case, row, inputs):
|
|
44
|
+
events = inputs["events"]
|
|
45
|
+
selected = [events[identity] for identity in case.target_spans]
|
|
46
|
+
leaf = all(item["event_id"] not in inputs["children"] for item in selected)
|
|
47
|
+
all_models = all(item["event_type"] == "model_call" for item in selected)
|
|
48
|
+
selected_token_status = (
|
|
49
|
+
token_status([SimpleNamespace(**item) for item in selected]) if all_models else None
|
|
50
|
+
)
|
|
51
|
+
eligible = bool(case.removal_attribution_ref and leaf and case.family != "context_relevance")
|
|
52
|
+
predictions = {
|
|
53
|
+
"latency_ms": None,
|
|
54
|
+
"cost_usd": None,
|
|
55
|
+
"input_tokens": None,
|
|
56
|
+
"output_tokens": None,
|
|
57
|
+
}
|
|
58
|
+
assumptions = [
|
|
59
|
+
"Selected evidence and approved summaries cover the relevant task context and downstream outcomes.",
|
|
60
|
+
"The finding is a hypothesis; independent paired replay must preserve the caller's quality criteria.",
|
|
61
|
+
]
|
|
62
|
+
if row["basis"] == "deterministic_reuse":
|
|
63
|
+
assumptions.append(
|
|
64
|
+
"The referenced host reuse contract guarantees complete inputs, unchanged operation semantics, and safe result reuse without required side effects or independent verification."
|
|
65
|
+
)
|
|
66
|
+
costs = []
|
|
67
|
+
serial = False
|
|
68
|
+
if eligible:
|
|
69
|
+
assumptions.extend(
|
|
70
|
+
[
|
|
71
|
+
"The caller's removal attribution applies to every selected leaf span, including side effects and downstream dependencies.",
|
|
72
|
+
"Removing those spans requires no replacement work, cache lookup, repair, or additional validation overhead.",
|
|
73
|
+
"Resource estimates are conditional upper bounds on recorded work, not realized savings or a correctness guarantee.",
|
|
74
|
+
]
|
|
75
|
+
)
|
|
76
|
+
if "serial" not in inputs:
|
|
77
|
+
intervals = [event_interval_ms(SimpleNamespace(**item)) for item in events.values()]
|
|
78
|
+
details = context.graph.dependency_summary()
|
|
79
|
+
inputs["serial"] = False
|
|
80
|
+
if (
|
|
81
|
+
all(value is not None for value in intervals)
|
|
82
|
+
and all(
|
|
83
|
+
item["duration_ms"] > 0 or interval[0] == interval[1]
|
|
84
|
+
for item, interval in zip(events.values(), intervals)
|
|
85
|
+
)
|
|
86
|
+
and all(not item.get("parent_id") for item in events.values())
|
|
87
|
+
and (details is None or details["valid"])
|
|
88
|
+
):
|
|
89
|
+
ordered = sorted(intervals)
|
|
90
|
+
inputs["serial"] = all(
|
|
91
|
+
left[1] <= right[0] for left, right in zip(ordered, ordered[1:])
|
|
92
|
+
)
|
|
93
|
+
serial = inputs["serial"]
|
|
94
|
+
if serial:
|
|
95
|
+
predictions["latency_ms"] = _total(item["duration_ms"] for item in selected)
|
|
96
|
+
assumptions.append(
|
|
97
|
+
"The recorded flat, nonoverlapping execution remains serial after the change, so removed span time can shorten elapsed runtime."
|
|
98
|
+
)
|
|
99
|
+
if all_models:
|
|
100
|
+
exact_tokens = selected_token_status in {
|
|
101
|
+
"exact",
|
|
102
|
+
"empty",
|
|
103
|
+
}
|
|
104
|
+
if exact_tokens:
|
|
105
|
+
predictions["input_tokens"] = sum(item["input_tokens"] for item in selected)
|
|
106
|
+
predictions["output_tokens"] = sum(item["output_tokens"] for item in selected)
|
|
107
|
+
by_id = inputs["model_costs"]
|
|
108
|
+
if all(item["event_id"] in by_id for item in selected):
|
|
109
|
+
costs = [
|
|
110
|
+
{"span_id": item["event_id"], **by_id[item["event_id"]]} for item in selected
|
|
111
|
+
]
|
|
112
|
+
if is_cost_evaluable(context.report.get("cost_status")) and all(
|
|
113
|
+
item["state"] == "provider_reported"
|
|
114
|
+
or (item["state"] == "calculated" and exact_tokens)
|
|
115
|
+
for item in costs
|
|
116
|
+
):
|
|
117
|
+
predictions["cost_usd"] = _total(item["amount_usd"] for item in costs)
|
|
118
|
+
return {
|
|
119
|
+
"estimator_id": "semantic_leaf_removal",
|
|
120
|
+
"estimator_version": "1.0",
|
|
121
|
+
"method": "conditional_upper_bound" if eligible else "unavailable_attribution",
|
|
122
|
+
"confidence": row["confidence"],
|
|
123
|
+
"calibrated": False,
|
|
124
|
+
"formula": "conditional removal: sum selected nonoverlapping serial leaf durations; sum separately attributed model billing and exact model token counts",
|
|
125
|
+
"parameters": {},
|
|
126
|
+
"assumptions": assumptions,
|
|
127
|
+
"inputs": {
|
|
128
|
+
"family": case.family,
|
|
129
|
+
"attribution_eligible": eligible,
|
|
130
|
+
"all_model_calls": all_models,
|
|
131
|
+
"token_status": selected_token_status,
|
|
132
|
+
"trace_cost_status": context.report.get("cost_status"),
|
|
133
|
+
"target_spans": list(case.target_spans),
|
|
134
|
+
"leaf_spans": leaf,
|
|
135
|
+
"flat_serial_timing": serial,
|
|
136
|
+
"removal_attribution_ref": case.removal_attribution_ref,
|
|
137
|
+
"recorded_target_work_ms": _total(item["duration_ms"] for item in selected),
|
|
138
|
+
"model_costs": costs,
|
|
139
|
+
},
|
|
140
|
+
"predictions": predictions,
|
|
141
|
+
"unmodeled_metrics": [key for key, value in predictions.items() if value is None],
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def semantic_candidates(context, family):
|
|
146
|
+
from agentloop.rules import FindingCandidate, RecommendationType
|
|
147
|
+
|
|
148
|
+
evidence = context.report.get("semantic_waste")
|
|
149
|
+
if not evidence or evidence["status"] in {"invalid", "unsupported"}:
|
|
150
|
+
return []
|
|
151
|
+
result = []
|
|
152
|
+
inputs = None
|
|
153
|
+
for row in evidence["cases"]:
|
|
154
|
+
if row["status"] != "supported" or row["definition"]["family"] != family:
|
|
155
|
+
continue
|
|
156
|
+
case = SemanticInvestigation.from_dict(row["definition"])
|
|
157
|
+
if inputs is None:
|
|
158
|
+
inputs = _inputs(context)
|
|
159
|
+
estimate = _estimate(context, case, row, inputs)
|
|
160
|
+
observations = {
|
|
161
|
+
"investigation_id": case.investigation_id,
|
|
162
|
+
"criteria_ref": case.criteria_ref,
|
|
163
|
+
"reference_spans": list(case.reference_spans),
|
|
164
|
+
"target_spans": list(case.target_spans),
|
|
165
|
+
"evidence_hash": evidence["evidence_hash"],
|
|
166
|
+
"basis": row["basis"],
|
|
167
|
+
"reuse_contract_ref": case.reuse_contract_ref,
|
|
168
|
+
"judgments": [
|
|
169
|
+
{
|
|
170
|
+
"record_hash": item["record_hash"],
|
|
171
|
+
"judge": item["judge"],
|
|
172
|
+
"uncertainty": item["evaluation"]["uncertainty"],
|
|
173
|
+
"status": item["effective_status"],
|
|
174
|
+
"value": item["effective_value"],
|
|
175
|
+
"evidence_refs": [part["span_id"] for part in item["request"]["evidence"]],
|
|
176
|
+
}
|
|
177
|
+
for item in row["judgments"]
|
|
178
|
+
],
|
|
179
|
+
"validation_criteria": "Run a paired intervention against the original trace, with an independent scorer implementing the referenced task criteria, preserved required side effects, and complete cost/token provenance.",
|
|
180
|
+
}
|
|
181
|
+
result.append(
|
|
182
|
+
FindingCandidate(
|
|
183
|
+
type=RecommendationType(family),
|
|
184
|
+
title=_TITLES[family],
|
|
185
|
+
why="An explicit investigation supports this hypothesis under the recorded task criteria and evidence coverage assumptions.",
|
|
186
|
+
rewrite_hint="Prepare an offline candidate and validate its independent task quality before accepting any step removal or context change.",
|
|
187
|
+
confidence=row["confidence"],
|
|
188
|
+
affected_nodes=list(case.target_spans),
|
|
189
|
+
estimated_latency_savings_ms=estimate["predictions"]["latency_ms"],
|
|
190
|
+
estimated_cost_savings_usd=estimate["predictions"]["cost_usd"],
|
|
191
|
+
evidence_level="deterministic_inference"
|
|
192
|
+
if row["basis"] == "deterministic_reuse"
|
|
193
|
+
else "semantic_judgment",
|
|
194
|
+
assumptions=estimate["assumptions"],
|
|
195
|
+
observations=observations,
|
|
196
|
+
estimate_formula=estimate["formula"],
|
|
197
|
+
estimate=estimate,
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
return result
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
"""Optional offline investigations; report construction only reads saved evidence."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from copy import deepcopy
|
|
7
|
+
from types import SimpleNamespace
|
|
8
|
+
|
|
9
|
+
from agentloop.judgment_types import (
|
|
10
|
+
JUDGMENT_KEY,
|
|
11
|
+
JudgmentRequest,
|
|
12
|
+
JudgmentSpec,
|
|
13
|
+
canonical,
|
|
14
|
+
fingerprint,
|
|
15
|
+
finite,
|
|
16
|
+
)
|
|
17
|
+
from agentloop.judgments import (
|
|
18
|
+
JudgmentSession,
|
|
19
|
+
_bound,
|
|
20
|
+
attach_judgment,
|
|
21
|
+
judgment_request,
|
|
22
|
+
read_judgments,
|
|
23
|
+
)
|
|
24
|
+
from agentloop.operations import operation_kind
|
|
25
|
+
from agentloop.semantic_waste_types import (
|
|
26
|
+
QUESTIONS,
|
|
27
|
+
SEMANTIC_WASTE_KEY,
|
|
28
|
+
SEMANTIC_WASTE_VERSION,
|
|
29
|
+
SemanticInvestigation,
|
|
30
|
+
)
|
|
31
|
+
from agentloop.timing import event_interval_ms
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _copy(value):
|
|
35
|
+
return json.loads(canonical(value))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _spec(case):
|
|
39
|
+
question = (
|
|
40
|
+
QUESTIONS[case.family]
|
|
41
|
+
+ f" The first {len(case.reference_spans)} evidence spans are reference context; "
|
|
42
|
+
+ f"the remaining {len(case.target_spans)} spans are targets. "
|
|
43
|
+
+ "Task criteria: "
|
|
44
|
+
+ case.task_criteria
|
|
45
|
+
+ " Criteria reference digest: "
|
|
46
|
+
+ fingerprint(case.criteria_ref)
|
|
47
|
+
)
|
|
48
|
+
return JudgmentSpec(question, "boolean")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _deterministic_reuse(case, events):
|
|
52
|
+
"""Exact recorded equality under an explicit host reuse contract, not name counts."""
|
|
53
|
+
if (
|
|
54
|
+
case.family != "semantic_redundancy"
|
|
55
|
+
or not case.reuse_safe
|
|
56
|
+
or len(case.target_spans) != 1
|
|
57
|
+
or len(case.reference_spans) != 1
|
|
58
|
+
):
|
|
59
|
+
return False
|
|
60
|
+
earlier = events.get(case.reference_spans[0])
|
|
61
|
+
later = events.get(case.target_spans[0])
|
|
62
|
+
if earlier is None or later is None:
|
|
63
|
+
return False
|
|
64
|
+
first, second = event_interval_ms(earlier), event_interval_ms(later)
|
|
65
|
+
ignored = {
|
|
66
|
+
"otel_span_id",
|
|
67
|
+
"otel_trace_id",
|
|
68
|
+
"provider_reported_cost_usd",
|
|
69
|
+
"cost_usd",
|
|
70
|
+
"cached_input_tokens",
|
|
71
|
+
}
|
|
72
|
+
same_context = fingerprint(
|
|
73
|
+
{key: value for key, value in earlier.metadata.items() if key not in ignored}
|
|
74
|
+
) == fingerprint({key: value for key, value in later.metadata.items() if key not in ignored})
|
|
75
|
+
return bool(
|
|
76
|
+
first is not None
|
|
77
|
+
and second is not None
|
|
78
|
+
and first[1] <= second[0]
|
|
79
|
+
and earlier.status == later.status == "ok"
|
|
80
|
+
and earlier.name == later.name
|
|
81
|
+
and earlier.model == later.model
|
|
82
|
+
and same_context
|
|
83
|
+
and earlier.input_tokens == later.input_tokens
|
|
84
|
+
and earlier.output_tokens == later.output_tokens
|
|
85
|
+
and operation_kind(earlier) == operation_kind(later)
|
|
86
|
+
and isinstance(earlier.input_text, str)
|
|
87
|
+
and bool(earlier.input_text)
|
|
88
|
+
and isinstance(earlier.output_text, str)
|
|
89
|
+
and bool(earlier.output_text)
|
|
90
|
+
and earlier.input_text == later.input_text
|
|
91
|
+
and earlier.output_text == later.output_text
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _scope_gap(case, events):
|
|
96
|
+
targets = [events[identity] for identity in case.target_spans]
|
|
97
|
+
references = [events[identity] for identity in case.reference_spans]
|
|
98
|
+
target_times = [event_interval_ms(item) for item in targets]
|
|
99
|
+
reference_times = [event_interval_ms(item) for item in references]
|
|
100
|
+
if case.family == "retry_usefulness":
|
|
101
|
+
if not all(
|
|
102
|
+
item.event_type == "retry" or item.metadata.get("retry_of") in case.reference_spans
|
|
103
|
+
for item in targets
|
|
104
|
+
):
|
|
105
|
+
return "missing_retry_relation"
|
|
106
|
+
if case.family in {"semantic_redundancy", "low_contribution", "semantic_no_progress"}:
|
|
107
|
+
if any(item is None for item in target_times + reference_times):
|
|
108
|
+
return "missing_order_evidence"
|
|
109
|
+
if case.family == "semantic_redundancy" and max(item[1] for item in reference_times) > min(
|
|
110
|
+
item[0] for item in target_times
|
|
111
|
+
):
|
|
112
|
+
return "reference_not_available"
|
|
113
|
+
if case.family == "low_contribution" and not any(
|
|
114
|
+
item[0] >= max(target[1] for target in target_times) for item in reference_times
|
|
115
|
+
):
|
|
116
|
+
return "missing_downstream_evidence"
|
|
117
|
+
if case.family == "semantic_no_progress" and any(
|
|
118
|
+
left[1] > right[0] for left, right in zip(target_times, target_times[1:])
|
|
119
|
+
):
|
|
120
|
+
return "unordered_sequence"
|
|
121
|
+
if case.family == "context_relevance" and not all(
|
|
122
|
+
item.event_type == "model_call" or operation_kind(item) in {"model", "classifier", "rule"}
|
|
123
|
+
for item in targets
|
|
124
|
+
):
|
|
125
|
+
return "ineligible_context_target"
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def evaluate_semantic_waste(trace, investigations, *, judges=(), enabled=False, timeout_s=None):
|
|
130
|
+
"""Attach new purpose-bound investigations; no runtime action is proposed or run.
|
|
131
|
+
|
|
132
|
+
Source traces must remain unchanged during this offline operation. Prior
|
|
133
|
+
investigation IDs cannot be replaced. Use a new ID to record a reevaluation.
|
|
134
|
+
Approved criteria/summaries are the only caller text forwarded to a judge.
|
|
135
|
+
"""
|
|
136
|
+
if type(enabled) is not bool:
|
|
137
|
+
raise ValueError("enabled must be a boolean")
|
|
138
|
+
if timeout_s is not None and (not finite(timeout_s) or timeout_s <= 0):
|
|
139
|
+
raise ValueError("timeout_s must be finite and positive")
|
|
140
|
+
if (
|
|
141
|
+
not isinstance(investigations, (tuple, list))
|
|
142
|
+
or not investigations
|
|
143
|
+
or len(investigations) > 100
|
|
144
|
+
or any(type(item) is not SemanticInvestigation for item in investigations)
|
|
145
|
+
):
|
|
146
|
+
raise ValueError("supply between one and 100 SemanticInvestigation objects")
|
|
147
|
+
if not isinstance(judges, (tuple, list)) or len(judges) > 8:
|
|
148
|
+
raise ValueError("judges must be a list or tuple of at most eight backends")
|
|
149
|
+
identities = [item.investigation_id for item in investigations]
|
|
150
|
+
if len(set(identities)) != len(identities):
|
|
151
|
+
raise ValueError("investigation IDs must be unique")
|
|
152
|
+
old = trace.metadata.get(SEMANTIC_WASTE_KEY)
|
|
153
|
+
if old is not None:
|
|
154
|
+
current = read_semantic_waste(trace)
|
|
155
|
+
if current["status"] in {"invalid", "unsupported"}:
|
|
156
|
+
raise ValueError("existing semantic investigations are invalid or unsupported")
|
|
157
|
+
if set(identities) & {item["definition"]["investigation_id"] for item in old["cases"]}:
|
|
158
|
+
raise ValueError("use a new investigation ID rather than replacing saved evidence")
|
|
159
|
+
elif SEMANTIC_WASTE_KEY in trace.metadata:
|
|
160
|
+
raise ValueError("existing semantic investigations are invalid")
|
|
161
|
+
working = SimpleNamespace(
|
|
162
|
+
run_id=trace.run_id, events=trace.events, metadata=deepcopy(trace.metadata)
|
|
163
|
+
)
|
|
164
|
+
sessions = [JudgmentSession(backend) for backend in judges]
|
|
165
|
+
events = {event.event_id: event for event in trace.events}
|
|
166
|
+
cases = []
|
|
167
|
+
for case in investigations:
|
|
168
|
+
row = {
|
|
169
|
+
"definition": case.to_dict(),
|
|
170
|
+
"request": None,
|
|
171
|
+
"judgment_hashes": [],
|
|
172
|
+
"basis": "unavailable",
|
|
173
|
+
"reason": None,
|
|
174
|
+
}
|
|
175
|
+
try:
|
|
176
|
+
request = judgment_request(
|
|
177
|
+
trace, _spec(case), list(case.evidence_spans), summaries=dict(case.summaries)
|
|
178
|
+
)
|
|
179
|
+
except ValueError:
|
|
180
|
+
row["reason"] = "missing_or_invalid_evidence"
|
|
181
|
+
cases.append(row)
|
|
182
|
+
continue
|
|
183
|
+
row["request"] = request.to_dict()
|
|
184
|
+
if case.retention_required:
|
|
185
|
+
row["reason"] = "retention_required"
|
|
186
|
+
elif _scope_gap(case, events) is not None:
|
|
187
|
+
row["reason"] = _scope_gap(case, events)
|
|
188
|
+
elif _deterministic_reuse(case, events):
|
|
189
|
+
row["basis"] = "deterministic_reuse"
|
|
190
|
+
elif set(dict(case.summaries)) != set(case.evidence_spans):
|
|
191
|
+
row["reason"] = "missing_approved_summaries"
|
|
192
|
+
elif not sessions:
|
|
193
|
+
row["reason"] = "no_judge"
|
|
194
|
+
else:
|
|
195
|
+
row["basis"] = "semantic_judgment"
|
|
196
|
+
for session in sessions:
|
|
197
|
+
receipt = session.evaluate(request, enabled=enabled, timeout_s=timeout_s)
|
|
198
|
+
attach_judgment(working, receipt)
|
|
199
|
+
row["judgment_hashes"].append(receipt["record_hash"])
|
|
200
|
+
cases.append(row)
|
|
201
|
+
envelope = {
|
|
202
|
+
"schema_version": SEMANTIC_WASTE_VERSION,
|
|
203
|
+
"run_id": trace.run_id,
|
|
204
|
+
"cases": [*(old["cases"] if old is not None else []), *cases],
|
|
205
|
+
}
|
|
206
|
+
envelope["evidence_hash"] = fingerprint(envelope)
|
|
207
|
+
working.metadata[SEMANTIC_WASTE_KEY] = envelope
|
|
208
|
+
checked = read_semantic_waste(working)
|
|
209
|
+
if checked["status"] == "invalid" or any(
|
|
210
|
+
item["status"] == "stale" for item in checked["cases"][-len(cases) :]
|
|
211
|
+
):
|
|
212
|
+
raise ValueError("source evidence changed during the investigation")
|
|
213
|
+
if JUDGMENT_KEY in working.metadata:
|
|
214
|
+
trace.metadata[JUDGMENT_KEY] = working.metadata[JUDGMENT_KEY]
|
|
215
|
+
trace.metadata[SEMANTIC_WASTE_KEY] = envelope
|
|
216
|
+
return checked
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def read_semantic_waste(trace, *, judgments=None):
|
|
220
|
+
"""Validate the purpose, source and judge bindings without executing a backend."""
|
|
221
|
+
metadata = getattr(trace, "metadata", {})
|
|
222
|
+
if SEMANTIC_WASTE_KEY not in metadata:
|
|
223
|
+
return None
|
|
224
|
+
empty = {"status": "invalid", "cases": []}
|
|
225
|
+
try:
|
|
226
|
+
envelope = _copy(metadata[SEMANTIC_WASTE_KEY])
|
|
227
|
+
if not isinstance(envelope, dict):
|
|
228
|
+
return empty
|
|
229
|
+
if envelope.get("schema_version") != SEMANTIC_WASTE_VERSION:
|
|
230
|
+
return {**empty, "status": "unsupported"}
|
|
231
|
+
digest = envelope.pop("evidence_hash")
|
|
232
|
+
if digest != fingerprint(envelope) or not isinstance(envelope.get("cases"), list):
|
|
233
|
+
return empty
|
|
234
|
+
if judgments is None:
|
|
235
|
+
judgments = read_judgments(trace)
|
|
236
|
+
receipts = {item["record_hash"]: item for item in (judgments or {}).get("records", [])}
|
|
237
|
+
events = {event.event_id: event for event in trace.events}
|
|
238
|
+
source_hashes, seen, cases = {}, set(), []
|
|
239
|
+
for raw in envelope["cases"]:
|
|
240
|
+
case = SemanticInvestigation.from_dict(raw["definition"])
|
|
241
|
+
if case.investigation_id in seen:
|
|
242
|
+
return empty
|
|
243
|
+
seen.add(case.investigation_id)
|
|
244
|
+
hashes = raw["judgment_hashes"]
|
|
245
|
+
if (
|
|
246
|
+
not isinstance(hashes, list)
|
|
247
|
+
or any(not isinstance(value, str) for value in hashes)
|
|
248
|
+
or len(set(hashes)) != len(hashes)
|
|
249
|
+
):
|
|
250
|
+
return empty
|
|
251
|
+
row = {**raw, "status": "unknown", "confidence": "unknown", "judgments": []}
|
|
252
|
+
if envelope["run_id"] != trace.run_id:
|
|
253
|
+
row["status"] = "stale"
|
|
254
|
+
elif raw["request"] is None:
|
|
255
|
+
if raw["basis"] != "unavailable" or hashes:
|
|
256
|
+
return empty
|
|
257
|
+
else:
|
|
258
|
+
request = JudgmentRequest.from_dict(raw["request"])
|
|
259
|
+
if (
|
|
260
|
+
request.spec != _spec(case)
|
|
261
|
+
or tuple(item.span_id for item in request.evidence) != case.evidence_spans
|
|
262
|
+
or {
|
|
263
|
+
item.span_id: item.summary
|
|
264
|
+
for item in request.evidence
|
|
265
|
+
if item.summary is not None
|
|
266
|
+
}
|
|
267
|
+
!= dict(case.summaries)
|
|
268
|
+
):
|
|
269
|
+
return empty
|
|
270
|
+
if not _bound(
|
|
271
|
+
trace, {"request": raw["request"]}, events=events, hashes=source_hashes
|
|
272
|
+
):
|
|
273
|
+
row["status"] = "stale"
|
|
274
|
+
elif case.retention_required:
|
|
275
|
+
row["status"] = "retained"
|
|
276
|
+
elif _scope_gap(case, events) is not None:
|
|
277
|
+
row["reason"] = _scope_gap(case, events)
|
|
278
|
+
elif raw["basis"] == "deterministic_reuse":
|
|
279
|
+
if hashes or not _deterministic_reuse(case, events):
|
|
280
|
+
return empty
|
|
281
|
+
row.update(status="supported", confidence="medium")
|
|
282
|
+
elif raw["basis"] == "semantic_judgment":
|
|
283
|
+
if not hashes:
|
|
284
|
+
return empty
|
|
285
|
+
selected = [receipts.get(value) for value in hashes]
|
|
286
|
+
if any(item is None or item["request"] != raw["request"] for item in selected):
|
|
287
|
+
row["reason"] = "missing_or_mismatched_judgment"
|
|
288
|
+
else:
|
|
289
|
+
row["judgments"] = selected
|
|
290
|
+
if any(item["effective_status"] != "known" for item in selected):
|
|
291
|
+
row["reason"] = "unknown_or_failed_judgment"
|
|
292
|
+
elif len({item["effective_value"] for item in selected}) != 1:
|
|
293
|
+
row["status"] = "disagreement"
|
|
294
|
+
elif not selected[0]["effective_value"]:
|
|
295
|
+
row["status"] = "not_supported"
|
|
296
|
+
elif any(
|
|
297
|
+
item["evaluation"]["uncertainty"]["confidence"] is not None
|
|
298
|
+
and item["evaluation"]["uncertainty"]["confidence"]
|
|
299
|
+
< case.minimum_confidence
|
|
300
|
+
for item in selected
|
|
301
|
+
):
|
|
302
|
+
row["status"] = "low_confidence"
|
|
303
|
+
else:
|
|
304
|
+
row.update(status="supported", confidence="low")
|
|
305
|
+
elif raw["basis"] != "unavailable" or hashes:
|
|
306
|
+
return empty
|
|
307
|
+
cases.append(row)
|
|
308
|
+
return {
|
|
309
|
+
"status": "complete"
|
|
310
|
+
if cases
|
|
311
|
+
and all(item["status"] in {"supported", "not_supported", "retained"} for item in cases)
|
|
312
|
+
else "incomplete",
|
|
313
|
+
"cases": cases,
|
|
314
|
+
"evidence_hash": digest,
|
|
315
|
+
}
|
|
316
|
+
except (ValueError, TypeError, KeyError, AttributeError):
|
|
317
|
+
return empty
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def semantic_waste_markdown(evidence):
|
|
321
|
+
from agentloop.markdown import markdown_table_cell
|
|
322
|
+
|
|
323
|
+
lines = [
|
|
324
|
+
"",
|
|
325
|
+
"## Offline semantic investigations",
|
|
326
|
+
"",
|
|
327
|
+
"Observed facts, deterministic inferences and judge opinions are distinct. Unsupported cases do not produce findings; conditional savings require independent paired replay.",
|
|
328
|
+
"",
|
|
329
|
+
f"Investigation completeness: {markdown_table_cell(evidence['status'])}",
|
|
330
|
+
"",
|
|
331
|
+
"| Investigation | Family | Status | Basis | Task criteria reference | Judge references |",
|
|
332
|
+
"| --- | --- | --- | --- | --- | --- |",
|
|
333
|
+
]
|
|
334
|
+
for item in evidence["cases"]:
|
|
335
|
+
case = item["definition"]
|
|
336
|
+
cells = [
|
|
337
|
+
case["investigation_id"],
|
|
338
|
+
case["family"],
|
|
339
|
+
item["status"],
|
|
340
|
+
item["basis"],
|
|
341
|
+
case["criteria_ref"],
|
|
342
|
+
", ".join(item["judgment_hashes"]) or "none",
|
|
343
|
+
]
|
|
344
|
+
lines.append("| " + " | ".join(markdown_table_cell(str(value)) for value in cells) + " |")
|
|
345
|
+
return lines
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Explicit investigation declarations for optional semantic waste findings."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from dataclasses import asdict, dataclass
|
|
7
|
+
|
|
8
|
+
from agentloop.judgment_types import canonical, finite, text
|
|
9
|
+
|
|
10
|
+
SEMANTIC_WASTE_KEY = "agentloop.semantic_waste"
|
|
11
|
+
SEMANTIC_WASTE_VERSION = "1.0"
|
|
12
|
+
QUESTIONS = {
|
|
13
|
+
"semantic_redundancy": "Do the target steps materially repeat work already available in the reference steps, without a required independent check?",
|
|
14
|
+
"low_contribution": "Do the target steps add no material information needed by the supplied downstream result under the task criteria?",
|
|
15
|
+
"semantic_no_progress": "Does the target sequence fail to make meaningful progress under the task criteria, rather than perform required polling, verification or state transitions?",
|
|
16
|
+
"retry_usefulness": "Do the target retries repeat the same failure or input without useful new evidence, rather than satisfy an intentional retry policy?",
|
|
17
|
+
"context_relevance": "Is the supplied context in the target steps unrelated to the decision under the task criteria?",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class SemanticInvestigation:
|
|
23
|
+
investigation_id: str
|
|
24
|
+
family: str
|
|
25
|
+
target_spans: tuple[str, ...]
|
|
26
|
+
reference_spans: tuple[str, ...]
|
|
27
|
+
task_criteria: str
|
|
28
|
+
criteria_ref: str
|
|
29
|
+
summaries: tuple[tuple[str, str], ...] = ()
|
|
30
|
+
retention_required: bool = False
|
|
31
|
+
reuse_safe: bool = False
|
|
32
|
+
reuse_contract_ref: str | None = None
|
|
33
|
+
removal_attribution_ref: str | None = None
|
|
34
|
+
minimum_confidence: float = 0.8
|
|
35
|
+
|
|
36
|
+
def __post_init__(self):
|
|
37
|
+
for key in ("investigation_id", "task_criteria", "criteria_ref"):
|
|
38
|
+
text(getattr(self, key), key)
|
|
39
|
+
if len(self.task_criteria) > 2048:
|
|
40
|
+
raise ValueError("task_criteria must be at most 2048 characters")
|
|
41
|
+
if not finite(self.minimum_confidence) or not 0 <= self.minimum_confidence <= 1:
|
|
42
|
+
raise ValueError("minimum_confidence must be in [0, 1]")
|
|
43
|
+
if self.family not in QUESTIONS:
|
|
44
|
+
raise ValueError("unsupported semantic finding family")
|
|
45
|
+
for key in ("target_spans", "reference_spans"):
|
|
46
|
+
value = getattr(self, key)
|
|
47
|
+
if not isinstance(value, (tuple, list)) or not value:
|
|
48
|
+
raise ValueError(f"{key} requires at least one span ID")
|
|
49
|
+
for item in value:
|
|
50
|
+
text(item, key)
|
|
51
|
+
if len(set(value)) != len(value):
|
|
52
|
+
raise ValueError(f"{key} must be unique")
|
|
53
|
+
object.__setattr__(self, key, tuple(value))
|
|
54
|
+
if set(self.target_spans) & set(self.reference_spans):
|
|
55
|
+
raise ValueError("target and reference spans must be distinct")
|
|
56
|
+
if len(self.evidence_spans) > 64:
|
|
57
|
+
raise ValueError("an investigation can reference at most 64 spans")
|
|
58
|
+
if self.family == "semantic_no_progress" and len(self.target_spans) < 2:
|
|
59
|
+
raise ValueError("no-progress investigation requires a sequence of target spans")
|
|
60
|
+
for key in ("retention_required", "reuse_safe"):
|
|
61
|
+
if type(getattr(self, key)) is not bool:
|
|
62
|
+
raise ValueError(f"{key} must be a boolean declaration")
|
|
63
|
+
for key in ("reuse_contract_ref", "removal_attribution_ref"):
|
|
64
|
+
text(getattr(self, key), key, optional=True)
|
|
65
|
+
if self.reuse_safe and self.reuse_contract_ref is None:
|
|
66
|
+
raise ValueError("safe reuse requires a supporting contract reference")
|
|
67
|
+
values = self.summaries.items() if isinstance(self.summaries, dict) else self.summaries
|
|
68
|
+
try:
|
|
69
|
+
pairs = tuple((key, value) for key, value in values)
|
|
70
|
+
except (TypeError, ValueError):
|
|
71
|
+
raise ValueError("summaries must map span IDs to approved derived text") from None
|
|
72
|
+
for key, value in pairs:
|
|
73
|
+
text(key, "summary span ID")
|
|
74
|
+
text(value, "summary")
|
|
75
|
+
if len(dict(pairs)) != len(pairs) or set(dict(pairs)) - set(self.evidence_spans):
|
|
76
|
+
raise ValueError("summaries must name unique selected spans")
|
|
77
|
+
object.__setattr__(self, "summaries", tuple(sorted(pairs)))
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def evidence_spans(self):
|
|
81
|
+
return self.reference_spans + self.target_spans
|
|
82
|
+
|
|
83
|
+
def to_dict(self):
|
|
84
|
+
return json.loads(canonical(asdict(self)))
|
|
85
|
+
|
|
86
|
+
@classmethod
|
|
87
|
+
def from_dict(cls, value):
|
|
88
|
+
return cls(**value)
|