agentloop-profiler 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. agentloop/__init__.py +136 -0
  2. agentloop/ablation_protocol.py +229 -0
  3. agentloop/ablations.py +564 -0
  4. agentloop/aggregate_cli.py +88 -0
  5. agentloop/aggregate_stats.py +175 -0
  6. agentloop/aggregates.py +532 -0
  7. agentloop/audit.py +39 -0
  8. agentloop/autoinstrument.py +145 -0
  9. agentloop/batch_experiments.py +295 -0
  10. agentloop/batching.py +95 -0
  11. agentloop/budget_types.py +335 -0
  12. agentloop/budgets.py +539 -0
  13. agentloop/calibration.py +872 -0
  14. agentloop/checkpoint_state.py +306 -0
  15. agentloop/checkpoint_store.py +86 -0
  16. agentloop/ci.py +202 -0
  17. agentloop/cli.py +1314 -0
  18. agentloop/client.py +228 -0
  19. agentloop/completion.py +345 -0
  20. agentloop/completion_types.py +199 -0
  21. agentloop/config.py +50 -0
  22. agentloop/context_controls.py +443 -0
  23. agentloop/context_experiments.py +337 -0
  24. agentloop/context_types.py +216 -0
  25. agentloop/costs.py +667 -0
  26. agentloop/cursor_validation.py +48 -0
  27. agentloop/decorators.py +445 -0
  28. agentloop/demo.py +300 -0
  29. agentloop/doctor.py +341 -0
  30. agentloop/drift.py +361 -0
  31. agentloop/drift_cli.py +147 -0
  32. agentloop/drift_types.py +110 -0
  33. agentloop/drift_windows.py +340 -0
  34. agentloop/entrypoint.py +201 -0
  35. agentloop/estimates.py +133 -0
  36. agentloop/events.py +75 -0
  37. agentloop/experiment_artifacts.py +80 -0
  38. agentloop/experiment_observations.py +169 -0
  39. agentloop/experiment_reports.py +545 -0
  40. agentloop/experiment_types.py +355 -0
  41. agentloop/experiments.py +392 -0
  42. agentloop/exporters.py +117 -0
  43. agentloop/finding_benchmark_types.py +131 -0
  44. agentloop/finding_benchmarks.py +424 -0
  45. agentloop/findings.py +419 -0
  46. agentloop/graph.py +464 -0
  47. agentloop/harness.py +825 -0
  48. agentloop/harness_bench/__init__.py +1 -0
  49. agentloop/harness_bench/cli.py +77 -0
  50. agentloop/harness_bench/drivers.py +96 -0
  51. agentloop/harness_bench/offline.py +507 -0
  52. agentloop/harness_evidence.py +585 -0
  53. agentloop/harness_usage.py +91 -0
  54. agentloop/html_report.py +860 -0
  55. agentloop/integrations/__init__.py +1 -0
  56. agentloop/integrations/crewai.py +144 -0
  57. agentloop/integrations/harbor/__init__.py +1 -0
  58. agentloop/integrations/harbor/atif.py +982 -0
  59. agentloop/integrations/harbor/atif_validation.py +329 -0
  60. agentloop/integrations/harbor/cli.py +207 -0
  61. agentloop/integrations/harbor/manifest.py +86 -0
  62. agentloop/integrations/harbor/trial_evidence.py +196 -0
  63. agentloop/integrations/harbor/trials.py +1109 -0
  64. agentloop/integrations/harbor/verifier.py +118 -0
  65. agentloop/integrations/langgraph.py +150 -0
  66. agentloop/integrations/langgraph_harness.py +466 -0
  67. agentloop/integrations/langgraph_recovery.py +366 -0
  68. agentloop/integrations/omnigent/__init__.py +1 -0
  69. agentloop/integrations/omnigent/cli.py +78 -0
  70. agentloop/integrations/omnigent/evidence.py +93 -0
  71. agentloop/integrations/omnigent/telemetry.py +422 -0
  72. agentloop/integrations/openai.py +415 -0
  73. agentloop/integrations/openai_agents.py +291 -0
  74. agentloop/integrations/sdk.py +82 -0
  75. agentloop/integrations/vercel_ai.py +83 -0
  76. agentloop/interoperability/__init__.py +5 -0
  77. agentloop/interoperability/artifacts.py +112 -0
  78. agentloop/interoperability/cli.py +59 -0
  79. agentloop/interoperability/cohort_evidence.py +175 -0
  80. agentloop/interoperability/cohorts.py +681 -0
  81. agentloop/interoperability/contracts.py +427 -0
  82. agentloop/interoperability/coordination.py +554 -0
  83. agentloop/interoperability/evidence.py +215 -0
  84. agentloop/interoperability/otlp_jsonl.py +775 -0
  85. agentloop/interoperability/otlp_validation.py +496 -0
  86. agentloop/interoperability/registry.py +561 -0
  87. agentloop/interoperability/validation.py +333 -0
  88. agentloop/intervention_service.py +110 -0
  89. agentloop/intervention_store.py +118 -0
  90. agentloop/interventions.py +190 -0
  91. agentloop/issues.py +90 -0
  92. agentloop/judgment_adapters.py +125 -0
  93. agentloop/judgment_benchmark_types.py +98 -0
  94. agentloop/judgment_benchmarks.py +606 -0
  95. agentloop/judgment_types.py +241 -0
  96. agentloop/judgment_views.py +38 -0
  97. agentloop/judgments.py +342 -0
  98. agentloop/loop_guards.py +274 -0
  99. agentloop/loop_types.py +66 -0
  100. agentloop/markdown.py +102 -0
  101. agentloop/metrics.py +342 -0
  102. agentloop/migrations.py +303 -0
  103. agentloop/model_routing.py +470 -0
  104. agentloop/onboarding.py +315 -0
  105. agentloop/operations.py +44 -0
  106. agentloop/optimizer.py +199 -0
  107. agentloop/otel.py +715 -0
  108. agentloop/otel_ids.py +81 -0
  109. agentloop/otel_semantics.py +134 -0
  110. agentloop/parallelism.py +141 -0
  111. agentloop/patches.py +603 -0
  112. agentloop/plan_export.py +126 -0
  113. agentloop/policy_promotion.py +325 -0
  114. agentloop/promotion_evidence.py +304 -0
  115. agentloop/promotion_types.py +209 -0
  116. agentloop/quality.py +446 -0
  117. agentloop/ranking.py +529 -0
  118. agentloop/recovery_evidence.py +96 -0
  119. agentloop/replay.py +753 -0
  120. agentloop/retention.py +375 -0
  121. agentloop/retention_types.py +113 -0
  122. agentloop/reuse_experiments.py +247 -0
  123. agentloop/routing_checks.py +116 -0
  124. agentloop/routing_types.py +241 -0
  125. agentloop/rules.py +396 -0
  126. agentloop/runtime.py +286 -0
  127. agentloop/savings.py +158 -0
  128. agentloop/scheduling_plan.py +99 -0
  129. agentloop/scheduling_types.py +169 -0
  130. agentloop/schema.py +257 -0
  131. agentloop/semantic_rules.py +200 -0
  132. agentloop/semantic_waste.py +345 -0
  133. agentloop/semantic_waste_types.py +88 -0
  134. agentloop/server.py +494 -0
  135. agentloop/store.py +1429 -0
  136. agentloop/structural_experiments.py +431 -0
  137. agentloop/structural_types.py +113 -0
  138. agentloop/structured_quality.py +1020 -0
  139. agentloop/studies.py +629 -0
  140. agentloop/study_statistics.py +58 -0
  141. agentloop/substitution_evidence.py +199 -0
  142. agentloop/substitution_export.py +190 -0
  143. agentloop/substitution_reports.py +407 -0
  144. agentloop/substitution_types.py +284 -0
  145. agentloop/substitutions.py +267 -0
  146. agentloop/timing.py +142 -0
  147. agentloop/tokens.py +188 -0
  148. agentloop/tool_scheduling.py +410 -0
  149. agentloop/trace_sources.py +31 -0
  150. agentloop/tracer.py +494 -0
  151. agentloop/value.py +306 -0
  152. agentloop/version.py +3 -0
  153. agentloop/workflow_types.py +264 -0
  154. agentloop/workflows.py +284 -0
  155. agentloop_profiler-0.1.0.dist-info/METADATA +513 -0
  156. agentloop_profiler-0.1.0.dist-info/RECORD +166 -0
  157. agentloop_profiler-0.1.0.dist-info/WHEEL +5 -0
  158. agentloop_profiler-0.1.0.dist-info/entry_points.txt +2 -0
  159. agentloop_profiler-0.1.0.dist-info/licenses/LICENSE +201 -0
  160. agentloop_profiler-0.1.0.dist-info/licenses/THIRD_PARTY_LICENSES.md +101 -0
  161. agentloop_profiler-0.1.0.dist-info/top_level.txt +2 -0
  162. dashboard/__init__.py +5 -0
  163. dashboard/app.py +922 -0
  164. dashboard/streamlit_compat.py +63 -0
  165. dashboard/trace_upload.py +57 -0
  166. dashboard/value_view.py +105 -0
