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