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,431 @@
|
|
|
1
|
+
"""Structural candidates built on the existing declared scheduler and experiments."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import replace
|
|
6
|
+
from threading import RLock
|
|
7
|
+
from types import MappingProxyType
|
|
8
|
+
|
|
9
|
+
from agentloop.budget_types import ResourceUsage
|
|
10
|
+
from agentloop.context_experiments import _close_invalid, _deadline, _sum_usage
|
|
11
|
+
from agentloop.context_types import synchronous
|
|
12
|
+
from agentloop.experiment_observations import observation, record_experiment_observations
|
|
13
|
+
from agentloop.experiment_types import (
|
|
14
|
+
ExperimentResult,
|
|
15
|
+
ExperimentRunner,
|
|
16
|
+
fingerprint,
|
|
17
|
+
owned,
|
|
18
|
+
reference,
|
|
19
|
+
)
|
|
20
|
+
from agentloop.harness import Harness, HarnessConfig
|
|
21
|
+
from agentloop.scheduling_plan import plan_tools
|
|
22
|
+
from agentloop.scheduling_types import ScheduleConfig, SchedulingError, ToolCall
|
|
23
|
+
from agentloop.structural_types import (
|
|
24
|
+
STRUCTURAL_KEY,
|
|
25
|
+
StageReference,
|
|
26
|
+
publish_structural_observations,
|
|
27
|
+
)
|
|
28
|
+
from agentloop.tool_scheduling import ToolScheduler, scheduling_policy
|
|
29
|
+
from agentloop.tracer import current_trace
|
|
30
|
+
from agentloop.workflows import StageInfo, trace_operation
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _stages(values):
|
|
34
|
+
if (
|
|
35
|
+
not isinstance(values, (tuple, list))
|
|
36
|
+
or not 1 <= len(values) <= 64
|
|
37
|
+
or any(type(item) is not StageReference for item in values)
|
|
38
|
+
or len({item.stage_id for item in values}) != len(values)
|
|
39
|
+
):
|
|
40
|
+
raise ValueError("structural candidates require unique bounded stage references")
|
|
41
|
+
return tuple(values)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _graph_runner(
|
|
45
|
+
candidate_id,
|
|
46
|
+
factory,
|
|
47
|
+
assemble,
|
|
48
|
+
*,
|
|
49
|
+
stages,
|
|
50
|
+
factory_ref,
|
|
51
|
+
assembler_ref,
|
|
52
|
+
why_ref,
|
|
53
|
+
removed,
|
|
54
|
+
max_concurrency,
|
|
55
|
+
strict,
|
|
56
|
+
reservation,
|
|
57
|
+
):
|
|
58
|
+
stages = _stages(stages)
|
|
59
|
+
if not synchronous(factory) or not synchronous(assemble):
|
|
60
|
+
raise ValueError("stage factory and assembler must be trusted synchronous callbacks")
|
|
61
|
+
for name, value in (
|
|
62
|
+
("factory_ref", factory_ref),
|
|
63
|
+
("assembler_ref", assembler_ref),
|
|
64
|
+
("why_ref", why_ref),
|
|
65
|
+
):
|
|
66
|
+
reference(value, name)
|
|
67
|
+
removed = owned(removed)
|
|
68
|
+
ids = {stage.stage_id for stage in stages}
|
|
69
|
+
if not isinstance(removed, dict) or not set(removed) <= ids:
|
|
70
|
+
raise ValueError("removed stages must belong to the explicit stage set")
|
|
71
|
+
settings = ScheduleConfig(
|
|
72
|
+
"structural-schedule",
|
|
73
|
+
"1.0",
|
|
74
|
+
"stages",
|
|
75
|
+
max_concurrency=max_concurrency,
|
|
76
|
+
on_unknown="error" if strict else "sequential",
|
|
77
|
+
)
|
|
78
|
+
config = {
|
|
79
|
+
"schema_version": "1.0",
|
|
80
|
+
"family": "parallelization" if strict else "stage_removal",
|
|
81
|
+
"stages": [stage.declaration() for stage in stages],
|
|
82
|
+
"factory_ref": factory_ref,
|
|
83
|
+
"assembler_ref": assembler_ref,
|
|
84
|
+
"why_ref": why_ref,
|
|
85
|
+
"removed": {key: fingerprint(value) for key, value in removed.items()},
|
|
86
|
+
"scheduler": settings.to_dict(),
|
|
87
|
+
}
|
|
88
|
+
config_hash = fingerprint(config)
|
|
89
|
+
source = "structural-config:" + config_hash
|
|
90
|
+
|
|
91
|
+
def invoke(request):
|
|
92
|
+
trace = current_trace()
|
|
93
|
+
if trace is None:
|
|
94
|
+
raise ValueError("structural candidate requires an active experiment")
|
|
95
|
+
receipt = {
|
|
96
|
+
"schema_version": "1.0",
|
|
97
|
+
"config_hash": config_hash,
|
|
98
|
+
"family": config["family"],
|
|
99
|
+
"why_ref": why_ref,
|
|
100
|
+
"changed_stages": sorted(ids if strict else removed),
|
|
101
|
+
"stages": [stage.declaration() for stage in stages],
|
|
102
|
+
"removed_value_hashes": config["removed"],
|
|
103
|
+
"state": "preparing",
|
|
104
|
+
"invoked_stages": [],
|
|
105
|
+
"skipped_stages": [],
|
|
106
|
+
"output_hashes": {},
|
|
107
|
+
"usage": {},
|
|
108
|
+
"scheduler": None,
|
|
109
|
+
}
|
|
110
|
+
trace.metadata[STRUCTURAL_KEY] = receipt
|
|
111
|
+
lock, event_ids = RLock(), {}
|
|
112
|
+
scheduler = ToolScheduler(
|
|
113
|
+
Harness(HarnessConfig("enforce", (scheduling_policy(settings),))).start_run(),
|
|
114
|
+
config=settings,
|
|
115
|
+
)
|
|
116
|
+
result = None
|
|
117
|
+
try:
|
|
118
|
+
_deadline(request)
|
|
119
|
+
calls = factory(request)
|
|
120
|
+
if (
|
|
121
|
+
not isinstance(calls, (tuple, list))
|
|
122
|
+
or any(type(call) is not ToolCall for call in calls)
|
|
123
|
+
or {call.call_id for call in calls} != ids
|
|
124
|
+
or len(calls) != len(ids)
|
|
125
|
+
):
|
|
126
|
+
raise ValueError("stage factory differs from declared stage identities")
|
|
127
|
+
# Validate the original dependency/resource contract before any
|
|
128
|
+
# removal can hide an unsafe or cyclic source plan.
|
|
129
|
+
receipt["original_plan"] = plan_tools(calls, settings)
|
|
130
|
+
references = {stage.stage_id: stage for stage in stages}
|
|
131
|
+
wrapped = []
|
|
132
|
+
for call in calls:
|
|
133
|
+
if call.call_id in removed:
|
|
134
|
+
|
|
135
|
+
def skipped(context, call=call):
|
|
136
|
+
with lock:
|
|
137
|
+
receipt["skipped_stages"].append(call.call_id)
|
|
138
|
+
return ExperimentResult(
|
|
139
|
+
removed[call.call_id],
|
|
140
|
+
# Fixed accounting provenance, not a credential.
|
|
141
|
+
usage=ResourceUsage(
|
|
142
|
+
tokens=0,
|
|
143
|
+
token_provenance="user_supplied",
|
|
144
|
+
complete=True, # nosec B106
|
|
145
|
+
),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
wrapped.append(
|
|
149
|
+
replace(
|
|
150
|
+
call,
|
|
151
|
+
invoke=skipped,
|
|
152
|
+
effect="read_only",
|
|
153
|
+
reads=(),
|
|
154
|
+
writes=(),
|
|
155
|
+
concurrent=True,
|
|
156
|
+
usage_reader=lambda value: value.usage,
|
|
157
|
+
error_usage_reader=None,
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
continue
|
|
161
|
+
|
|
162
|
+
def measured(context, call=call):
|
|
163
|
+
_deadline(request)
|
|
164
|
+
stage = references[call.call_id]
|
|
165
|
+
with lock:
|
|
166
|
+
dependencies = [
|
|
167
|
+
event_ids[key] for key in (call.depends_on or ()) if key in event_ids
|
|
168
|
+
]
|
|
169
|
+
receipt["invoked_stages"].append(call.call_id)
|
|
170
|
+
copied = replace(
|
|
171
|
+
context,
|
|
172
|
+
prerequisites=MappingProxyType(
|
|
173
|
+
{key: value.output for key, value in context.prerequisites.items()}
|
|
174
|
+
),
|
|
175
|
+
)
|
|
176
|
+
with trace_operation(
|
|
177
|
+
call.call_id,
|
|
178
|
+
kind=stage.kind,
|
|
179
|
+
stage=StageInfo(
|
|
180
|
+
call.call_id,
|
|
181
|
+
stage.version,
|
|
182
|
+
kind=stage.kind,
|
|
183
|
+
input_ref=request.input_ref,
|
|
184
|
+
),
|
|
185
|
+
depends_on=dependencies if call.depends_on is not None else None,
|
|
186
|
+
metadata={
|
|
187
|
+
"structural_source_ref": stage.source_ref,
|
|
188
|
+
"parallel_safe": call.concurrent is True and call.effect == "read_only",
|
|
189
|
+
},
|
|
190
|
+
) as span:
|
|
191
|
+
with lock:
|
|
192
|
+
event_ids[call.call_id] = span.event_id
|
|
193
|
+
value = call.invoke(copied)
|
|
194
|
+
if type(value) is not ExperimentResult:
|
|
195
|
+
_close_invalid(value)
|
|
196
|
+
raise ValueError("stage callback must return ExperimentResult")
|
|
197
|
+
with lock:
|
|
198
|
+
receipt["output_hashes"][call.call_id] = fingerprint(value.output)
|
|
199
|
+
receipt["usage"][call.call_id] = value.usage.to_dict()
|
|
200
|
+
return value
|
|
201
|
+
|
|
202
|
+
wrapped.append(
|
|
203
|
+
replace(
|
|
204
|
+
call,
|
|
205
|
+
invoke=measured,
|
|
206
|
+
usage_reader=lambda value: value.usage,
|
|
207
|
+
error_usage_reader=None,
|
|
208
|
+
)
|
|
209
|
+
)
|
|
210
|
+
result = scheduler.execute(wrapped, timeout_s=request.remaining_s)
|
|
211
|
+
if not result.completed:
|
|
212
|
+
raise ValueError("structural stage execution is incomplete")
|
|
213
|
+
_deadline(request)
|
|
214
|
+
outputs = MappingProxyType(
|
|
215
|
+
{item.call_id: item.value.output for item in result.outcomes}
|
|
216
|
+
)
|
|
217
|
+
value = assemble(request, outputs)
|
|
218
|
+
if type(value) is not ExperimentResult:
|
|
219
|
+
_close_invalid(value)
|
|
220
|
+
raise ValueError("assembler must return ExperimentResult with usage")
|
|
221
|
+
receipt["assembler_usage"] = value.usage.to_dict()
|
|
222
|
+
usages = [item.value.usage for item in result.outcomes] + [value.usage]
|
|
223
|
+
receipt["state"] = "completed"
|
|
224
|
+
return ExperimentResult(value.output, usage=_sum_usage(usages))
|
|
225
|
+
except SchedulingError as exc:
|
|
226
|
+
receipt.update(state="abstained", reason=exc.reason_code)
|
|
227
|
+
raise
|
|
228
|
+
except BaseException:
|
|
229
|
+
receipt["state"] = "failed"
|
|
230
|
+
raise
|
|
231
|
+
finally:
|
|
232
|
+
receipt["scheduler"] = scheduler.export_evidence()
|
|
233
|
+
receipt["invoked_stages"].sort()
|
|
234
|
+
receipt["skipped_stages"].sort()
|
|
235
|
+
evidence = list(receipt["scheduler"]["records"].values())
|
|
236
|
+
peak = evidence[0]["peak_running"] if evidence else 0
|
|
237
|
+
observed = result or scheduler.last_result
|
|
238
|
+
failures = (
|
|
239
|
+
sum(item.status != "completed" for item in observed.outcomes)
|
|
240
|
+
if observed is not None
|
|
241
|
+
else None
|
|
242
|
+
)
|
|
243
|
+
publish_structural_observations(
|
|
244
|
+
receipt,
|
|
245
|
+
{
|
|
246
|
+
"structural_state": observation(
|
|
247
|
+
receipt["state"], unit="state", source_ref=source
|
|
248
|
+
),
|
|
249
|
+
"stages_invoked": observation(
|
|
250
|
+
len(receipt["invoked_stages"]), unit="operations", source_ref=source
|
|
251
|
+
),
|
|
252
|
+
"stages_skipped": observation(
|
|
253
|
+
len(receipt["skipped_stages"]), unit="operations", source_ref=source
|
|
254
|
+
),
|
|
255
|
+
"stage_failures": observation(failures, unit="operations", source_ref=source),
|
|
256
|
+
"peak_parallel_stages": observation(peak, unit="operations", source_ref=source),
|
|
257
|
+
"parallel_execution_observed": observation(
|
|
258
|
+
peak > 1, unit="boolean", source_ref=source
|
|
259
|
+
),
|
|
260
|
+
},
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
return ExperimentRunner(
|
|
264
|
+
candidate_id,
|
|
265
|
+
"1.0",
|
|
266
|
+
"agentloop.structural-" + config["family"] + ":1.0",
|
|
267
|
+
"sha256:" + config_hash,
|
|
268
|
+
invoke,
|
|
269
|
+
reservation=reservation,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def stage_removal_runner(
|
|
274
|
+
candidate_id,
|
|
275
|
+
factory,
|
|
276
|
+
assemble,
|
|
277
|
+
*,
|
|
278
|
+
stages,
|
|
279
|
+
removed,
|
|
280
|
+
factory_ref,
|
|
281
|
+
assembler_ref,
|
|
282
|
+
why_ref,
|
|
283
|
+
reservation,
|
|
284
|
+
):
|
|
285
|
+
if not removed:
|
|
286
|
+
raise ValueError(
|
|
287
|
+
"stage removal requires explicit replacement values for omitted operations"
|
|
288
|
+
)
|
|
289
|
+
return _graph_runner(
|
|
290
|
+
candidate_id,
|
|
291
|
+
factory,
|
|
292
|
+
assemble,
|
|
293
|
+
stages=stages,
|
|
294
|
+
factory_ref=factory_ref,
|
|
295
|
+
assembler_ref=assembler_ref,
|
|
296
|
+
why_ref=why_ref,
|
|
297
|
+
removed=removed,
|
|
298
|
+
max_concurrency=1,
|
|
299
|
+
strict=False,
|
|
300
|
+
reservation=reservation,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def parallel_experiment_runner(
|
|
305
|
+
candidate_id,
|
|
306
|
+
factory,
|
|
307
|
+
assemble,
|
|
308
|
+
*,
|
|
309
|
+
stages,
|
|
310
|
+
factory_ref,
|
|
311
|
+
assembler_ref,
|
|
312
|
+
why_ref,
|
|
313
|
+
reservation,
|
|
314
|
+
max_concurrency=4,
|
|
315
|
+
):
|
|
316
|
+
return _graph_runner(
|
|
317
|
+
candidate_id,
|
|
318
|
+
factory,
|
|
319
|
+
assemble,
|
|
320
|
+
stages=stages,
|
|
321
|
+
factory_ref=factory_ref,
|
|
322
|
+
assembler_ref=assembler_ref,
|
|
323
|
+
why_ref=why_ref,
|
|
324
|
+
removed={},
|
|
325
|
+
max_concurrency=max_concurrency,
|
|
326
|
+
strict=True,
|
|
327
|
+
reservation=reservation,
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def conditional_experiment_runner(
|
|
332
|
+
candidate_id,
|
|
333
|
+
expensive,
|
|
334
|
+
fallback,
|
|
335
|
+
predicate,
|
|
336
|
+
*,
|
|
337
|
+
stages,
|
|
338
|
+
predicate_ref,
|
|
339
|
+
why_ref,
|
|
340
|
+
reservation,
|
|
341
|
+
on_unknown="expensive",
|
|
342
|
+
):
|
|
343
|
+
stages = _stages(stages)
|
|
344
|
+
if (
|
|
345
|
+
type(expensive) is not ExperimentRunner
|
|
346
|
+
or type(fallback) is not ExperimentRunner
|
|
347
|
+
or not synchronous(predicate)
|
|
348
|
+
or on_unknown not in {"expensive", "fallback", "error"}
|
|
349
|
+
):
|
|
350
|
+
raise ValueError(
|
|
351
|
+
"conditional candidates require explicit implementations and unknown handling"
|
|
352
|
+
)
|
|
353
|
+
reference(predicate_ref, "predicate_ref")
|
|
354
|
+
reference(why_ref, "why_ref")
|
|
355
|
+
config = {
|
|
356
|
+
"schema_version": "1.0",
|
|
357
|
+
"stages": [stage.declaration() for stage in stages],
|
|
358
|
+
"expensive": expensive.declaration(),
|
|
359
|
+
"fallback": fallback.declaration(),
|
|
360
|
+
"predicate_ref": predicate_ref,
|
|
361
|
+
"why_ref": why_ref,
|
|
362
|
+
"on_unknown": on_unknown,
|
|
363
|
+
}
|
|
364
|
+
config_hash = fingerprint(config)
|
|
365
|
+
source = "conditional-config:" + config_hash
|
|
366
|
+
|
|
367
|
+
def invoke(request):
|
|
368
|
+
if (
|
|
369
|
+
expensive.declaration() != config["expensive"]
|
|
370
|
+
or fallback.declaration() != config["fallback"]
|
|
371
|
+
):
|
|
372
|
+
raise ValueError("conditional implementation binding changed")
|
|
373
|
+
trace = current_trace()
|
|
374
|
+
receipt = {
|
|
375
|
+
"schema_version": "1.0",
|
|
376
|
+
"family": "conditional",
|
|
377
|
+
"config_hash": config_hash,
|
|
378
|
+
"why_ref": why_ref,
|
|
379
|
+
"changed_stages": [stage.declaration() for stage in stages],
|
|
380
|
+
"predicate": None,
|
|
381
|
+
"route": None,
|
|
382
|
+
"unknown_reason": None,
|
|
383
|
+
}
|
|
384
|
+
trace.metadata[STRUCTURAL_KEY] = receipt
|
|
385
|
+
_deadline(request)
|
|
386
|
+
usage = ResourceUsage(tokens=0, token_provenance="user_supplied", complete=True) # nosec B106
|
|
387
|
+
try:
|
|
388
|
+
value = predicate(request)
|
|
389
|
+
reported_usage = type(value) is ExperimentResult
|
|
390
|
+
if type(value) is ExperimentResult:
|
|
391
|
+
usage, value = value.usage, value.output
|
|
392
|
+
if value is not None and type(value) is not bool:
|
|
393
|
+
_close_invalid(value)
|
|
394
|
+
if not reported_usage:
|
|
395
|
+
usage = ResourceUsage()
|
|
396
|
+
value, receipt["unknown_reason"] = None, "invalid_predicate"
|
|
397
|
+
except Exception:
|
|
398
|
+
value, usage, receipt["unknown_reason"] = None, ResourceUsage(), "predicate_error"
|
|
399
|
+
known = type(value) is bool
|
|
400
|
+
route = ("expensive" if value else "fallback") if known else on_unknown
|
|
401
|
+
receipt.update(predicate=value, route=route, predicate_usage=usage.to_dict())
|
|
402
|
+
record_experiment_observations(
|
|
403
|
+
{
|
|
404
|
+
"conditional_predicate_known": observation(
|
|
405
|
+
known, unit="boolean", source_ref=source
|
|
406
|
+
),
|
|
407
|
+
"conditional_predicate_value": observation(
|
|
408
|
+
value, unit="boolean", source_ref=source
|
|
409
|
+
),
|
|
410
|
+
"conditional_route": observation(route, unit="state", source_ref=source),
|
|
411
|
+
"conditional_unknown": observation(not known, unit="boolean", source_ref=source),
|
|
412
|
+
}
|
|
413
|
+
)
|
|
414
|
+
if route == "error":
|
|
415
|
+
raise ValueError("conditional predicate is unknown")
|
|
416
|
+
_deadline(request)
|
|
417
|
+
result = (expensive if route == "expensive" else fallback).invoke(request)
|
|
418
|
+
if type(result) is not ExperimentResult:
|
|
419
|
+
_close_invalid(result)
|
|
420
|
+
raise ValueError("conditional implementation must return ExperimentResult")
|
|
421
|
+
receipt["selected_usage"] = result.usage.to_dict()
|
|
422
|
+
return ExperimentResult(result.output, usage=_sum_usage([usage, result.usage]))
|
|
423
|
+
|
|
424
|
+
return ExperimentRunner(
|
|
425
|
+
candidate_id,
|
|
426
|
+
"1.0",
|
|
427
|
+
"agentloop.conditional-execution:1.0",
|
|
428
|
+
"sha256:" + config_hash,
|
|
429
|
+
invoke,
|
|
430
|
+
reservation=reservation,
|
|
431
|
+
)
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Explicit stage identities and bounded batch requests for offline experiments."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from dataclasses import asdict, dataclass, field
|
|
7
|
+
|
|
8
|
+
from agentloop.budget_types import ResourceUsage
|
|
9
|
+
from agentloop.context_types import identifier
|
|
10
|
+
from agentloop.experiment_observations import record_experiment_observations
|
|
11
|
+
from agentloop.experiment_types import canonical, reference
|
|
12
|
+
|
|
13
|
+
STRUCTURAL_KEY = "agentloop.structural_experiment"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def publish_structural_observations(receipt, values):
|
|
17
|
+
try:
|
|
18
|
+
record_experiment_observations(values)
|
|
19
|
+
except BaseException:
|
|
20
|
+
receipt["capture_error"] = "observation_capture_failed"
|
|
21
|
+
if receipt["state"] not in {"failed", "abstained"}:
|
|
22
|
+
raise
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class StageReference:
|
|
27
|
+
stage_id: str
|
|
28
|
+
version: str
|
|
29
|
+
source_ref: str
|
|
30
|
+
kind: str = "transform"
|
|
31
|
+
|
|
32
|
+
def __post_init__(self):
|
|
33
|
+
identifier(self.stage_id, "stage_id")
|
|
34
|
+
identifier(self.version, "stage version")
|
|
35
|
+
reference(self.source_ref, "stage source")
|
|
36
|
+
if self.kind not in {
|
|
37
|
+
"tool",
|
|
38
|
+
"transform",
|
|
39
|
+
"classifier",
|
|
40
|
+
"rule",
|
|
41
|
+
"external_service",
|
|
42
|
+
"retriever",
|
|
43
|
+
"evaluator",
|
|
44
|
+
}:
|
|
45
|
+
raise ValueError("stage wrappers require a declared non-model operation kind")
|
|
46
|
+
|
|
47
|
+
def declaration(self):
|
|
48
|
+
return asdict(self)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True, init=False)
|
|
52
|
+
class BatchItem:
|
|
53
|
+
item_id: str
|
|
54
|
+
_json: str = field(repr=False)
|
|
55
|
+
|
|
56
|
+
def __init__(self, item_id, inputs):
|
|
57
|
+
identifier(item_id, "item_id")
|
|
58
|
+
object.__setattr__(self, "item_id", item_id)
|
|
59
|
+
object.__setattr__(self, "_json", canonical(inputs))
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def inputs(self):
|
|
63
|
+
return json.loads(self._json)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(frozen=True)
|
|
67
|
+
class BatchRequest:
|
|
68
|
+
batch_id: str
|
|
69
|
+
items: tuple[BatchItem, ...]
|
|
70
|
+
request: object = field(repr=False)
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def remaining_s(self):
|
|
74
|
+
return self.request.remaining_s
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass(frozen=True, init=False)
|
|
78
|
+
class BatchItemOutcome:
|
|
79
|
+
item_id: str
|
|
80
|
+
status: str
|
|
81
|
+
error_code: str | None
|
|
82
|
+
_json: str | None = field(repr=False)
|
|
83
|
+
|
|
84
|
+
def __init__(self, item_id, output=None, *, status="completed", error_code=None):
|
|
85
|
+
identifier(item_id, "item_id")
|
|
86
|
+
if status not in {"completed", "failed", "unknown"}:
|
|
87
|
+
raise ValueError("unsupported batch item status")
|
|
88
|
+
if error_code is not None:
|
|
89
|
+
identifier(error_code, "error_code")
|
|
90
|
+
object.__setattr__(self, "item_id", item_id)
|
|
91
|
+
object.__setattr__(self, "status", status)
|
|
92
|
+
object.__setattr__(self, "error_code", error_code)
|
|
93
|
+
object.__setattr__(self, "_json", canonical(output) if status == "completed" else None)
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def output(self):
|
|
97
|
+
return None if self._json is None else json.loads(self._json)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass(frozen=True)
|
|
101
|
+
class BatchResult:
|
|
102
|
+
items: tuple[BatchItemOutcome, ...]
|
|
103
|
+
usage: ResourceUsage = field(default_factory=ResourceUsage)
|
|
104
|
+
|
|
105
|
+
def __post_init__(self):
|
|
106
|
+
if (
|
|
107
|
+
not isinstance(self.items, (list, tuple))
|
|
108
|
+
or len(self.items) > 1024
|
|
109
|
+
or any(type(item) is not BatchItemOutcome for item in self.items)
|
|
110
|
+
or type(self.usage) is not ResourceUsage
|
|
111
|
+
):
|
|
112
|
+
raise ValueError("batch result requires bounded item outcomes and exclusive usage")
|
|
113
|
+
object.__setattr__(self, "items", tuple(self.items))
|