@@ -0,0 +1,337 @@
1
+ """Explicit remove/compress candidates on the generic experiment contract."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ from copy import deepcopy
7
+ from dataclasses import asdict, dataclass, replace
8
+ from fractions import Fraction
9
+
10
+ from agentloop.budget_types import Reservation, ResourceUsage
11
+ from agentloop.context_types import EXACT_COUNTS, ContextTokenCount, identifier, synchronous
12
+ from agentloop.experiment_observations import observation, record_experiment_observations
13
+ from agentloop.experiment_types import (
14
+ ExperimentResult,
15
+ ExperimentRunner,
16
+ canonical,
17
+ fingerprint,
18
+ reference,
19
+ )
20
+ from agentloop.tracer import current_trace
21
+
22
+ CONTEXT_EXPERIMENT_KEY = "agentloop.context_experiment"
23
+
24
+
25
+ def _path(value):
26
+ if (
27
+ not isinstance(value, (list, tuple))
28
+ or not 1 <= len(value) <= 32
29
+ or any(
30
+ not (isinstance(part, str) and 0 < len(part) <= 256 or type(part) is int and part >= 0)
31
+ for part in value
32
+ )
33
+ ):
34
+ raise ValueError("context paths require bounded string keys or nonnegative indexes")
35
+ return tuple(value)
36
+
37
+
38
+ def _overlap(left, right):
39
+ return left[: len(right)] == right or right[: len(left)] == left
40
+
41
+
42
+ def _value(payload, path):
43
+ value = payload
44
+ for part in path:
45
+ if isinstance(value, dict) and isinstance(part, str) and part in value:
46
+ value = value[part]
47
+ elif isinstance(value, list) and type(part) is int and part < len(value):
48
+ value = value[part]
49
+ else:
50
+ raise ValueError("context selection is missing or incompatible")
51
+ return value
52
+
53
+
54
+ def _deadline(request):
55
+ if request.remaining_s <= 0:
56
+ raise TimeoutError("experiment template deadline expired")
57
+
58
+
59
+ def _count_kind(count):
60
+ return (
61
+ "observed"
62
+ if count.provenance in EXACT_COUNTS
63
+ else "estimated"
64
+ if count.provenance == "estimated_words"
65
+ else "declared"
66
+ )
67
+
68
+
69
+ def _close_invalid(value):
70
+ if inspect.iscoroutine(value) or inspect.isgenerator(value):
71
+ value.close()
72
+
73
+
74
+ @dataclass(frozen=True)
75
+ class ContextSelection:
76
+ selection_id: str
77
+ path: tuple[str | int, ...]
78
+ source_ref: str
79
+ action: str = "remove"
80
+
81
+ def __post_init__(self):
82
+ identifier(self.selection_id, "selection_id")
83
+ reference(self.source_ref, "selection reference")
84
+ object.__setattr__(self, "path", _path(self.path))
85
+ if self.action not in {"remove", "compress"}:
86
+ raise ValueError("context action must be remove or compress")
87
+
88
+
89
+ def _sum_usage(usages):
90
+ if len(usages) == 1:
91
+ return usages[0]
92
+ ids = [value.usage_id for value in usages if value.usage_id is not None]
93
+ if len(ids) != len(set(ids)):
94
+ raise ValueError("compression and backend usage must be exclusive")
95
+ complete = all(value.complete and value.exclusive for value in usages)
96
+ tokens = (
97
+ sum(value.tokens for value in usages)
98
+ if all(value.tokens_known for value in usages)
99
+ else None
100
+ )
101
+ cost = (
102
+ float(sum((Fraction(str(value.cost_usd)) for value in usages), Fraction(0)))
103
+ if all(value.cost_known for value in usages)
104
+ else None
105
+ )
106
+ return ResourceUsage(
107
+ tokens=tokens,
108
+ cost_usd=cost,
109
+ token_provenance="user_supplied" if tokens is not None else "unavailable",
110
+ cost_provenance="user_reported" if cost is not None else "unavailable",
111
+ complete=complete,
112
+ )
113
+
114
+
115
+ def context_reduction_runner(
116
+ candidate_id,
117
+ backend,
118
+ *,
119
+ selections,
120
+ protected_paths=(),
121
+ token_counter=None,
122
+ counter_ref=None,
123
+ compressor=None,
124
+ compressor_ref=None,
125
+ on_missing="error",
126
+ reservation=None,
127
+ ):
128
+ if (
129
+ type(backend) is not ExperimentRunner
130
+ or not isinstance(selections, (tuple, list))
131
+ or not 1 <= len(selections) <= 64
132
+ or any(type(item) is not ContextSelection for item in selections)
133
+ ):
134
+ raise ValueError("context experiments require an explicit backend and selections")
135
+ selections = tuple(selections)
136
+ if not isinstance(protected_paths, (list, tuple)) or len(protected_paths) > 64:
137
+ raise ValueError("protected paths must be a bounded sequence")
138
+ protected = tuple(_path(path) for path in protected_paths)
139
+ if len(protected) > 64 or len({item.selection_id for item in selections}) != len(selections):
140
+ raise ValueError("context declarations must be unique and bounded")
141
+ if any(
142
+ _overlap(item.path, other.path)
143
+ for index, item in enumerate(selections)
144
+ for other in selections[index + 1 :]
145
+ ) or any(_overlap(item.path, path) for item in selections for path in protected):
146
+ raise ValueError("selected paths overlap each other or protected context")
147
+ compressing = any(item.action == "compress" for item in selections)
148
+ if compressing and (not synchronous(compressor) or compressor_ref is None):
149
+ raise ValueError("compression requires a trusted explicit callback/reference")
150
+ if compressor_ref is not None:
151
+ reference(compressor_ref, "compressor_ref")
152
+ if token_counter is not None and (not synchronous(token_counter) or counter_ref is None):
153
+ raise ValueError("token counter requires a trusted callback and reference")
154
+ if counter_ref is not None:
155
+ identifier(counter_ref, "counter_ref")
156
+ if on_missing not in {"error", "keep_original"}:
157
+ raise ValueError("unknown missing-selection policy")
158
+ config = {
159
+ "schema_version": "1.0",
160
+ "backend": backend.declaration(),
161
+ "selections": [asdict(item) for item in selections],
162
+ "protected_paths": protected,
163
+ "counter_ref": counter_ref,
164
+ "compressor_ref": compressor_ref,
165
+ "on_missing": on_missing,
166
+ }
167
+ config_hash = fingerprint(config)
168
+ source = "context-config:" + config_hash
169
+
170
+ def count(payload):
171
+ if token_counter is None:
172
+ return ContextTokenCount()
173
+ result = token_counter(deepcopy(payload))
174
+ if type(result) is not ContextTokenCount or result.reference not in {None, counter_ref}:
175
+ _close_invalid(result)
176
+ raise ValueError("token counter result/reference differs from declaration")
177
+ return result
178
+
179
+ def invoke(request):
180
+ if backend.declaration() != config["backend"]:
181
+ raise ValueError("context backend binding changed after configuration")
182
+ before = request.inputs
183
+ trace = current_trace()
184
+ if trace is None:
185
+ raise ValueError("context candidate requires an active experiment")
186
+ receipt = {
187
+ "schema_version": "1.0",
188
+ "config_hash": config_hash,
189
+ "input_hash": fingerprint(before),
190
+ "selections": [],
191
+ "state": "started",
192
+ "output_input_hash": None,
193
+ "compression_usage": [],
194
+ }
195
+ trace.metadata[CONTEXT_EXPERIMENT_KEY] = receipt
196
+ usages = []
197
+ try:
198
+ _deadline(request)
199
+ before_count = count(before)
200
+ after = deepcopy(before)
201
+ try:
202
+ values = {item.selection_id: _value(before, item.path) for item in selections}
203
+ except ValueError:
204
+ receipt["state"] = "selection_missing"
205
+ record_experiment_observations(
206
+ {
207
+ "context_applied": observation(False, unit="boolean", source_ref=source),
208
+ "context_state": observation(
209
+ "selection_missing", unit="state", source_ref=source
210
+ ),
211
+ }
212
+ )
213
+ if on_missing == "error":
214
+ raise
215
+ _deadline(request)
216
+ return backend.invoke(request)
217
+ ordered = sorted(
218
+ selections,
219
+ key=lambda item: tuple(
220
+ (0, part) if type(part) is int else (1, part) for part in item.path
221
+ ),
222
+ reverse=True,
223
+ )
224
+ for item in ordered:
225
+ value = values[item.selection_id]
226
+ parent = _value(after, item.path[:-1]) if len(item.path) > 1 else after
227
+ record = {
228
+ "selection_id": item.selection_id,
229
+ "source_ref": item.source_ref,
230
+ "path_hash": fingerprint(item.path),
231
+ "source_hash": fingerprint(value),
232
+ "action": item.action,
233
+ "replacement_hash": None,
234
+ }
235
+ receipt["selections"].append(record)
236
+ if item.action == "remove":
237
+ del parent[item.path[-1]]
238
+ else:
239
+ _deadline(request)
240
+ replacement = compressor(deepcopy(value), request)
241
+ if type(replacement) is not ExperimentResult:
242
+ _close_invalid(replacement)
243
+ raise ValueError("compressor must return ExperimentResult with usage")
244
+ parent[item.path[-1]] = replacement.output
245
+ record["replacement_hash"] = fingerprint(replacement.output)
246
+ usages.append(replacement.usage)
247
+ receipt["compression_usage"].append(replacement.usage.to_dict())
248
+ _deadline(request)
249
+ after_count = count(after)
250
+ same_basis = (
251
+ before_count.reference == after_count.reference
252
+ and before_count.provenance == after_count.provenance
253
+ )
254
+ exact = (
255
+ same_basis
256
+ and before_count.provenance in EXACT_COUNTS
257
+ and before_count.value is not None
258
+ and after_count.value is not None
259
+ )
260
+ delta = (
261
+ before_count.value - after_count.value
262
+ if same_basis and before_count.value is not None and after_count.value is not None
263
+ else None
264
+ )
265
+ kind = (
266
+ "observed"
267
+ if exact
268
+ else "estimated"
269
+ if same_basis and before_count.provenance == "estimated_words"
270
+ else "declared"
271
+ )
272
+ receipt.update(
273
+ state="applied",
274
+ output_input_hash=fingerprint(after),
275
+ token_counts={"before": before_count.to_dict(), "after": after_count.to_dict()},
276
+ compression_usage=[usage.to_dict() for usage in usages],
277
+ )
278
+ record_experiment_observations(
279
+ {
280
+ "context_applied": observation(True, unit="boolean", source_ref=source),
281
+ "context_state": observation("applied", unit="state", source_ref=source),
282
+ "input_tokens_before": observation(
283
+ before_count.value,
284
+ kind=_count_kind(before_count),
285
+ unit="tokens",
286
+ source_ref=source,
287
+ ),
288
+ "input_tokens_after": observation(
289
+ after_count.value,
290
+ kind=_count_kind(after_count),
291
+ unit="tokens",
292
+ source_ref=source,
293
+ ),
294
+ "input_tokens_removed": observation(
295
+ delta, kind=kind, unit="tokens", source_ref=source
296
+ ),
297
+ "exact_token_reduction": observation(
298
+ delta > 0 if exact else None, unit="boolean", source_ref=source
299
+ ),
300
+ }
301
+ )
302
+ _deadline(request)
303
+ result = backend.invoke(replace(request, _input_json=canonical(after)))
304
+ if type(result) is not ExperimentResult:
305
+ _close_invalid(result)
306
+ raise ValueError("backend must return ExperimentResult")
307
+ receipt["backend_usage"] = result.usage.to_dict()
308
+ usages.append(result.usage)
309
+ return ExperimentResult(result.output, usage=_sum_usage(usages))
310
+ except BaseException:
311
+ if receipt["state"] == "started":
312
+ receipt["state"] = "failed"
313
+ try:
314
+ record_experiment_observations(
315
+ {
316
+ "context_applied": observation(
317
+ False, unit="boolean", source_ref=source
318
+ ),
319
+ "context_state": observation("failed", unit="state", source_ref=source),
320
+ }
321
+ )
322
+ except BaseException:
323
+ receipt["capture_error"] = "observation_capture_failed"
324
+ raise
325
+
326
+ return ExperimentRunner(
327
+ candidate_id,
328
+ "1.0",
329
+ "agentloop.context-reduction:1.0",
330
+ "sha256:" + config_hash,
331
+ invoke,
332
+ reservation=reservation
333
+ if reservation is not None
334
+ else Reservation()
335
+ if compressing
336
+ else backend.reservation,
337
+ )
@@ -0,0 +1,216 @@
1
+ """Explicit, owned request contracts for optional tool-result summaries."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ import json
7
+ import re
8
+ from dataclasses import asdict, dataclass, field
9
+ from hashlib import sha256
10
+ from typing import Callable
11
+
12
+ from agentloop.budget_types import DispatchOptions, ResourceUsage
13
+
14
+ CONTEXT_VERSION = "1.0"
15
+ CONTEXT_KEY = "agentloop.context_transform"
16
+ _IDENTIFIER = re.compile(r"[A-Za-z0-9_.:-]{1,96}\Z")
17
+ EXACT_COUNTS = {"provider", "tokenizer", "user_supplied"}
18
+ _REASONS = frozenset(
19
+ "invalid_history invalid_role invalid_tool_calls unsupported_content "
20
+ "unpaired_tool_result missing_tool_result invalid_tool_reference ambiguous_tool_call "
21
+ "token_counter_failed invalid_token_count unsupported_adapter streaming_not_supported "
22
+ "unknown_tool_selection summary_count_limit summary_failed invalid_summary "
23
+ "token_counter_changed context_limit_unverified tokens_not_reduced "
24
+ "unexpected_streaming_result recursive_context_transform evidence_limit "
25
+ "invalid_trace_evidence invalid_json_request invalid_request request_too_large "
26
+ "invalid_evidence_selection transformation_error".split()
27
+ )
28
+
29
+
30
+ class ContextTransformError(ValueError):
31
+ """A bounded failure category; never include request or summary content."""
32
+
33
+ def __init__(self, reason):
34
+ if not isinstance(reason, str) or reason not in _REASONS:
35
+ reason = "transformation_error"
36
+ self.reason_code = reason
37
+ super().__init__(f"Context transformation failed: {reason}")
38
+
39
+
40
+ def canonical(value):
41
+ try:
42
+
43
+ def validate_keys(item):
44
+ if isinstance(item, dict):
45
+ if any(not isinstance(key, str) for key in item):
46
+ raise TypeError("non-string JSON key")
47
+ for child in item.values():
48
+ validate_keys(child)
49
+ elif isinstance(item, (list, tuple)):
50
+ for child in item:
51
+ validate_keys(child)
52
+
53
+ validate_keys(value)
54
+ return json.dumps(
55
+ value, sort_keys=True, separators=(",", ":"), ensure_ascii=False, allow_nan=False
56
+ )
57
+ except (ValueError, TypeError, RecursionError):
58
+ raise ContextTransformError("invalid_json_request") from None
59
+
60
+
61
+ def fingerprint(value):
62
+ return sha256(canonical(value).encode("utf-8")).hexdigest()
63
+
64
+
65
+ def identifier(value, label):
66
+ if not isinstance(value, str) or not _IDENTIFIER.fullmatch(value):
67
+ raise ValueError(f"{label} must be a bounded identifier")
68
+
69
+
70
+ def synchronous(function):
71
+ return callable(function) and not any(
72
+ check(function) or check(getattr(function, "__call__", None))
73
+ for check in (
74
+ inspect.iscoroutinefunction,
75
+ inspect.isgeneratorfunction,
76
+ inspect.isasyncgenfunction,
77
+ )
78
+ )
79
+
80
+
81
+ @dataclass(frozen=True, init=False)
82
+ class ContextRequest:
83
+ """Own a JSON request; every payload export is a fresh deep copy.
84
+
85
+ Only explicitly optional tool results can change. All other messages and
86
+ all provider options are preserved. Protected tool results take precedence.
87
+ """
88
+
89
+ _json: str = field(repr=False)
90
+ optional_tool_results: tuple[str, ...]
91
+ protected_tool_results: tuple[str, ...]
92
+
93
+ def __init__(self, payload, *, optional_tool_results=(), protected_tool_results=()):
94
+ if not isinstance(payload, dict):
95
+ raise ContextTransformError("invalid_request")
96
+ canonical(payload) # Validate finite JSON without changing option ordering.
97
+ encoded = json.dumps(payload, ensure_ascii=False, separators=(",", ":"), allow_nan=False)
98
+ if len(encoded.encode("utf-8")) > 1_000_000:
99
+ raise ContextTransformError("request_too_large")
100
+ for name, values in (
101
+ ("optional_tool_results", optional_tool_results),
102
+ ("protected_tool_results", protected_tool_results),
103
+ ):
104
+ if (
105
+ not isinstance(values, (tuple, list, set, frozenset))
106
+ or len(values) > 256
107
+ or any(
108
+ not isinstance(value, str) or not value or len(value) > 256 for value in values
109
+ )
110
+ ):
111
+ raise ContextTransformError("invalid_evidence_selection")
112
+ object.__setattr__(self, name, tuple(sorted(set(values))))
113
+ object.__setattr__(self, "_json", encoded)
114
+
115
+ @property
116
+ def payload(self):
117
+ return json.loads(self._json)
118
+
119
+
120
+ @dataclass(frozen=True)
121
+ class ContextPolicyConfig:
122
+ policy_id: str
123
+ version: str
124
+ branch_id: str
125
+ quality_ref: str
126
+ max_summary_chars: int = 512
127
+ max_summaries: int = 1
128
+ max_input_tokens: int | None = None
129
+ on_invalid: str = "error"
130
+
131
+ def __post_init__(self):
132
+ for name in ("policy_id", "version", "branch_id", "quality_ref"):
133
+ identifier(getattr(self, name), name)
134
+ for name, maximum in (("max_summary_chars", 8000), ("max_summaries", 8)):
135
+ value = getattr(self, name)
136
+ if type(value) is not int or not 1 <= value <= maximum:
137
+ raise ValueError(f"{name} must be a positive bounded integer")
138
+ if self.max_input_tokens is not None and (
139
+ type(self.max_input_tokens) is not int or self.max_input_tokens < 1
140
+ ):
141
+ raise ValueError("max_input_tokens must be positive when configured")
142
+ if self.on_invalid not in {"error", "original"}:
143
+ raise ValueError("on_invalid must be error or original")
144
+
145
+ def to_dict(self):
146
+ return asdict(self)
147
+
148
+
149
+ @dataclass(frozen=True)
150
+ class ContextAdapter:
151
+ """Host declaration for the first supported synchronous text-chat boundary."""
152
+
153
+ name: str
154
+ before_model: bool = True
155
+ format: str = "text_tool_messages_v1"
156
+
157
+ def __post_init__(self):
158
+ identifier(self.name, "adapter name")
159
+ if type(self.before_model) is not bool or not isinstance(self.format, str):
160
+ raise ValueError("invalid adapter declaration")
161
+
162
+
163
+ @dataclass(frozen=True)
164
+ class ContextTokenCount:
165
+ value: int | None = None
166
+ provenance: str = "unavailable"
167
+ reference: str | None = None
168
+
169
+ def __post_init__(self):
170
+ if self.provenance not in EXACT_COUNTS | {"estimated_words", "unavailable"}:
171
+ raise ValueError("unsupported token provenance")
172
+ if self.value is not None and (type(self.value) is not int or self.value < 0):
173
+ raise ValueError("token count must be nonnegative")
174
+ if self.provenance == "unavailable" and self.value is not None:
175
+ raise ValueError("unavailable token count cannot claim a value")
176
+ if self.provenance != "unavailable" and (self.value is None or self.reference is None):
177
+ raise ValueError("available token counts require a versioned reference")
178
+ if self.reference is not None:
179
+ identifier(self.reference, "tokenizer reference")
180
+
181
+ def to_dict(self):
182
+ return asdict(self)
183
+
184
+
185
+ @dataclass(frozen=True)
186
+ class SummaryInput:
187
+ """Untrusted tool data; the callback must preserve this trust classification."""
188
+
189
+ content: str = field(repr=False)
190
+ max_chars: int
191
+ source_hash: str
192
+ trust_class: str = field(default="tool", init=False)
193
+
194
+
195
+ @dataclass(frozen=True)
196
+ class SummaryResult:
197
+ text: str = field(repr=False)
198
+ usage: ResourceUsage | None = None
199
+
200
+ def __post_init__(self):
201
+ if not isinstance(self.text, str) or (
202
+ self.usage is not None and type(self.usage) is not ResourceUsage
203
+ ):
204
+ raise ValueError("summary requires text and normalized usage")
205
+
206
+
207
+ @dataclass(frozen=True)
208
+ class SummaryBackend:
209
+ reference: str
210
+ summarize: Callable[[SummaryInput], SummaryResult] = field(repr=False, compare=False)
211
+ dispatch: DispatchOptions = field(default_factory=DispatchOptions)
212
+
213
+ def __post_init__(self):
214
+ identifier(self.reference, "summarizer reference")
215
+ if not synchronous(self.summarize) or type(self.dispatch) is not DispatchOptions:
216
+ raise ValueError("summarizer must be a synchronous declared dispatch")