millforge 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.
- millforge/__init__.py +1174 -0
- millforge/_forge/LICENSE +21 -0
- millforge/_forge/PROVENANCE.json +295 -0
- millforge/_forge/UPDATE_POLICY.md +24 -0
- millforge/_forge/__init__.py +14 -0
- millforge/_forge/adapter.py +2232 -0
- millforge/_forge/base_runner.py +121 -0
- millforge/_forge/clients/__init__.py +10 -0
- millforge/_forge/clients/base.py +200 -0
- millforge/_forge/context/__init__.py +23 -0
- millforge/_forge/context/manager.py +178 -0
- millforge/_forge/context/strategies.py +335 -0
- millforge/_forge/core/__init__.py +16 -0
- millforge/_forge/core/inference.py +433 -0
- millforge/_forge/core/messages.py +119 -0
- millforge/_forge/core/runner.py +479 -0
- millforge/_forge/core/steps.py +108 -0
- millforge/_forge/core/workflow.py +400 -0
- millforge/_forge/errors.py +222 -0
- millforge/_forge/guardrails/__init__.py +21 -0
- millforge/_forge/guardrails/error_tracker.py +71 -0
- millforge/_forge/guardrails/guardrails.py +194 -0
- millforge/_forge/guardrails/nudge.py +47 -0
- millforge/_forge/guardrails/response_validator.py +119 -0
- millforge/_forge/guardrails/step_enforcer.py +183 -0
- millforge/_forge/prompts/__init__.py +16 -0
- millforge/_forge/prompts/nudges.py +95 -0
- millforge/_forge/prompts/templates.py +285 -0
- millforge/_version.py +3 -0
- millforge/artifacts.py +570 -0
- millforge/base/__init__.py +97 -0
- millforge/base/composition.py +402 -0
- millforge/base/context.py +285 -0
- millforge/base/harness.py +138 -0
- millforge/base/identity.py +465 -0
- millforge/base/options.py +34 -0
- millforge/base/platform.py +17 -0
- millforge/base/prompt.py +317 -0
- millforge/base/runner.py +546 -0
- millforge/compiled_plan.py +970 -0
- millforge/compiler/__init__.py +231 -0
- millforge/compiler/artifact_validation.py +257 -0
- millforge/compiler/canonicalization.py +169 -0
- millforge/compiler/capabilities.py +66 -0
- millforge/compiler/catalogs.py +500 -0
- millforge/compiler/diagnostics.py +491 -0
- millforge/compiler/graph.py +678 -0
- millforge/compiler/lowering.py +198 -0
- millforge/compiler/output.py +692 -0
- millforge/compiler/parsing.py +1424 -0
- millforge/compiler/requests.py +1180 -0
- millforge/compiler/schema_validation.py +272 -0
- millforge/compiler/semantic.py +490 -0
- millforge/compiler/service.py +448 -0
- millforge/compiler/source.py +375 -0
- millforge/compiler/validators.py +184 -0
- millforge/connectors/__init__.py +95 -0
- millforge/connectors/admission.py +801 -0
- millforge/connectors/broker.py +202 -0
- millforge/connectors/contracts.py +1159 -0
- millforge/connectors/diagnostics.py +189 -0
- millforge/connectors/fake.py +66 -0
- millforge/connectors/runtime.py +236 -0
- millforge/contracts.py +2860 -0
- millforge/custom_tools/__init__.py +67 -0
- millforge/custom_tools/compiler.py +724 -0
- millforge/custom_tools/contracts.py +1093 -0
- millforge/custom_tools/diagnostics.py +205 -0
- millforge/eval_artifacts.py +952 -0
- millforge/eval_boundary.py +2435 -0
- millforge/eval_fixtures/__init__.py +1 -0
- millforge/eval_fixtures/default_pack/__init__.py +1 -0
- millforge/eval_fixtures/default_pack/fixtures/fixture.08a.bug_diagnosis.traceback.v1.json +52 -0
- millforge/eval_fixtures/default_pack/fixtures/fixture.08a.direct_edit.import_sort.v1.json +52 -0
- millforge/eval_fixtures/default_pack/fixtures/fixture.08a.evidence_discipline.no_source_change.v1.json +51 -0
- millforge/eval_fixtures/default_pack/fixtures/fixture.08a.false_closure.visible_green.v1.json +52 -0
- millforge/eval_fixtures/default_pack/fixtures/fixture.08a.multi_file.api_contract.v1.json +54 -0
- millforge/eval_fixtures/default_pack/fixtures/fixture.08a.recovery.malformed_artifact.v1.json +54 -0
- millforge/eval_fixtures/default_pack/manifest.json +12 -0
- millforge/eval_modes.py +1282 -0
- millforge/eval_presets.py +1398 -0
- millforge/eval_reports.py +2517 -0
- millforge/eval_suite.py +2429 -0
- millforge/eval_trials.py +2632 -0
- millforge/eval_workflow.py +794 -0
- millforge/exceptions.py +122 -0
- millforge/model_backend.py +2098 -0
- millforge/protocols.py +340 -0
- millforge/py.typed +0 -0
- millforge/runtime.py +1791 -0
- millforge/testing/__init__.py +1089 -0
- millforge/tools/__init__.py +83 -0
- millforge/tools/builtin_runtime.py +1339 -0
- millforge/tools/builtins.py +773 -0
- millforge/tools/execution.py +1545 -0
- millforge/tools/path_policy.py +155 -0
- millforge/tools/pi_compat/PI_LICENSE +21 -0
- millforge/tools/pi_compat/PROVENANCE.json +55 -0
- millforge/tools/pi_compat/UPDATE_POLICY.md +36 -0
- millforge/tools/pi_compat/__init__.py +34 -0
- millforge/tools/pi_compat/contracts.py +49 -0
- millforge/tools/pi_compat/editing.py +390 -0
- millforge/tools/pi_compat/mutations.py +57 -0
- millforge/tools/pi_compat/operations.py +401 -0
- millforge/tools/pi_compat/paths.py +155 -0
- millforge/tools/pi_compat/process.py +1375 -0
- millforge/tools/pi_compat/search.py +738 -0
- millforge/tools/pi_compat/truncation.py +267 -0
- millforge/tools/pi_compat_catalog.py +396 -0
- millforge/tools/pi_compat_runtime.py +460 -0
- millforge/tools/registry.py +553 -0
- millforge/tools/results.py +533 -0
- millforge-0.1.0.dist-info/METADATA +844 -0
- millforge-0.1.0.dist-info/RECORD +116 -0
- millforge-0.1.0.dist-info/WHEEL +4 -0
- millforge-0.1.0.dist-info/licenses/LICENSE +201 -0
millforge/contracts.py
ADDED
|
@@ -0,0 +1,2860 @@
|
|
|
1
|
+
"""Contract models for the Millforge runtime.
|
|
2
|
+
|
|
3
|
+
All models are defined using Pydantic v2 APIs with ``extra="forbid"``
|
|
4
|
+
(closed-world) validation. Immutable models use ``frozen=True``;
|
|
5
|
+
mutable working models are explicitly noted in their docstrings.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
import json
|
|
12
|
+
import math
|
|
13
|
+
import re
|
|
14
|
+
from collections.abc import Mapping
|
|
15
|
+
from enum import Enum
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import (
|
|
18
|
+
Annotated,
|
|
19
|
+
Any,
|
|
20
|
+
Callable,
|
|
21
|
+
Dict,
|
|
22
|
+
Literal,
|
|
23
|
+
Optional,
|
|
24
|
+
Self,
|
|
25
|
+
Tuple,
|
|
26
|
+
TypeAlias,
|
|
27
|
+
)
|
|
28
|
+
from urllib.parse import parse_qsl, urlsplit, urlunsplit
|
|
29
|
+
|
|
30
|
+
from pydantic import (
|
|
31
|
+
BaseModel,
|
|
32
|
+
ConfigDict,
|
|
33
|
+
Field,
|
|
34
|
+
StrictBool,
|
|
35
|
+
field_validator,
|
|
36
|
+
model_serializer,
|
|
37
|
+
model_validator,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
from millforge.compiled_plan import (
|
|
41
|
+
CompiledArtifactPolicy,
|
|
42
|
+
DiagnosticField,
|
|
43
|
+
IdempotencyClass,
|
|
44
|
+
SessionEvent,
|
|
45
|
+
SideEffectCertainty,
|
|
46
|
+
SideEffectClass,
|
|
47
|
+
StageIdentity,
|
|
48
|
+
ToolBindingRef,
|
|
49
|
+
ToolExecutionStatus,
|
|
50
|
+
ToolTraceRecord,
|
|
51
|
+
canonical_json_serialize,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
_SHA256_RE = re.compile(r"[0-9a-f]{64}")
|
|
55
|
+
_SANITIZED_METADATA_MAX_ITEMS = 32
|
|
56
|
+
_SANITIZED_METADATA_KEY_MAX_LENGTH = 64
|
|
57
|
+
_SANITIZED_METADATA_STRING_MAX_LENGTH = 2048
|
|
58
|
+
_SANITIZED_METADATA_BYTES_MAX_LENGTH = 32768
|
|
59
|
+
_REDACTION_DEFAULT_DEPTH = 8
|
|
60
|
+
_REDACTION_DEFAULT_COLLECTION_ITEMS = 64
|
|
61
|
+
_REDACTION_DEFAULT_STRING_LENGTH = 2048
|
|
62
|
+
_REDACTION_DEFAULT_TOTAL_BYTES = 32768
|
|
63
|
+
_REDACTION_MAX_DEPTH = 32
|
|
64
|
+
_REDACTION_MAX_COLLECTION_ITEMS = 1024
|
|
65
|
+
_REDACTION_MAX_STRING_LENGTH = 64 * 1024 * 1024
|
|
66
|
+
_REDACTION_MAX_TOTAL_BYTES = 64 * 1024 * 1024
|
|
67
|
+
_SECRET_PATTERNS = (
|
|
68
|
+
re.compile(
|
|
69
|
+
r"(?i)\b[A-Z][A-Z0-9_]*(?:SECRET|TOKEN|PASSWORD|API_KEY)[A-Z0-9_]*=([^\s]+)"
|
|
70
|
+
),
|
|
71
|
+
re.compile(r"(?i)(api[_-]?key|token|secret|password)=([^&\s]+)"),
|
|
72
|
+
re.compile(r"(?i)(bearer\s+)[a-z0-9._~+/=-]+"),
|
|
73
|
+
re.compile(r"\b(sk|pk|org|sess)-[a-zA-Z0-9]{8,}\b"),
|
|
74
|
+
)
|
|
75
|
+
_URL_PATTERN = re.compile(r"\b[a-z][a-z0-9+.-]*://[^\s<>'\"]+", re.IGNORECASE)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _nonblank(value: str, field_name: str) -> str:
|
|
79
|
+
if not value.strip():
|
|
80
|
+
raise ValueError(f"{field_name} must be a non-empty string")
|
|
81
|
+
return value
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _unique(values: tuple[str, ...], field_name: str) -> None:
|
|
85
|
+
if len(set(values)) != len(values):
|
|
86
|
+
raise ValueError(f"{field_name} values must be unique")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _validate_sha256(value: str, field_name: str) -> str:
|
|
90
|
+
if not _SHA256_RE.fullmatch(value):
|
|
91
|
+
raise ValueError(f"{field_name} must be exactly 64 lowercase hex characters")
|
|
92
|
+
return value
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _is_sensitive_field_name(value: str, policy: RedactionPolicy) -> bool:
|
|
96
|
+
lowered = value.lower()
|
|
97
|
+
compact = lowered.replace("_", "").replace("-", "")
|
|
98
|
+
return any(
|
|
99
|
+
marker in lowered or marker.replace("-", "") in compact
|
|
100
|
+
for marker in policy.sensitive_field_markers
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
JsonScalar: TypeAlias = str | int | float | bool | None
|
|
105
|
+
JsonValue: TypeAlias = Any
|
|
106
|
+
JsonObject: TypeAlias = dict[str, JsonValue]
|
|
107
|
+
SanitizedMetadataValue = JsonScalar
|
|
108
|
+
|
|
109
|
+
# Public global ceilings for one invocation-local selected JSON output.
|
|
110
|
+
MAX_SELECTED_OUTPUT_SCHEMA_BYTES = 64 * 1024
|
|
111
|
+
MAX_SELECTED_OUTPUT_PAYLOAD_BYTES = 1024 * 1024
|
|
112
|
+
MAX_SELECTED_OUTPUT_NESTING_DEPTH = 16
|
|
113
|
+
MAX_SELECTED_OUTPUT_OBJECT_PROPERTIES = 64
|
|
114
|
+
MAX_SELECTED_OUTPUT_ARRAY_ITEMS = 1024
|
|
115
|
+
MAX_SELECTED_OUTPUT_STRING_LENGTH = 64 * 1024
|
|
116
|
+
|
|
117
|
+
_SELECTED_OUTPUT_TYPES = {
|
|
118
|
+
"object",
|
|
119
|
+
"array",
|
|
120
|
+
"string",
|
|
121
|
+
"integer",
|
|
122
|
+
"number",
|
|
123
|
+
"boolean",
|
|
124
|
+
"null",
|
|
125
|
+
}
|
|
126
|
+
_SELECTED_OUTPUT_SCHEMA_KEYWORDS = {
|
|
127
|
+
"object": {"type", "properties", "required", "additionalProperties"},
|
|
128
|
+
"array": {"type", "items", "minItems", "maxItems"},
|
|
129
|
+
"string": {"type", "minLength", "maxLength"},
|
|
130
|
+
"integer": {"type"},
|
|
131
|
+
"number": {"type"},
|
|
132
|
+
"boolean": {"type"},
|
|
133
|
+
"null": {"type"},
|
|
134
|
+
}
|
|
135
|
+
_SELECTED_OUTPUT_VALUE_CONSTRAINTS = {"const", "enum"}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class _FrozenSelectedOutputDict(dict[str, Any]):
|
|
139
|
+
"""Internal recursively frozen dict that retains JSON serialization shape."""
|
|
140
|
+
|
|
141
|
+
@staticmethod
|
|
142
|
+
def _immutable(*_args: Any, **_kwargs: Any) -> None:
|
|
143
|
+
raise TypeError("selected output authority is immutable")
|
|
144
|
+
|
|
145
|
+
__setitem__ = _immutable
|
|
146
|
+
__delitem__ = _immutable
|
|
147
|
+
clear = _immutable
|
|
148
|
+
pop = _immutable
|
|
149
|
+
popitem = _immutable # type: ignore[assignment]
|
|
150
|
+
setdefault = _immutable
|
|
151
|
+
update = _immutable # type: ignore[assignment]
|
|
152
|
+
__ior__ = _immutable # type: ignore[assignment]
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class _FrozenSelectedOutputList(list[Any]):
|
|
156
|
+
"""Internal recursively frozen list that retains JSON serialization shape."""
|
|
157
|
+
|
|
158
|
+
@staticmethod
|
|
159
|
+
def _immutable(*_args: Any, **_kwargs: Any) -> None:
|
|
160
|
+
raise TypeError("selected output authority is immutable")
|
|
161
|
+
|
|
162
|
+
__setitem__ = _immutable
|
|
163
|
+
__delitem__ = _immutable
|
|
164
|
+
__iadd__ = _immutable # type: ignore[assignment]
|
|
165
|
+
__imul__ = _immutable # type: ignore[assignment]
|
|
166
|
+
append = _immutable
|
|
167
|
+
clear = _immutable
|
|
168
|
+
extend = _immutable
|
|
169
|
+
insert = _immutable
|
|
170
|
+
pop = _immutable
|
|
171
|
+
remove = _immutable
|
|
172
|
+
reverse = _immutable
|
|
173
|
+
sort = _immutable
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _freeze_selected_output_json(value: JsonValue) -> JsonValue:
|
|
177
|
+
if isinstance(value, dict):
|
|
178
|
+
return _FrozenSelectedOutputDict(
|
|
179
|
+
{key: _freeze_selected_output_json(item) for key, item in value.items()}
|
|
180
|
+
)
|
|
181
|
+
if isinstance(value, list):
|
|
182
|
+
return _FrozenSelectedOutputList(
|
|
183
|
+
_freeze_selected_output_json(item) for item in value
|
|
184
|
+
)
|
|
185
|
+
return value
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _reject_selected_output_duplicate_keys(
|
|
189
|
+
pairs: list[tuple[str, Any]],
|
|
190
|
+
) -> dict[str, Any]:
|
|
191
|
+
result: dict[str, Any] = {}
|
|
192
|
+
for key, value in pairs:
|
|
193
|
+
if key in result:
|
|
194
|
+
raise ValueError(f"selected output JSON contains duplicate key {key!r}")
|
|
195
|
+
result[key] = value
|
|
196
|
+
return result
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _reject_selected_output_constant(value: str) -> None:
|
|
200
|
+
raise ValueError(f"selected output JSON contains non-finite number {value}")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _parse_selected_output_float(value: str) -> float:
|
|
204
|
+
parsed = float(value)
|
|
205
|
+
if not math.isfinite(parsed):
|
|
206
|
+
raise ValueError("selected output JSON contains a non-finite number")
|
|
207
|
+
return parsed
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _parse_selected_output_json(raw: str | bytes, *, field_name: str) -> JsonValue:
|
|
211
|
+
if isinstance(raw, bytes):
|
|
212
|
+
try:
|
|
213
|
+
text = raw.decode("utf-8")
|
|
214
|
+
except UnicodeDecodeError as exc:
|
|
215
|
+
raise ValueError(f"{field_name} must be UTF-8 JSON") from exc
|
|
216
|
+
else:
|
|
217
|
+
text = raw
|
|
218
|
+
try:
|
|
219
|
+
return json.loads(
|
|
220
|
+
text,
|
|
221
|
+
object_pairs_hook=_reject_selected_output_duplicate_keys,
|
|
222
|
+
parse_constant=_reject_selected_output_constant,
|
|
223
|
+
parse_float=_parse_selected_output_float,
|
|
224
|
+
)
|
|
225
|
+
except (json.JSONDecodeError, ValueError) as exc:
|
|
226
|
+
raise ValueError(f"{field_name} must be strict JSON: {exc}") from exc
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class _RawJsonObject(list[tuple[str, Any]]):
|
|
230
|
+
"""A JSON object represented as its ordered raw key/value pairs."""
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _parse_json_objects_preserving_pairs(
|
|
234
|
+
raw: str | bytes | bytearray,
|
|
235
|
+
) -> JsonValue | None:
|
|
236
|
+
"""Parse JSON while retaining object key pairs for strict raw validation."""
|
|
237
|
+
try:
|
|
238
|
+
return json.loads(raw, object_pairs_hook=_RawJsonObject)
|
|
239
|
+
except (UnicodeDecodeError, json.JSONDecodeError, TypeError):
|
|
240
|
+
# Let Pydantic preserve its normal raw-JSON validation behavior.
|
|
241
|
+
return None
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _validate_raw_json_strictness(value: JsonValue, *, field_name: str) -> None:
|
|
245
|
+
"""Reject duplicate object keys and non-finite numbers before conversion."""
|
|
246
|
+
if isinstance(value, _RawJsonObject):
|
|
247
|
+
seen: set[str] = set()
|
|
248
|
+
for key, item in value:
|
|
249
|
+
if key in seen:
|
|
250
|
+
raise ValueError(f"{field_name} contains duplicate key {key!r}")
|
|
251
|
+
seen.add(key)
|
|
252
|
+
_validate_raw_json_strictness(item, field_name=field_name)
|
|
253
|
+
elif isinstance(value, list):
|
|
254
|
+
for item in value:
|
|
255
|
+
_validate_raw_json_strictness(item, field_name=field_name)
|
|
256
|
+
elif type(value) is float and not math.isfinite(value):
|
|
257
|
+
raise ValueError(f"{field_name} contains a non-finite number")
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _validate_harness_request_raw_json(
|
|
261
|
+
raw: str | bytes | bytearray,
|
|
262
|
+
) -> None:
|
|
263
|
+
"""Reject ambiguous or non-finite values throughout a raw request."""
|
|
264
|
+
parsed = _parse_json_objects_preserving_pairs(raw)
|
|
265
|
+
if parsed is None:
|
|
266
|
+
return
|
|
267
|
+
_validate_raw_json_strictness(parsed, field_name="request JSON")
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _validate_selected_output_bound(
|
|
271
|
+
schema: Mapping[str, Any],
|
|
272
|
+
*,
|
|
273
|
+
minimum_key: str,
|
|
274
|
+
maximum_key: str,
|
|
275
|
+
global_maximum: int,
|
|
276
|
+
) -> None:
|
|
277
|
+
minimum = schema.get(minimum_key, 0)
|
|
278
|
+
maximum = schema.get(maximum_key, global_maximum)
|
|
279
|
+
if type(minimum) is not int or type(maximum) is not int:
|
|
280
|
+
raise ValueError(f"{minimum_key} and {maximum_key} must be integers")
|
|
281
|
+
if minimum < 0 or maximum < 0:
|
|
282
|
+
raise ValueError(f"{minimum_key} and {maximum_key} must be non-negative")
|
|
283
|
+
if minimum > maximum:
|
|
284
|
+
raise ValueError(f"{minimum_key} must not exceed {maximum_key}")
|
|
285
|
+
if maximum > global_maximum:
|
|
286
|
+
raise ValueError(f"{maximum_key} exceeds the selected output global ceiling")
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _normalize_selected_output_scalar(value: Any, *, field_name: str) -> JsonScalar:
|
|
290
|
+
if value is None or type(value) in {bool, int}:
|
|
291
|
+
return value
|
|
292
|
+
if type(value) is float:
|
|
293
|
+
if not math.isfinite(value):
|
|
294
|
+
raise ValueError(f"{field_name} contains a non-finite number")
|
|
295
|
+
return value
|
|
296
|
+
if type(value) is str:
|
|
297
|
+
if len(value) > MAX_SELECTED_OUTPUT_STRING_LENGTH:
|
|
298
|
+
raise ValueError(f"{field_name} string exceeds the string ceiling")
|
|
299
|
+
return value
|
|
300
|
+
raise ValueError(f"{field_name} must contain strict JSON scalar values")
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _selected_output_scalar_matches_type(value: JsonScalar, schema_type: str) -> bool:
|
|
304
|
+
if schema_type == "string":
|
|
305
|
+
return isinstance(value, str)
|
|
306
|
+
if schema_type == "integer":
|
|
307
|
+
return isinstance(value, int) and not isinstance(value, bool)
|
|
308
|
+
if schema_type == "number":
|
|
309
|
+
return isinstance(value, int | float) and not isinstance(value, bool)
|
|
310
|
+
if schema_type == "boolean":
|
|
311
|
+
return isinstance(value, bool)
|
|
312
|
+
if schema_type == "null":
|
|
313
|
+
return value is None
|
|
314
|
+
return False
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _canonical_selected_output_scalar_bytes(value: JsonScalar) -> bytes:
|
|
318
|
+
return canonical_json_serialize(value).encode("utf-8")
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _normalize_selected_output_schema(
|
|
322
|
+
schema: Any,
|
|
323
|
+
*,
|
|
324
|
+
depth: int,
|
|
325
|
+
) -> JsonObject:
|
|
326
|
+
if depth > MAX_SELECTED_OUTPUT_NESTING_DEPTH:
|
|
327
|
+
raise ValueError("selected output schema exceeds the nesting-depth ceiling")
|
|
328
|
+
if not isinstance(schema, Mapping):
|
|
329
|
+
raise ValueError("every selected output schema node must be a JSON object")
|
|
330
|
+
if any(not isinstance(key, str) for key in schema):
|
|
331
|
+
raise ValueError("selected output schema object keys must be strings")
|
|
332
|
+
|
|
333
|
+
schema_type = schema.get("type")
|
|
334
|
+
constraints = _SELECTED_OUTPUT_VALUE_CONSTRAINTS.intersection(schema)
|
|
335
|
+
if len(constraints) > 1:
|
|
336
|
+
raise ValueError("selected output schema cannot contain both const and enum")
|
|
337
|
+
if schema_type is None:
|
|
338
|
+
if len(constraints) != 1:
|
|
339
|
+
raise ValueError(
|
|
340
|
+
"selected output schema type is outside the admitted subset"
|
|
341
|
+
)
|
|
342
|
+
unsupported = set(schema) - constraints
|
|
343
|
+
if unsupported:
|
|
344
|
+
rendered = ", ".join(sorted(unsupported))
|
|
345
|
+
raise ValueError(
|
|
346
|
+
f"unsupported selected output schema keyword(s): {rendered}"
|
|
347
|
+
)
|
|
348
|
+
normalized: JsonObject = {}
|
|
349
|
+
elif not isinstance(schema_type, str) or schema_type not in _SELECTED_OUTPUT_TYPES:
|
|
350
|
+
raise ValueError("selected output schema type is outside the admitted subset")
|
|
351
|
+
else:
|
|
352
|
+
unsupported = set(schema) - (
|
|
353
|
+
_SELECTED_OUTPUT_SCHEMA_KEYWORDS[schema_type]
|
|
354
|
+
| _SELECTED_OUTPUT_VALUE_CONSTRAINTS
|
|
355
|
+
)
|
|
356
|
+
if unsupported:
|
|
357
|
+
rendered = ", ".join(sorted(unsupported))
|
|
358
|
+
raise ValueError(
|
|
359
|
+
f"unsupported selected output schema keyword(s): {rendered}"
|
|
360
|
+
)
|
|
361
|
+
normalized = {"type": schema_type}
|
|
362
|
+
|
|
363
|
+
if schema_type == "object":
|
|
364
|
+
if schema.get("additionalProperties") is not False:
|
|
365
|
+
raise ValueError("object schemas require additionalProperties=false")
|
|
366
|
+
properties = schema.get("properties", {})
|
|
367
|
+
required = schema.get("required", [])
|
|
368
|
+
if not isinstance(properties, Mapping):
|
|
369
|
+
raise ValueError("object schema properties must be a JSON object")
|
|
370
|
+
if any(not isinstance(key, str) for key in properties):
|
|
371
|
+
raise ValueError("selected output property names must be strings")
|
|
372
|
+
if len(properties) > MAX_SELECTED_OUTPUT_OBJECT_PROPERTIES:
|
|
373
|
+
raise ValueError(
|
|
374
|
+
"selected output schema exceeds the object-property ceiling"
|
|
375
|
+
)
|
|
376
|
+
if not isinstance(required, list) or any(
|
|
377
|
+
not isinstance(item, str) for item in required
|
|
378
|
+
):
|
|
379
|
+
raise ValueError("object schema required must be an array of strings")
|
|
380
|
+
if len(set(required)) != len(required):
|
|
381
|
+
raise ValueError("object schema required values must be unique")
|
|
382
|
+
unknown_required = set(required) - set(properties)
|
|
383
|
+
if unknown_required:
|
|
384
|
+
raise ValueError(
|
|
385
|
+
"object schema required values must name declared properties"
|
|
386
|
+
)
|
|
387
|
+
for property_name in properties:
|
|
388
|
+
if len(property_name) > MAX_SELECTED_OUTPUT_STRING_LENGTH:
|
|
389
|
+
raise ValueError("selected output property name exceeds string ceiling")
|
|
390
|
+
normalized["properties"] = {
|
|
391
|
+
key: _normalize_selected_output_schema(value, depth=depth + 1)
|
|
392
|
+
for key, value in properties.items()
|
|
393
|
+
}
|
|
394
|
+
normalized["required"] = sorted(required)
|
|
395
|
+
normalized["additionalProperties"] = False
|
|
396
|
+
elif schema_type == "array":
|
|
397
|
+
if "items" not in schema:
|
|
398
|
+
raise ValueError("array schemas require an items schema")
|
|
399
|
+
_validate_selected_output_bound(
|
|
400
|
+
schema,
|
|
401
|
+
minimum_key="minItems",
|
|
402
|
+
maximum_key="maxItems",
|
|
403
|
+
global_maximum=MAX_SELECTED_OUTPUT_ARRAY_ITEMS,
|
|
404
|
+
)
|
|
405
|
+
normalized["items"] = _normalize_selected_output_schema(
|
|
406
|
+
schema["items"],
|
|
407
|
+
depth=depth + 1,
|
|
408
|
+
)
|
|
409
|
+
if "minItems" in schema:
|
|
410
|
+
normalized["minItems"] = schema["minItems"]
|
|
411
|
+
if "maxItems" in schema:
|
|
412
|
+
normalized["maxItems"] = schema["maxItems"]
|
|
413
|
+
elif schema_type == "string":
|
|
414
|
+
_validate_selected_output_bound(
|
|
415
|
+
schema,
|
|
416
|
+
minimum_key="minLength",
|
|
417
|
+
maximum_key="maxLength",
|
|
418
|
+
global_maximum=MAX_SELECTED_OUTPUT_STRING_LENGTH,
|
|
419
|
+
)
|
|
420
|
+
if "minLength" in schema:
|
|
421
|
+
normalized["minLength"] = schema["minLength"]
|
|
422
|
+
if "maxLength" in schema:
|
|
423
|
+
normalized["maxLength"] = schema["maxLength"]
|
|
424
|
+
|
|
425
|
+
if "const" in constraints:
|
|
426
|
+
const = _normalize_selected_output_scalar(
|
|
427
|
+
schema["const"], field_name="selected output const"
|
|
428
|
+
)
|
|
429
|
+
if schema_type is not None and not _selected_output_scalar_matches_type(
|
|
430
|
+
const, schema_type
|
|
431
|
+
):
|
|
432
|
+
raise ValueError("selected output const does not satisfy schema type")
|
|
433
|
+
normalized["const"] = const
|
|
434
|
+
elif "enum" in constraints:
|
|
435
|
+
raw_enum = schema["enum"]
|
|
436
|
+
if not isinstance(raw_enum, list):
|
|
437
|
+
raise ValueError("selected output enum must be a JSON array")
|
|
438
|
+
if not 1 <= len(raw_enum) <= 64:
|
|
439
|
+
raise ValueError(
|
|
440
|
+
"selected output enum must contain between 1 and 64 values"
|
|
441
|
+
)
|
|
442
|
+
canonical_values: list[tuple[bytes, JsonScalar]] = []
|
|
443
|
+
seen: set[bytes] = set()
|
|
444
|
+
for value in raw_enum:
|
|
445
|
+
normalized_value = _normalize_selected_output_scalar(
|
|
446
|
+
value, field_name="selected output enum"
|
|
447
|
+
)
|
|
448
|
+
if schema_type is not None and not _selected_output_scalar_matches_type(
|
|
449
|
+
normalized_value, schema_type
|
|
450
|
+
):
|
|
451
|
+
raise ValueError(
|
|
452
|
+
"selected output enum value does not satisfy schema type"
|
|
453
|
+
)
|
|
454
|
+
canonical = _canonical_selected_output_scalar_bytes(normalized_value)
|
|
455
|
+
if canonical in seen:
|
|
456
|
+
raise ValueError("selected output enum values must be unique")
|
|
457
|
+
seen.add(canonical)
|
|
458
|
+
canonical_values.append((canonical, normalized_value))
|
|
459
|
+
normalized["enum"] = [
|
|
460
|
+
value
|
|
461
|
+
for _canonical, value in sorted(canonical_values, key=lambda item: item[0])
|
|
462
|
+
]
|
|
463
|
+
return normalized
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def canonical_selected_output_schema_bytes(
|
|
467
|
+
schema: str | bytes | Mapping[str, Any],
|
|
468
|
+
) -> bytes:
|
|
469
|
+
"""Admit and canonically serialize the closed selected-schema subset."""
|
|
470
|
+
if isinstance(schema, (str, bytes)):
|
|
471
|
+
raw_size = (
|
|
472
|
+
len(schema.encode("utf-8")) if isinstance(schema, str) else len(schema)
|
|
473
|
+
)
|
|
474
|
+
if raw_size > MAX_SELECTED_OUTPUT_SCHEMA_BYTES:
|
|
475
|
+
raise ValueError("selected output schema exceeds the schema-byte ceiling")
|
|
476
|
+
parsed = _parse_selected_output_json(
|
|
477
|
+
schema, field_name="selected output schema"
|
|
478
|
+
)
|
|
479
|
+
else:
|
|
480
|
+
parsed = schema
|
|
481
|
+
normalized = _normalize_selected_output_schema(parsed, depth=1)
|
|
482
|
+
canonical = canonical_json_serialize(normalized).encode("utf-8")
|
|
483
|
+
if len(canonical) > MAX_SELECTED_OUTPUT_SCHEMA_BYTES:
|
|
484
|
+
raise ValueError("selected output schema exceeds the schema-byte ceiling")
|
|
485
|
+
return canonical
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def selected_output_schema_sha256(
|
|
489
|
+
schema: str | bytes | Mapping[str, Any],
|
|
490
|
+
) -> str:
|
|
491
|
+
"""Return the SHA-256 digest of an admitted canonical selected schema."""
|
|
492
|
+
return hashlib.sha256(canonical_selected_output_schema_bytes(schema)).hexdigest()
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def _normalize_selected_output_payload(value: Any, *, depth: int) -> JsonValue:
|
|
496
|
+
if depth > MAX_SELECTED_OUTPUT_NESTING_DEPTH:
|
|
497
|
+
raise ValueError("selected output payload exceeds the nesting-depth ceiling")
|
|
498
|
+
if value is None or type(value) in {bool, int}:
|
|
499
|
+
return value
|
|
500
|
+
if type(value) is float:
|
|
501
|
+
if not math.isfinite(value):
|
|
502
|
+
raise ValueError("selected output payload contains a non-finite number")
|
|
503
|
+
return value
|
|
504
|
+
if isinstance(value, str):
|
|
505
|
+
if len(value) > MAX_SELECTED_OUTPUT_STRING_LENGTH:
|
|
506
|
+
raise ValueError(
|
|
507
|
+
"selected output payload string exceeds the string ceiling"
|
|
508
|
+
)
|
|
509
|
+
return value
|
|
510
|
+
if isinstance(value, list):
|
|
511
|
+
if len(value) > MAX_SELECTED_OUTPUT_ARRAY_ITEMS:
|
|
512
|
+
raise ValueError("selected output payload exceeds the array-item ceiling")
|
|
513
|
+
return [
|
|
514
|
+
_normalize_selected_output_payload(item, depth=depth + 1) for item in value
|
|
515
|
+
]
|
|
516
|
+
if isinstance(value, Mapping):
|
|
517
|
+
if any(not isinstance(key, str) for key in value):
|
|
518
|
+
raise ValueError("selected output payload object keys must be strings")
|
|
519
|
+
if len(value) > MAX_SELECTED_OUTPUT_OBJECT_PROPERTIES:
|
|
520
|
+
raise ValueError(
|
|
521
|
+
"selected output payload exceeds the object-property ceiling"
|
|
522
|
+
)
|
|
523
|
+
for key in value:
|
|
524
|
+
if len(key) > MAX_SELECTED_OUTPUT_STRING_LENGTH:
|
|
525
|
+
raise ValueError(
|
|
526
|
+
"selected output payload key exceeds the string ceiling"
|
|
527
|
+
)
|
|
528
|
+
return {
|
|
529
|
+
key: _normalize_selected_output_payload(item, depth=depth + 1)
|
|
530
|
+
for key, item in value.items()
|
|
531
|
+
}
|
|
532
|
+
raise ValueError("selected output payload contains a non-JSON value")
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def canonical_selected_output_payload_bytes(value: JsonValue) -> bytes:
|
|
536
|
+
"""Validate global JSON bounds and return canonical selected payload bytes."""
|
|
537
|
+
normalized = _normalize_selected_output_payload(value, depth=1)
|
|
538
|
+
canonical = canonical_json_serialize(normalized).encode("utf-8")
|
|
539
|
+
if len(canonical) > MAX_SELECTED_OUTPUT_PAYLOAD_BYTES:
|
|
540
|
+
raise ValueError("selected output payload exceeds the payload-byte ceiling")
|
|
541
|
+
return canonical
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def parse_selected_output_payload_json(raw: str | bytes) -> JsonValue:
|
|
545
|
+
"""Parse strict JSON, rejecting duplicate keys and all global bound violations."""
|
|
546
|
+
raw_size = len(raw.encode("utf-8")) if isinstance(raw, str) else len(raw)
|
|
547
|
+
if raw_size > MAX_SELECTED_OUTPUT_PAYLOAD_BYTES:
|
|
548
|
+
raise ValueError("selected output payload exceeds the payload-byte ceiling")
|
|
549
|
+
parsed = _parse_selected_output_json(raw, field_name="selected output payload")
|
|
550
|
+
canonical_selected_output_payload_bytes(parsed)
|
|
551
|
+
return parsed
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
# ---------------------------------------------------------------------------
|
|
555
|
+
# Closed enums
|
|
556
|
+
# ---------------------------------------------------------------------------
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
class ExecutionStatus(str, Enum):
|
|
560
|
+
"""Closed enum of possible execution statuses for a run or session."""
|
|
561
|
+
|
|
562
|
+
COMPLETED = "completed"
|
|
563
|
+
FAILED = "failed"
|
|
564
|
+
INTERRUPTED = "interrupted"
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
class ExecutionResultClass(str, Enum):
|
|
568
|
+
"""Closed enum of possible execution result classifications."""
|
|
569
|
+
|
|
570
|
+
DOMAIN_TERMINAL = "domain_terminal"
|
|
571
|
+
DOMAIN_REJECTED = "domain_rejected"
|
|
572
|
+
BINDING_REJECTED = "binding_rejected"
|
|
573
|
+
COMPILED_HARNESS_INVALID = "compiled_harness_invalid"
|
|
574
|
+
BACKEND_FAILURE = "backend_failure"
|
|
575
|
+
MODEL_FAILURE = "model_failure"
|
|
576
|
+
TOOL_FAILURE = "tool_failure"
|
|
577
|
+
BUDGET_EXHAUSTED = "budget_exhausted"
|
|
578
|
+
TIMED_OUT = "timed_out"
|
|
579
|
+
CANCELLED = "cancelled"
|
|
580
|
+
TERMINAL_RESULT_INVALID = "terminal_result_invalid"
|
|
581
|
+
ARTIFACT_FINALIZATION_FAILED = "artifact_finalization_failed"
|
|
582
|
+
INTERNAL_FAILURE = "internal_failure"
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
class TerminalCertainty(str, Enum):
|
|
586
|
+
"""Certainty of terminal-result commit ordering."""
|
|
587
|
+
|
|
588
|
+
NOT_APPLICABLE = "not_applicable"
|
|
589
|
+
COMMITTED = "committed"
|
|
590
|
+
UNKNOWN = "unknown"
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
class GuardedSessionStatus(str, Enum):
|
|
594
|
+
"""Closed enum of possible guarded session statuses."""
|
|
595
|
+
|
|
596
|
+
TERMINAL = "terminal"
|
|
597
|
+
REJECTED = "rejected"
|
|
598
|
+
BACKEND_FAILED = "backend_failed"
|
|
599
|
+
MODEL_FAILED = "model_failed"
|
|
600
|
+
TOOL_FAILED = "tool_failed"
|
|
601
|
+
BUDGET_EXHAUSTED = "budget_exhausted"
|
|
602
|
+
PREREQUISITE_BUDGET_EXHAUSTED = "prerequisite_budget_exhausted"
|
|
603
|
+
TIMED_OUT = "timed_out"
|
|
604
|
+
CANCELLED = "cancelled"
|
|
605
|
+
INVALID_TERMINAL = "invalid_terminal"
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
class TimeoutOrigin(str, Enum):
|
|
609
|
+
"""Closed timeout origins that preserve the stable timeout result class."""
|
|
610
|
+
|
|
611
|
+
SESSION_DEADLINE = "session_deadline"
|
|
612
|
+
MODEL_CONNECT_TIMEOUT = "model_connect_timeout"
|
|
613
|
+
MODEL_READ_TIMEOUT = "model_read_timeout"
|
|
614
|
+
MODEL_WRITE_TIMEOUT = "model_write_timeout"
|
|
615
|
+
MODEL_POOL_TIMEOUT = "model_pool_timeout"
|
|
616
|
+
TOOL_TIMEOUT = "tool_timeout"
|
|
617
|
+
BACKEND_TIMEOUT = "backend_timeout"
|
|
618
|
+
ARTIFACT_FINALIZATION_TIMEOUT = "artifact_finalization_timeout"
|
|
619
|
+
CLEANUP_TIMEOUT = "cleanup_timeout"
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
# ---------------------------------------------------------------------------
|
|
623
|
+
# New standalone types
|
|
624
|
+
# ---------------------------------------------------------------------------
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
class Deadline(BaseModel):
|
|
628
|
+
"""Immutable monotonic deadline specification."""
|
|
629
|
+
|
|
630
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
631
|
+
|
|
632
|
+
started_monotonic: float = Field(
|
|
633
|
+
ge=0, description="Monotonic time when deadline evaluation started"
|
|
634
|
+
)
|
|
635
|
+
outer_deadline_monotonic: float = Field(
|
|
636
|
+
ge=0, description="Outer request deadline as monotonic seconds"
|
|
637
|
+
)
|
|
638
|
+
compiled_harness_deadline_monotonic: float | None = Field(
|
|
639
|
+
default=None,
|
|
640
|
+
ge=0,
|
|
641
|
+
description="Optional smaller compiled harness deadline as monotonic seconds",
|
|
642
|
+
)
|
|
643
|
+
effective_deadline_monotonic: float = Field(
|
|
644
|
+
ge=0, description="Effective deadline after all bounds are applied"
|
|
645
|
+
)
|
|
646
|
+
source: Literal["request", "compiled_harness", "request_and_harness"] = Field(
|
|
647
|
+
description="Source used to derive the effective deadline"
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
@model_validator(mode="after")
|
|
651
|
+
def _check_deadline_ordering(self) -> Deadline:
|
|
652
|
+
if self.outer_deadline_monotonic < self.started_monotonic:
|
|
653
|
+
raise ValueError("outer_deadline_monotonic must not precede start")
|
|
654
|
+
if self.effective_deadline_monotonic < self.started_monotonic:
|
|
655
|
+
raise ValueError("effective_deadline_monotonic must not precede start")
|
|
656
|
+
if self.effective_deadline_monotonic > self.outer_deadline_monotonic:
|
|
657
|
+
raise ValueError(
|
|
658
|
+
"effective_deadline_monotonic must not exceed outer deadline"
|
|
659
|
+
)
|
|
660
|
+
if self.compiled_harness_deadline_monotonic is not None:
|
|
661
|
+
if self.compiled_harness_deadline_monotonic < self.started_monotonic:
|
|
662
|
+
raise ValueError(
|
|
663
|
+
"compiled_harness_deadline_monotonic must not precede start"
|
|
664
|
+
)
|
|
665
|
+
expected = min(
|
|
666
|
+
self.outer_deadline_monotonic,
|
|
667
|
+
self.compiled_harness_deadline_monotonic,
|
|
668
|
+
)
|
|
669
|
+
if self.effective_deadline_monotonic != expected:
|
|
670
|
+
raise ValueError(
|
|
671
|
+
"effective_deadline_monotonic must equal the smaller request "
|
|
672
|
+
"or compiled harness deadline"
|
|
673
|
+
)
|
|
674
|
+
if self.source == "request":
|
|
675
|
+
raise ValueError(
|
|
676
|
+
"source=request is invalid when compiled harness deadline is present"
|
|
677
|
+
)
|
|
678
|
+
elif self.effective_deadline_monotonic != self.outer_deadline_monotonic:
|
|
679
|
+
raise ValueError(
|
|
680
|
+
"effective_deadline_monotonic must equal outer deadline when no "
|
|
681
|
+
"compiled harness deadline is present"
|
|
682
|
+
)
|
|
683
|
+
return self
|
|
684
|
+
|
|
685
|
+
@property
|
|
686
|
+
def request_deadline_monotonic(self) -> float:
|
|
687
|
+
"""Return the absolute request deadline in monotonic seconds."""
|
|
688
|
+
return self.outer_deadline_monotonic
|
|
689
|
+
|
|
690
|
+
def remaining(self, clock: Callable[[], float] | Any) -> float:
|
|
691
|
+
"""Return non-negative seconds remaining against the effective deadline."""
|
|
692
|
+
now = clock() if callable(clock) else clock.monotonic()
|
|
693
|
+
return max(0.0, self.effective_deadline_monotonic - float(now))
|
|
694
|
+
|
|
695
|
+
@classmethod
|
|
696
|
+
def from_deadlines(
|
|
697
|
+
cls,
|
|
698
|
+
*,
|
|
699
|
+
started_monotonic: float,
|
|
700
|
+
request_deadline_monotonic: float,
|
|
701
|
+
compiled_harness_deadline_monotonic: float | None = None,
|
|
702
|
+
) -> Deadline:
|
|
703
|
+
"""Build a deadline with effective time derived from admitted bounds."""
|
|
704
|
+
if compiled_harness_deadline_monotonic is None:
|
|
705
|
+
source: Literal["request", "compiled_harness", "request_and_harness"] = (
|
|
706
|
+
"request"
|
|
707
|
+
)
|
|
708
|
+
effective = request_deadline_monotonic
|
|
709
|
+
else:
|
|
710
|
+
effective = min(
|
|
711
|
+
request_deadline_monotonic,
|
|
712
|
+
compiled_harness_deadline_monotonic,
|
|
713
|
+
)
|
|
714
|
+
source = (
|
|
715
|
+
"compiled_harness"
|
|
716
|
+
if compiled_harness_deadline_monotonic < request_deadline_monotonic
|
|
717
|
+
else "request_and_harness"
|
|
718
|
+
)
|
|
719
|
+
return cls(
|
|
720
|
+
started_monotonic=started_monotonic,
|
|
721
|
+
outer_deadline_monotonic=request_deadline_monotonic,
|
|
722
|
+
compiled_harness_deadline_monotonic=compiled_harness_deadline_monotonic,
|
|
723
|
+
effective_deadline_monotonic=effective,
|
|
724
|
+
source=source,
|
|
725
|
+
)
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
class TokenUsage(BaseModel):
|
|
729
|
+
"""Immutable token usage breakdown for a model interaction."""
|
|
730
|
+
|
|
731
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
732
|
+
|
|
733
|
+
input_tokens: int = Field(ge=0, description="Number of input (prompt) tokens")
|
|
734
|
+
output_tokens: int = Field(ge=0, description="Number of output (completion) tokens")
|
|
735
|
+
total_tokens: int = Field(ge=0, description="Total tokens consumed")
|
|
736
|
+
provider_reported: bool = Field(
|
|
737
|
+
description="Whether the usage was reported by the provider"
|
|
738
|
+
)
|
|
739
|
+
|
|
740
|
+
@model_validator(mode="after")
|
|
741
|
+
def _total_matches_parts(self) -> TokenUsage:
|
|
742
|
+
if self.total_tokens != self.input_tokens + self.output_tokens:
|
|
743
|
+
raise ValueError("total_tokens must equal input_tokens + output_tokens")
|
|
744
|
+
return self
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
# ---------------------------------------------------------------------------
|
|
748
|
+
# Secret reference
|
|
749
|
+
# ---------------------------------------------------------------------------
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
class SecretRef(BaseModel):
|
|
753
|
+
"""Reference to a secret stored outside the contract boundary.
|
|
754
|
+
|
|
755
|
+
Stores an opaque handle (``secret_id``) and the environment-variable
|
|
756
|
+
name that resolves to the secret value. **The secret value itself
|
|
757
|
+
must never appear in any contract field or serialization output.**
|
|
758
|
+
"""
|
|
759
|
+
|
|
760
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
761
|
+
|
|
762
|
+
secret_id: str = Field(description="Unique secret identifier")
|
|
763
|
+
env_var: str = Field(description="Environment variable name holding the secret")
|
|
764
|
+
|
|
765
|
+
@field_validator("secret_id")
|
|
766
|
+
@classmethod
|
|
767
|
+
def _secret_id_must_be_non_empty(cls, v: str) -> str:
|
|
768
|
+
if not v.strip():
|
|
769
|
+
raise ValueError("secret_id must be a non-empty string")
|
|
770
|
+
return v
|
|
771
|
+
|
|
772
|
+
@field_validator("env_var")
|
|
773
|
+
@classmethod
|
|
774
|
+
def _env_var_must_be_non_empty(cls, v: str) -> str:
|
|
775
|
+
if not v.strip():
|
|
776
|
+
raise ValueError("env_var must be a non-empty string")
|
|
777
|
+
return v
|
|
778
|
+
|
|
779
|
+
|
|
780
|
+
# ---------------------------------------------------------------------------
|
|
781
|
+
# Identity / Reference models (immutable snapshots)
|
|
782
|
+
# ---------------------------------------------------------------------------
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
class CompiledHarnessIdentity(BaseModel):
|
|
786
|
+
"""Immutable identity of a compiled harness plan."""
|
|
787
|
+
|
|
788
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
789
|
+
|
|
790
|
+
compiled_plan_id: str = Field(description="Unique identifier for the compiled plan")
|
|
791
|
+
harness_id: str = Field(description="Harness identifier")
|
|
792
|
+
harness_version: int = Field(gt=0, description="Positive harness version")
|
|
793
|
+
|
|
794
|
+
@field_validator("compiled_plan_id", "harness_id")
|
|
795
|
+
@classmethod
|
|
796
|
+
def _strings_nonblank(cls, value: str, info: Any) -> str:
|
|
797
|
+
return _nonblank(value, info.field_name)
|
|
798
|
+
|
|
799
|
+
|
|
800
|
+
class CompiledHarnessHash(BaseModel):
|
|
801
|
+
"""Immutable cryptographic hash of a compiled harness."""
|
|
802
|
+
|
|
803
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
804
|
+
|
|
805
|
+
algorithm: Literal["sha256"] = Field(description="Hash algorithm (sha256 only)")
|
|
806
|
+
digest: str = Field(description="Hex-encoded digest value")
|
|
807
|
+
|
|
808
|
+
@field_validator("digest")
|
|
809
|
+
@classmethod
|
|
810
|
+
def _digest_valid(cls, value: str) -> str:
|
|
811
|
+
return _validate_sha256(value, "digest")
|
|
812
|
+
|
|
813
|
+
|
|
814
|
+
class CompiledHarnessRef(BaseModel):
|
|
815
|
+
"""Immutable reference to a compiled harness, including identity, path, and hash."""
|
|
816
|
+
|
|
817
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
818
|
+
|
|
819
|
+
identity: CompiledHarnessIdentity = Field(description="Compiled harness identity")
|
|
820
|
+
path: Path = Field(description="Filesystem path to the compiled harness")
|
|
821
|
+
expected_hash: CompiledHarnessHash = Field(
|
|
822
|
+
description="Expected cryptographic hash of the harness"
|
|
823
|
+
)
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
class RunDirRef(BaseModel):
|
|
827
|
+
"""Immutable reference to a run directory."""
|
|
828
|
+
|
|
829
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
830
|
+
|
|
831
|
+
run_id: str = Field(description="Unique run identifier")
|
|
832
|
+
path: Path = Field(description="Absolute or relative path to the run directory")
|
|
833
|
+
|
|
834
|
+
@field_validator("run_id")
|
|
835
|
+
@classmethod
|
|
836
|
+
def _run_id_nonblank(cls, value: str) -> str:
|
|
837
|
+
return _nonblank(value, "run_id")
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
class ArtifactRef(BaseModel):
|
|
841
|
+
"""Immutable reference to an artifact file."""
|
|
842
|
+
|
|
843
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
844
|
+
|
|
845
|
+
artifact_id: str = Field(description="Unique artifact identifier")
|
|
846
|
+
path: Path = Field(description="Path to the artifact file")
|
|
847
|
+
content_type: Optional[str] = Field(
|
|
848
|
+
default=None, description="MIME type or content format"
|
|
849
|
+
)
|
|
850
|
+
|
|
851
|
+
@field_validator("artifact_id")
|
|
852
|
+
@classmethod
|
|
853
|
+
def _artifact_id_nonblank(cls, value: str) -> str:
|
|
854
|
+
return _nonblank(value, "artifact_id")
|
|
855
|
+
|
|
856
|
+
@field_validator("content_type")
|
|
857
|
+
@classmethod
|
|
858
|
+
def _content_type_nonblank(cls, value: str | None) -> str | None:
|
|
859
|
+
return None if value is None else _nonblank(value, "content_type")
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
class HarnessTaskInput(BaseModel):
|
|
863
|
+
"""Exact bounded instruction supplied to an executable harness request."""
|
|
864
|
+
|
|
865
|
+
model_config = ConfigDict(
|
|
866
|
+
extra="forbid",
|
|
867
|
+
frozen=True,
|
|
868
|
+
hide_input_in_errors=True,
|
|
869
|
+
)
|
|
870
|
+
|
|
871
|
+
schema_version: Literal["1.0"] = "1.0"
|
|
872
|
+
instruction: str
|
|
873
|
+
|
|
874
|
+
@field_validator("instruction")
|
|
875
|
+
@classmethod
|
|
876
|
+
def _instruction_is_bounded(cls, value: str) -> str:
|
|
877
|
+
if not value.strip():
|
|
878
|
+
raise ValueError(
|
|
879
|
+
"instruction must be non-empty after whitespace inspection"
|
|
880
|
+
)
|
|
881
|
+
if "\x00" in value:
|
|
882
|
+
raise ValueError("instruction must not contain NUL")
|
|
883
|
+
try:
|
|
884
|
+
encoded = value.encode("utf-8")
|
|
885
|
+
except UnicodeEncodeError as exc:
|
|
886
|
+
raise ValueError("instruction must be valid UTF-8 text") from exc
|
|
887
|
+
if len(encoded) > 65_536:
|
|
888
|
+
raise ValueError("instruction must not exceed 65,536 UTF-8 bytes")
|
|
889
|
+
return value
|
|
890
|
+
|
|
891
|
+
@property
|
|
892
|
+
def utf8_byte_count(self) -> int:
|
|
893
|
+
"""Return the exact UTF-8 encoded instruction size."""
|
|
894
|
+
return len(self.instruction.encode("utf-8"))
|
|
895
|
+
|
|
896
|
+
@property
|
|
897
|
+
def sha256(self) -> str:
|
|
898
|
+
"""Return the lowercase SHA-256 of the exact UTF-8 instruction bytes."""
|
|
899
|
+
return hashlib.sha256(self.instruction.encode("utf-8")).hexdigest()
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
# ---------------------------------------------------------------------------
|
|
903
|
+
# Stage identity
|
|
904
|
+
# ---------------------------------------------------------------------------
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
# ---------------------------------------------------------------------------
|
|
908
|
+
# Capability models
|
|
909
|
+
# ---------------------------------------------------------------------------
|
|
910
|
+
|
|
911
|
+
CAPABILITY_GRANT_CONSTRAINTS_DESC = (
|
|
912
|
+
"Optional constraints applied to this capability grant"
|
|
913
|
+
)
|
|
914
|
+
|
|
915
|
+
|
|
916
|
+
class CapabilityGrant(BaseModel):
|
|
917
|
+
"""Immutable capability grant with capability identifier and optional constraints."""
|
|
918
|
+
|
|
919
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
920
|
+
|
|
921
|
+
capability_id: str = Field(description="Capability identifier")
|
|
922
|
+
constraints: Optional[Dict[str, Any]] = Field(
|
|
923
|
+
default=None, description=CAPABILITY_GRANT_CONSTRAINTS_DESC
|
|
924
|
+
)
|
|
925
|
+
|
|
926
|
+
@field_validator("capability_id")
|
|
927
|
+
@classmethod
|
|
928
|
+
def _capability_id_nonblank(cls, value: str) -> str:
|
|
929
|
+
return _nonblank(value, "capability_id")
|
|
930
|
+
|
|
931
|
+
|
|
932
|
+
class CapabilityEnvelope(BaseModel):
|
|
933
|
+
"""Immutable capability grant envelope containing a tuple of grants."""
|
|
934
|
+
|
|
935
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
936
|
+
|
|
937
|
+
grants: Tuple[CapabilityGrant, ...] = Field(
|
|
938
|
+
description="Tuple of capability grants"
|
|
939
|
+
)
|
|
940
|
+
|
|
941
|
+
@field_validator("grants")
|
|
942
|
+
@classmethod
|
|
943
|
+
def _grant_capabilities_unique(
|
|
944
|
+
cls, value: tuple[CapabilityGrant, ...]
|
|
945
|
+
) -> tuple[CapabilityGrant, ...]:
|
|
946
|
+
_unique(tuple(grant.capability_id for grant in value), "capability_id")
|
|
947
|
+
return value
|
|
948
|
+
|
|
949
|
+
|
|
950
|
+
# ---------------------------------------------------------------------------
|
|
951
|
+
# Profile and reference models
|
|
952
|
+
# ---------------------------------------------------------------------------
|
|
953
|
+
|
|
954
|
+
|
|
955
|
+
class ModelProfileRef(BaseModel):
|
|
956
|
+
"""Immutable reference to a model profile.
|
|
957
|
+
|
|
958
|
+
Contains only the profile identifier — model provider, name, and
|
|
959
|
+
details are resolved externally from the profile configuration.
|
|
960
|
+
"""
|
|
961
|
+
|
|
962
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
963
|
+
|
|
964
|
+
profile_id: str = Field(description="Model profile identifier")
|
|
965
|
+
|
|
966
|
+
@field_validator("profile_id")
|
|
967
|
+
@classmethod
|
|
968
|
+
def _profile_id_nonblank(cls, value: str) -> str:
|
|
969
|
+
return _nonblank(value, "profile_id")
|
|
970
|
+
|
|
971
|
+
|
|
972
|
+
class TimeoutRef(BaseModel):
|
|
973
|
+
"""Immutable timeout reference."""
|
|
974
|
+
|
|
975
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
976
|
+
|
|
977
|
+
timeout_seconds: float = Field(description="Timeout duration in seconds")
|
|
978
|
+
deadline: Optional[str] = Field(
|
|
979
|
+
default=None, description="ISO-8601 deadline timestamp"
|
|
980
|
+
)
|
|
981
|
+
|
|
982
|
+
@field_validator("timeout_seconds")
|
|
983
|
+
@classmethod
|
|
984
|
+
def _timeout_must_be_positive(cls, v: float) -> float:
|
|
985
|
+
if v <= 0:
|
|
986
|
+
raise ValueError("timeout_seconds must be a positive number")
|
|
987
|
+
return v
|
|
988
|
+
|
|
989
|
+
|
|
990
|
+
class CancellationRef(BaseModel):
|
|
991
|
+
"""Immutable cancellation reference."""
|
|
992
|
+
|
|
993
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
994
|
+
|
|
995
|
+
cancellation_id: str = Field(description="Cancellation identifier")
|
|
996
|
+
|
|
997
|
+
@field_validator("cancellation_id")
|
|
998
|
+
@classmethod
|
|
999
|
+
def _cancellation_id_must_be_non_empty(cls, v: str) -> str:
|
|
1000
|
+
if not v.strip():
|
|
1001
|
+
raise ValueError("cancellation_id must be a non-empty string")
|
|
1002
|
+
return v
|
|
1003
|
+
|
|
1004
|
+
|
|
1005
|
+
# ---------------------------------------------------------------------------
|
|
1006
|
+
class ConnectorApprovalGrant(BaseModel):
|
|
1007
|
+
"""Runtime-only grant authorizing one exact connector approval scope."""
|
|
1008
|
+
|
|
1009
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
1010
|
+
|
|
1011
|
+
connector_id: str = Field(description="Connector identity authorized by runtime")
|
|
1012
|
+
provider_tool_name: str = Field(description="Provider tool name authorized")
|
|
1013
|
+
tool_id: str = Field(description="Compiled connector tool identifier")
|
|
1014
|
+
tool_version: int = Field(ge=1, description="Compiled connector tool version")
|
|
1015
|
+
descriptor_sha256: str = Field(description="Compiled descriptor hash")
|
|
1016
|
+
request_id: str = Field(description="Runtime request identifier")
|
|
1017
|
+
run_id: str = Field(description="Runtime run identifier")
|
|
1018
|
+
stage: StageIdentity = Field(description="Runtime stage identity")
|
|
1019
|
+
approval_policy: Literal["millrace_explicit"] = Field(
|
|
1020
|
+
description="Approval policy authorized by the runtime"
|
|
1021
|
+
)
|
|
1022
|
+
expires_at_monotonic: float = Field(
|
|
1023
|
+
ge=0,
|
|
1024
|
+
description="Trusted monotonic timestamp after which the grant is invalid",
|
|
1025
|
+
)
|
|
1026
|
+
approval_id: str | None = Field(
|
|
1027
|
+
default=None, description="Operator or runtime approval identifier"
|
|
1028
|
+
)
|
|
1029
|
+
nonce: str | None = Field(
|
|
1030
|
+
default=None, description="Opaque runtime nonce for one approval grant"
|
|
1031
|
+
)
|
|
1032
|
+
|
|
1033
|
+
@field_validator(
|
|
1034
|
+
"connector_id",
|
|
1035
|
+
"provider_tool_name",
|
|
1036
|
+
"tool_id",
|
|
1037
|
+
"request_id",
|
|
1038
|
+
"run_id",
|
|
1039
|
+
"approval_id",
|
|
1040
|
+
"nonce",
|
|
1041
|
+
)
|
|
1042
|
+
@classmethod
|
|
1043
|
+
def _grant_text_nonblank(cls, value: str | None, info: Any) -> str | None:
|
|
1044
|
+
if value is None:
|
|
1045
|
+
return None
|
|
1046
|
+
return _nonblank(value, info.field_name)
|
|
1047
|
+
|
|
1048
|
+
@field_validator("descriptor_sha256")
|
|
1049
|
+
@classmethod
|
|
1050
|
+
def _descriptor_hash_valid(cls, value: str) -> str:
|
|
1051
|
+
return _validate_sha256(value, "descriptor_sha256")
|
|
1052
|
+
|
|
1053
|
+
@model_validator(mode="after")
|
|
1054
|
+
def _has_approval_identity(self) -> ConnectorApprovalGrant:
|
|
1055
|
+
if self.approval_id is None and self.nonce is None:
|
|
1056
|
+
raise ValueError("connector approval grant requires approval_id or nonce")
|
|
1057
|
+
return self
|
|
1058
|
+
|
|
1059
|
+
|
|
1060
|
+
# Tool execution context
|
|
1061
|
+
# ---------------------------------------------------------------------------
|
|
1062
|
+
|
|
1063
|
+
|
|
1064
|
+
class ToolExecutionContext(BaseModel):
|
|
1065
|
+
"""Contextual information passed to ``ToolExecutor.execute()``.
|
|
1066
|
+
|
|
1067
|
+
Provides the execution environment — request identity, stage,
|
|
1068
|
+
run directory, capability envelope, timeout, and cancellation
|
|
1069
|
+
reference — as the second argument to ``ToolExecutor.execute()``.
|
|
1070
|
+
"""
|
|
1071
|
+
|
|
1072
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
1073
|
+
|
|
1074
|
+
request_id: str = Field(description="Unique request identifier")
|
|
1075
|
+
run_id: str = Field(description="Run this request belongs to")
|
|
1076
|
+
stage: StageIdentity = Field(description="Stage identity")
|
|
1077
|
+
run_directory: RunDirRef = Field(description="Run directory reference")
|
|
1078
|
+
capability_envelope: CapabilityEnvelope = Field(
|
|
1079
|
+
description="Capability grant envelope"
|
|
1080
|
+
)
|
|
1081
|
+
timeout: TimeoutRef = Field(description="Timeout reference")
|
|
1082
|
+
cancellation: CancellationRef = Field(description="Cancellation reference")
|
|
1083
|
+
deadline: Deadline = Field(description="Deadline specification")
|
|
1084
|
+
workspace_root: Path | None = Field(
|
|
1085
|
+
default=None, description="Trusted workspace root supplied by runtime"
|
|
1086
|
+
)
|
|
1087
|
+
artifact_root: Path | None = Field(
|
|
1088
|
+
default=None, description="Trusted artifact root supplied by runtime"
|
|
1089
|
+
)
|
|
1090
|
+
compiled_artifact_policy: CompiledArtifactPolicy | None = Field(
|
|
1091
|
+
default=None,
|
|
1092
|
+
description="Compiled artifact declarations supplied by the runtime",
|
|
1093
|
+
)
|
|
1094
|
+
input_artifacts: Tuple[ArtifactRef, ...] = Field(
|
|
1095
|
+
default_factory=tuple,
|
|
1096
|
+
description="Runtime-supplied request input artifact references",
|
|
1097
|
+
)
|
|
1098
|
+
work_item_id: str | None = Field(
|
|
1099
|
+
default=None, description="Runtime-supplied active work item identifier"
|
|
1100
|
+
)
|
|
1101
|
+
cancellation_requested: bool = Field(
|
|
1102
|
+
default=False, description="Trusted pre-entry cancellation state"
|
|
1103
|
+
)
|
|
1104
|
+
current_monotonic: float = Field(
|
|
1105
|
+
default=0.0,
|
|
1106
|
+
ge=0,
|
|
1107
|
+
description="Trusted monotonic timestamp used for pre-entry deadline checks",
|
|
1108
|
+
)
|
|
1109
|
+
connector_approval_grants: Tuple[ConnectorApprovalGrant, ...] = Field(
|
|
1110
|
+
default_factory=tuple,
|
|
1111
|
+
description="Runtime-only connector approval grants scoped to exact bindings",
|
|
1112
|
+
)
|
|
1113
|
+
|
|
1114
|
+
|
|
1115
|
+
# ---------------------------------------------------------------------------
|
|
1116
|
+
# Invocation-local selected output authority and admitted value
|
|
1117
|
+
# ---------------------------------------------------------------------------
|
|
1118
|
+
|
|
1119
|
+
|
|
1120
|
+
class SelectedOutputRequirement(BaseModel):
|
|
1121
|
+
"""Immutable required/optional authority for one selected JSON output.
|
|
1122
|
+
|
|
1123
|
+
``json_schema`` may be supplied as a strict JSON string/bytes value or as a
|
|
1124
|
+
Python mapping. It is normalized to the documented closed subset and its
|
|
1125
|
+
canonical SHA-256 digest is pinned into the serialized request contract.
|
|
1126
|
+
"""
|
|
1127
|
+
|
|
1128
|
+
model_config = ConfigDict(
|
|
1129
|
+
extra="forbid",
|
|
1130
|
+
frozen=True,
|
|
1131
|
+
hide_input_in_errors=True,
|
|
1132
|
+
revalidate_instances="always",
|
|
1133
|
+
)
|
|
1134
|
+
|
|
1135
|
+
required: StrictBool = Field(
|
|
1136
|
+
description="Whether the invocation must admit a present selected output"
|
|
1137
|
+
)
|
|
1138
|
+
json_schema: JsonObject = Field(description="Admitted closed selected JSON schema")
|
|
1139
|
+
schema_sha256: str = Field(
|
|
1140
|
+
default="",
|
|
1141
|
+
description="SHA-256 digest of the canonical admitted selected schema",
|
|
1142
|
+
)
|
|
1143
|
+
|
|
1144
|
+
@classmethod
|
|
1145
|
+
def model_validate_json(
|
|
1146
|
+
cls,
|
|
1147
|
+
json_data: str | bytes | bytearray,
|
|
1148
|
+
*,
|
|
1149
|
+
strict: bool | None = None,
|
|
1150
|
+
extra: Any | None = None,
|
|
1151
|
+
context: Any | None = None,
|
|
1152
|
+
by_alias: bool | None = None,
|
|
1153
|
+
by_name: bool | None = None,
|
|
1154
|
+
) -> Self:
|
|
1155
|
+
"""Validate raw requirement JSON without losing duplicate keys first."""
|
|
1156
|
+
parsed = _parse_json_objects_preserving_pairs(json_data)
|
|
1157
|
+
if parsed is not None:
|
|
1158
|
+
_validate_raw_json_strictness(parsed, field_name="selected output JSON")
|
|
1159
|
+
return super().model_validate_json(
|
|
1160
|
+
json_data,
|
|
1161
|
+
strict=strict,
|
|
1162
|
+
extra=extra,
|
|
1163
|
+
context=context,
|
|
1164
|
+
by_alias=by_alias,
|
|
1165
|
+
by_name=by_name,
|
|
1166
|
+
)
|
|
1167
|
+
|
|
1168
|
+
@model_validator(mode="before")
|
|
1169
|
+
@classmethod
|
|
1170
|
+
def _admit_and_pin_schema(cls, value: Any) -> Any:
|
|
1171
|
+
if isinstance(value, cls):
|
|
1172
|
+
return value
|
|
1173
|
+
if not isinstance(value, Mapping) or "json_schema" not in value:
|
|
1174
|
+
return value
|
|
1175
|
+
canonical = canonical_selected_output_schema_bytes(value["json_schema"])
|
|
1176
|
+
admitted = json.loads(canonical)
|
|
1177
|
+
digest = hashlib.sha256(canonical).hexdigest()
|
|
1178
|
+
supplied_digest = value.get("schema_sha256")
|
|
1179
|
+
if supplied_digest not in (None, "", digest):
|
|
1180
|
+
raise ValueError("schema_sha256 does not match canonical selected schema")
|
|
1181
|
+
normalized = dict(value)
|
|
1182
|
+
normalized["json_schema"] = admitted
|
|
1183
|
+
normalized["schema_sha256"] = digest
|
|
1184
|
+
return normalized
|
|
1185
|
+
|
|
1186
|
+
@field_validator("schema_sha256")
|
|
1187
|
+
@classmethod
|
|
1188
|
+
def _schema_digest_valid(cls, value: str) -> str:
|
|
1189
|
+
return _validate_sha256(value, "schema_sha256")
|
|
1190
|
+
|
|
1191
|
+
@model_validator(mode="after")
|
|
1192
|
+
def _schema_digest_matches(self) -> SelectedOutputRequirement:
|
|
1193
|
+
if self.schema_sha256 != selected_output_schema_sha256(self.json_schema):
|
|
1194
|
+
raise ValueError("schema_sha256 does not match canonical selected schema")
|
|
1195
|
+
object.__setattr__(
|
|
1196
|
+
self,
|
|
1197
|
+
"json_schema",
|
|
1198
|
+
_freeze_selected_output_json(self.json_schema),
|
|
1199
|
+
)
|
|
1200
|
+
return self
|
|
1201
|
+
|
|
1202
|
+
@property
|
|
1203
|
+
def canonical_schema_bytes(self) -> bytes:
|
|
1204
|
+
"""Return the admitted schema as canonical UTF-8 JSON bytes."""
|
|
1205
|
+
return canonical_selected_output_schema_bytes(self.json_schema)
|
|
1206
|
+
|
|
1207
|
+
|
|
1208
|
+
class TerminalSelectedOutputRequirement(BaseModel):
|
|
1209
|
+
"""Immutable selected-output authority for one exact terminal result."""
|
|
1210
|
+
|
|
1211
|
+
model_config = ConfigDict(
|
|
1212
|
+
extra="forbid",
|
|
1213
|
+
frozen=True,
|
|
1214
|
+
revalidate_instances="always",
|
|
1215
|
+
)
|
|
1216
|
+
|
|
1217
|
+
terminal_result: str
|
|
1218
|
+
selected_output: SelectedOutputRequirement
|
|
1219
|
+
|
|
1220
|
+
@field_validator("terminal_result")
|
|
1221
|
+
@classmethod
|
|
1222
|
+
def _terminal_result_is_nonblank(cls, value: str) -> str:
|
|
1223
|
+
return _nonblank(value, "terminal_result")
|
|
1224
|
+
|
|
1225
|
+
|
|
1226
|
+
def _selected_output_requirements_by_terminal_result(
|
|
1227
|
+
requirements: tuple[TerminalSelectedOutputRequirement, ...],
|
|
1228
|
+
) -> dict[str, SelectedOutputRequirement]:
|
|
1229
|
+
"""Return the one canonical terminal-result lookup for selected output."""
|
|
1230
|
+
|
|
1231
|
+
if len(requirements) > 64:
|
|
1232
|
+
raise ValueError("selected output requirements exceed the 64-record ceiling")
|
|
1233
|
+
lookup: dict[str, SelectedOutputRequirement] = {}
|
|
1234
|
+
for item in requirements:
|
|
1235
|
+
terminal_result = _nonblank(item.terminal_result, "terminal_result")
|
|
1236
|
+
if terminal_result in lookup:
|
|
1237
|
+
raise ValueError("selected output terminal_result values must be unique")
|
|
1238
|
+
lookup[terminal_result] = item.selected_output
|
|
1239
|
+
return dict(sorted(lookup.items(), key=lambda item: item[0].encode("utf-8")))
|
|
1240
|
+
|
|
1241
|
+
|
|
1242
|
+
class SelectedOutputAbsent(BaseModel):
|
|
1243
|
+
"""Explicit absence for an admitted optional selected-output authority."""
|
|
1244
|
+
|
|
1245
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
1246
|
+
|
|
1247
|
+
present: Literal[False] = False
|
|
1248
|
+
|
|
1249
|
+
|
|
1250
|
+
class SelectedOutputPresent(BaseModel):
|
|
1251
|
+
"""A present, globally bounded JSON value; ``value=None`` is JSON null."""
|
|
1252
|
+
|
|
1253
|
+
model_config = ConfigDict(
|
|
1254
|
+
extra="forbid",
|
|
1255
|
+
frozen=True,
|
|
1256
|
+
hide_input_in_errors=True,
|
|
1257
|
+
revalidate_instances="always",
|
|
1258
|
+
)
|
|
1259
|
+
|
|
1260
|
+
present: Literal[True] = True
|
|
1261
|
+
value: JsonValue = Field(description="Admitted JSON value, including JSON null")
|
|
1262
|
+
|
|
1263
|
+
@field_validator("value", mode="before")
|
|
1264
|
+
@classmethod
|
|
1265
|
+
def _value_is_bounded_json(cls, value: Any) -> JsonValue:
|
|
1266
|
+
normalized = _normalize_selected_output_payload(value, depth=1)
|
|
1267
|
+
canonical_selected_output_payload_bytes(normalized)
|
|
1268
|
+
return _freeze_selected_output_json(normalized)
|
|
1269
|
+
|
|
1270
|
+
|
|
1271
|
+
SelectedOutput: TypeAlias = Annotated[
|
|
1272
|
+
SelectedOutputAbsent | SelectedOutputPresent,
|
|
1273
|
+
Field(discriminator="present"),
|
|
1274
|
+
]
|
|
1275
|
+
|
|
1276
|
+
|
|
1277
|
+
def admit_selected_output(
|
|
1278
|
+
requirement: SelectedOutputRequirement,
|
|
1279
|
+
*,
|
|
1280
|
+
present: bool,
|
|
1281
|
+
value: Any = None,
|
|
1282
|
+
) -> SelectedOutput:
|
|
1283
|
+
"""Mechanically admit one invocation-local selected-output candidate.
|
|
1284
|
+
|
|
1285
|
+
Absence is legal only for optional authorities. Present values first pass
|
|
1286
|
+
the global payload ceilings owned by :class:`SelectedOutputPresent`, then
|
|
1287
|
+
the exact closed schema carried by ``requirement``. No prose, artifact, or
|
|
1288
|
+
workspace fallback participates in admission.
|
|
1289
|
+
"""
|
|
1290
|
+
|
|
1291
|
+
if not present:
|
|
1292
|
+
if requirement.required:
|
|
1293
|
+
raise ValueError("required selected output candidate is missing")
|
|
1294
|
+
return SelectedOutputAbsent()
|
|
1295
|
+
|
|
1296
|
+
candidate = SelectedOutputPresent(value=value)
|
|
1297
|
+
error = _selected_output_schema_error(
|
|
1298
|
+
candidate.value,
|
|
1299
|
+
requirement.json_schema,
|
|
1300
|
+
path="$",
|
|
1301
|
+
)
|
|
1302
|
+
if error is not None:
|
|
1303
|
+
raise ValueError(f"selected output candidate failed schema validation: {error}")
|
|
1304
|
+
return candidate
|
|
1305
|
+
|
|
1306
|
+
|
|
1307
|
+
def _selected_output_schema_error(
|
|
1308
|
+
value: Any,
|
|
1309
|
+
schema: Mapping[str, Any],
|
|
1310
|
+
*,
|
|
1311
|
+
path: str,
|
|
1312
|
+
) -> str | None:
|
|
1313
|
+
"""Return the first deterministic mismatch against the admitted subset."""
|
|
1314
|
+
|
|
1315
|
+
if "const" in schema:
|
|
1316
|
+
if _canonical_selected_output_scalar_bytes(value) != (
|
|
1317
|
+
_canonical_selected_output_scalar_bytes(schema["const"])
|
|
1318
|
+
):
|
|
1319
|
+
return f"{path} must equal const value"
|
|
1320
|
+
elif "enum" in schema:
|
|
1321
|
+
candidate = _canonical_selected_output_scalar_bytes(value)
|
|
1322
|
+
if all(
|
|
1323
|
+
candidate != _canonical_selected_output_scalar_bytes(item)
|
|
1324
|
+
for item in schema["enum"]
|
|
1325
|
+
):
|
|
1326
|
+
return f"{path} must equal an enum value"
|
|
1327
|
+
|
|
1328
|
+
schema_type = schema.get("type")
|
|
1329
|
+
if schema_type is None:
|
|
1330
|
+
return None
|
|
1331
|
+
if schema_type == "object":
|
|
1332
|
+
if not isinstance(value, Mapping):
|
|
1333
|
+
return f"{path} must be object"
|
|
1334
|
+
properties = schema["properties"]
|
|
1335
|
+
for key in schema["required"]:
|
|
1336
|
+
if key not in value:
|
|
1337
|
+
return f"{path}.{key} is required"
|
|
1338
|
+
extra = sorted(set(value) - set(properties))
|
|
1339
|
+
if extra:
|
|
1340
|
+
return f"{path}.{extra[0]} is not allowed"
|
|
1341
|
+
for key, item in value.items():
|
|
1342
|
+
error = _selected_output_schema_error(
|
|
1343
|
+
item,
|
|
1344
|
+
properties[key],
|
|
1345
|
+
path=f"{path}.{key}",
|
|
1346
|
+
)
|
|
1347
|
+
if error is not None:
|
|
1348
|
+
return error
|
|
1349
|
+
return None
|
|
1350
|
+
if schema_type == "array":
|
|
1351
|
+
if not isinstance(value, list):
|
|
1352
|
+
return f"{path} must be array"
|
|
1353
|
+
minimum = schema.get("minItems", 0)
|
|
1354
|
+
maximum = schema.get("maxItems", MAX_SELECTED_OUTPUT_ARRAY_ITEMS)
|
|
1355
|
+
if len(value) < minimum:
|
|
1356
|
+
return f"{path} has fewer than {minimum} items"
|
|
1357
|
+
if len(value) > maximum:
|
|
1358
|
+
return f"{path} has more than {maximum} items"
|
|
1359
|
+
for index, item in enumerate(value):
|
|
1360
|
+
error = _selected_output_schema_error(
|
|
1361
|
+
item,
|
|
1362
|
+
schema["items"],
|
|
1363
|
+
path=f"{path}[{index}]",
|
|
1364
|
+
)
|
|
1365
|
+
if error is not None:
|
|
1366
|
+
return error
|
|
1367
|
+
return None
|
|
1368
|
+
if schema_type == "string":
|
|
1369
|
+
if not isinstance(value, str):
|
|
1370
|
+
return f"{path} must be string"
|
|
1371
|
+
minimum = schema.get("minLength", 0)
|
|
1372
|
+
maximum = schema.get("maxLength", MAX_SELECTED_OUTPUT_STRING_LENGTH)
|
|
1373
|
+
if len(value) < minimum:
|
|
1374
|
+
return f"{path} is shorter than {minimum} characters"
|
|
1375
|
+
if len(value) > maximum:
|
|
1376
|
+
return f"{path} is longer than {maximum} characters"
|
|
1377
|
+
return None
|
|
1378
|
+
if schema_type == "integer":
|
|
1379
|
+
return (
|
|
1380
|
+
None
|
|
1381
|
+
if isinstance(value, int) and not isinstance(value, bool)
|
|
1382
|
+
else f"{path} must be integer"
|
|
1383
|
+
)
|
|
1384
|
+
if schema_type == "number":
|
|
1385
|
+
return (
|
|
1386
|
+
None
|
|
1387
|
+
if isinstance(value, int | float)
|
|
1388
|
+
and not isinstance(value, bool)
|
|
1389
|
+
and (not isinstance(value, float) or math.isfinite(value))
|
|
1390
|
+
else f"{path} must be finite number"
|
|
1391
|
+
)
|
|
1392
|
+
if schema_type == "boolean":
|
|
1393
|
+
return None if isinstance(value, bool) else f"{path} must be boolean"
|
|
1394
|
+
if schema_type == "null":
|
|
1395
|
+
return None if value is None else f"{path} must be null"
|
|
1396
|
+
raise AssertionError(f"unreachable selected output schema type: {schema_type}")
|
|
1397
|
+
|
|
1398
|
+
|
|
1399
|
+
# ---------------------------------------------------------------------------
|
|
1400
|
+
# Harness execution request (primary executable boundary)
|
|
1401
|
+
# ---------------------------------------------------------------------------
|
|
1402
|
+
|
|
1403
|
+
|
|
1404
|
+
class HarnessExecutionRequest(BaseModel):
|
|
1405
|
+
"""Immutable executable boundary for harness execution.
|
|
1406
|
+
|
|
1407
|
+
This is the primary input contract for ``HarnessRuntime.execute()``.
|
|
1408
|
+
``stage`` is the provider-local identity admitted by the selected compiled
|
|
1409
|
+
Millforge harness; it does not carry a caller workflow plane, node, route,
|
|
1410
|
+
dispatch identity, or authority. ``request_id`` and ``run_id`` are opaque
|
|
1411
|
+
caller-owned correlation values that Millforge validates and echoes without
|
|
1412
|
+
interpreting them as workflow or terminal authority. Run IDs are checked
|
|
1413
|
+
for consistency and collection-level duplicates are rejected at
|
|
1414
|
+
construction time.
|
|
1415
|
+
"""
|
|
1416
|
+
|
|
1417
|
+
model_config = ConfigDict(
|
|
1418
|
+
extra="forbid",
|
|
1419
|
+
frozen=True,
|
|
1420
|
+
hide_input_in_errors=True,
|
|
1421
|
+
)
|
|
1422
|
+
|
|
1423
|
+
request_id: str = Field(description="Opaque caller request correlation value")
|
|
1424
|
+
run_id: str = Field(description="Opaque caller run correlation value")
|
|
1425
|
+
work_item_id: str = Field(description="Active work item identifier")
|
|
1426
|
+
task: HarnessTaskInput = Field(description="Exact bounded task instruction")
|
|
1427
|
+
stage: StageIdentity = Field(
|
|
1428
|
+
description="Provider-local identity of the admitted compiled harness stage"
|
|
1429
|
+
)
|
|
1430
|
+
compiled_harness: CompiledHarnessRef = Field(
|
|
1431
|
+
description="Reference to the compiled harness"
|
|
1432
|
+
)
|
|
1433
|
+
capability_envelope: CapabilityEnvelope = Field(
|
|
1434
|
+
description="Capability grant envelope"
|
|
1435
|
+
)
|
|
1436
|
+
input_artifacts: Tuple[ArtifactRef, ...] = Field(
|
|
1437
|
+
description="Input artifact references"
|
|
1438
|
+
)
|
|
1439
|
+
run_directory: RunDirRef = Field(description="Run directory reference")
|
|
1440
|
+
timeout: TimeoutRef = Field(description="Timeout reference")
|
|
1441
|
+
cancellation: CancellationRef = Field(description="Cancellation reference")
|
|
1442
|
+
secret_refs: Tuple[SecretRef, ...] = Field(
|
|
1443
|
+
description="Secret references (handles only, never values)"
|
|
1444
|
+
)
|
|
1445
|
+
model_profile: ModelProfileRef = Field(description="Model profile reference")
|
|
1446
|
+
selected_output_requirements: tuple[TerminalSelectedOutputRequirement, ...] = Field(
|
|
1447
|
+
default_factory=tuple,
|
|
1448
|
+
max_length=64,
|
|
1449
|
+
description="Terminal-result-scoped selected JSON output requirements",
|
|
1450
|
+
)
|
|
1451
|
+
|
|
1452
|
+
@field_validator("selected_output_requirements")
|
|
1453
|
+
@classmethod
|
|
1454
|
+
def _selected_output_requirements_are_canonical(
|
|
1455
|
+
cls,
|
|
1456
|
+
value: tuple[TerminalSelectedOutputRequirement, ...],
|
|
1457
|
+
) -> tuple[TerminalSelectedOutputRequirement, ...]:
|
|
1458
|
+
canonical_lookup = _selected_output_requirements_by_terminal_result(value)
|
|
1459
|
+
records = {item.terminal_result: item for item in value}
|
|
1460
|
+
return tuple(records[terminal_result] for terminal_result in canonical_lookup)
|
|
1461
|
+
|
|
1462
|
+
@classmethod
|
|
1463
|
+
def model_validate_json(
|
|
1464
|
+
cls,
|
|
1465
|
+
json_data: str | bytes | bytearray,
|
|
1466
|
+
*,
|
|
1467
|
+
strict: bool | None = None,
|
|
1468
|
+
extra: Any | None = None,
|
|
1469
|
+
context: Any | None = None,
|
|
1470
|
+
by_alias: bool | None = None,
|
|
1471
|
+
by_name: bool | None = None,
|
|
1472
|
+
) -> Self:
|
|
1473
|
+
"""Validate raw request JSON before Pydantic can collapse key pairs."""
|
|
1474
|
+
_validate_harness_request_raw_json(json_data)
|
|
1475
|
+
return super().model_validate_json(
|
|
1476
|
+
json_data,
|
|
1477
|
+
strict=strict,
|
|
1478
|
+
extra=extra,
|
|
1479
|
+
context=context,
|
|
1480
|
+
by_alias=by_alias,
|
|
1481
|
+
by_name=by_name,
|
|
1482
|
+
)
|
|
1483
|
+
|
|
1484
|
+
# ------------------------------------------------------------------
|
|
1485
|
+
# Cross-field validators
|
|
1486
|
+
# ------------------------------------------------------------------
|
|
1487
|
+
|
|
1488
|
+
@model_validator(mode="after")
|
|
1489
|
+
def _check_run_id_consistency(self) -> HarnessExecutionRequest:
|
|
1490
|
+
"""HarnessExecutionRequest.run_id must match RunDirRef.run_id."""
|
|
1491
|
+
if self.run_id != self.run_directory.run_id:
|
|
1492
|
+
raise ValueError(
|
|
1493
|
+
f"HarnessExecutionRequest.run_id ({self.run_id!r}) must match "
|
|
1494
|
+
f"RunDirRef.run_id ({self.run_directory.run_id!r})"
|
|
1495
|
+
)
|
|
1496
|
+
return self
|
|
1497
|
+
|
|
1498
|
+
@model_validator(mode="after")
|
|
1499
|
+
def _check_sha256_digest(self) -> HarnessExecutionRequest:
|
|
1500
|
+
"""When algorithm is sha256, digest must be exactly 64 lowercase hex chars."""
|
|
1501
|
+
h = self.compiled_harness.expected_hash
|
|
1502
|
+
if h.algorithm == "sha256":
|
|
1503
|
+
if not re.fullmatch(r"[0-9a-f]{64}", h.digest):
|
|
1504
|
+
raise ValueError(
|
|
1505
|
+
f"CompiledHarnessHash digest must be exactly 64 lowercase hex "
|
|
1506
|
+
f"characters when algorithm is 'sha256', got {h.digest!r}"
|
|
1507
|
+
)
|
|
1508
|
+
return self
|
|
1509
|
+
|
|
1510
|
+
@model_validator(mode="after")
|
|
1511
|
+
def _check_duplicate_grant_capabilities(self) -> HarnessExecutionRequest:
|
|
1512
|
+
"""Reject duplicate capability identifiers in the grants tuple."""
|
|
1513
|
+
seen: set[str] = set()
|
|
1514
|
+
for grant in self.capability_envelope.grants:
|
|
1515
|
+
if grant.capability_id in seen:
|
|
1516
|
+
raise ValueError(
|
|
1517
|
+
f"Duplicate capability_id {grant.capability_id!r} in CapabilityEnvelope"
|
|
1518
|
+
)
|
|
1519
|
+
seen.add(grant.capability_id)
|
|
1520
|
+
return self
|
|
1521
|
+
|
|
1522
|
+
@model_validator(mode="after")
|
|
1523
|
+
def _check_duplicate_artifact_ids(self) -> HarnessExecutionRequest:
|
|
1524
|
+
"""Reject duplicate artifact_id values in input_artifacts."""
|
|
1525
|
+
seen: set[str] = set()
|
|
1526
|
+
for artifact in self.input_artifacts:
|
|
1527
|
+
if artifact.artifact_id in seen:
|
|
1528
|
+
raise ValueError(
|
|
1529
|
+
f"Duplicate artifact_id {artifact.artifact_id!r} in input_artifacts"
|
|
1530
|
+
)
|
|
1531
|
+
seen.add(artifact.artifact_id)
|
|
1532
|
+
return self
|
|
1533
|
+
|
|
1534
|
+
@model_validator(mode="after")
|
|
1535
|
+
def _check_duplicate_secret_ids(self) -> HarnessExecutionRequest:
|
|
1536
|
+
"""Reject duplicate secret_id values in secret_refs."""
|
|
1537
|
+
seen: set[str] = set()
|
|
1538
|
+
for secret in self.secret_refs:
|
|
1539
|
+
if secret.secret_id in seen:
|
|
1540
|
+
raise ValueError(
|
|
1541
|
+
f"Duplicate secret_id {secret.secret_id!r} in secret_refs"
|
|
1542
|
+
)
|
|
1543
|
+
seen.add(secret.secret_id)
|
|
1544
|
+
return self
|
|
1545
|
+
|
|
1546
|
+
@model_validator(mode="after")
|
|
1547
|
+
def _check_duplicate_secret_env_vars(self) -> HarnessExecutionRequest:
|
|
1548
|
+
"""Reject duplicate env_var values in secret_refs."""
|
|
1549
|
+
seen: set[str] = set()
|
|
1550
|
+
for secret in self.secret_refs:
|
|
1551
|
+
if secret.env_var in seen:
|
|
1552
|
+
raise ValueError(f"Duplicate env_var {secret.env_var!r} in secret_refs")
|
|
1553
|
+
seen.add(secret.env_var)
|
|
1554
|
+
return self
|
|
1555
|
+
|
|
1556
|
+
|
|
1557
|
+
# ---------------------------------------------------------------------------
|
|
1558
|
+
# Model, tool, and bridge-owned request/response models
|
|
1559
|
+
# ---------------------------------------------------------------------------
|
|
1560
|
+
|
|
1561
|
+
|
|
1562
|
+
class ModelCapabilityRequirements(BaseModel):
|
|
1563
|
+
"""Exact model capabilities required by the 02C-02D Forge bridge."""
|
|
1564
|
+
|
|
1565
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
1566
|
+
|
|
1567
|
+
tool_calls: Literal[True] = True
|
|
1568
|
+
parallel_tool_calls: Literal[False] = False
|
|
1569
|
+
structured_output: Literal[False] = False
|
|
1570
|
+
reasoning_controls: Literal[False] = False
|
|
1571
|
+
usage_reporting: Literal[False] = False
|
|
1572
|
+
system_messages: Literal[True] = True
|
|
1573
|
+
tool_result_messages: Literal[True] = True
|
|
1574
|
+
|
|
1575
|
+
|
|
1576
|
+
class SamplingRequest(BaseModel):
|
|
1577
|
+
"""Canonical owned sampling controls for model calls."""
|
|
1578
|
+
|
|
1579
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
1580
|
+
|
|
1581
|
+
temperature: float | None = Field(default=None, ge=0, le=2)
|
|
1582
|
+
top_p: float | None = Field(default=None, ge=0, le=1)
|
|
1583
|
+
presence_penalty: float | None = Field(default=None, ge=-2, le=2)
|
|
1584
|
+
frequency_penalty: float | None = Field(default=None, ge=-2, le=2)
|
|
1585
|
+
seed: int | None = None
|
|
1586
|
+
stop: tuple[str, ...] | None = None
|
|
1587
|
+
reasoning_mode: str | None = None
|
|
1588
|
+
reasoning_effort: str | None = None
|
|
1589
|
+
|
|
1590
|
+
@field_validator("stop")
|
|
1591
|
+
@classmethod
|
|
1592
|
+
def _stop_values_nonblank(
|
|
1593
|
+
cls, value: tuple[str, ...] | None
|
|
1594
|
+
) -> tuple[str, ...] | None:
|
|
1595
|
+
if value is None:
|
|
1596
|
+
return None
|
|
1597
|
+
for item in value:
|
|
1598
|
+
_nonblank(item, "stop")
|
|
1599
|
+
return value
|
|
1600
|
+
|
|
1601
|
+
@field_validator("reasoning_mode", "reasoning_effort")
|
|
1602
|
+
@classmethod
|
|
1603
|
+
def _optional_strings_nonblank(cls, value: str | None, info: Any) -> str | None:
|
|
1604
|
+
return None if value is None else _nonblank(value, info.field_name)
|
|
1605
|
+
|
|
1606
|
+
|
|
1607
|
+
class SanitizedMetadata(BaseModel):
|
|
1608
|
+
"""Bounded metadata that is safe to persist across the public boundary."""
|
|
1609
|
+
|
|
1610
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
1611
|
+
|
|
1612
|
+
values: Dict[str, SanitizedMetadataValue] = Field(default_factory=dict)
|
|
1613
|
+
|
|
1614
|
+
@field_validator("values")
|
|
1615
|
+
@classmethod
|
|
1616
|
+
def _values_bounded(
|
|
1617
|
+
cls, value: dict[str, SanitizedMetadataValue]
|
|
1618
|
+
) -> dict[str, SanitizedMetadataValue]:
|
|
1619
|
+
if len(value) > _SANITIZED_METADATA_MAX_ITEMS:
|
|
1620
|
+
raise ValueError("sanitized metadata contains too many items")
|
|
1621
|
+
total_bytes = 0
|
|
1622
|
+
for key, item in value.items():
|
|
1623
|
+
_nonblank(key, "sanitized metadata key")
|
|
1624
|
+
if len(key) > _SANITIZED_METADATA_KEY_MAX_LENGTH:
|
|
1625
|
+
raise ValueError("sanitized metadata key is too long")
|
|
1626
|
+
if isinstance(item, str):
|
|
1627
|
+
if len(item) > _SANITIZED_METADATA_STRING_MAX_LENGTH:
|
|
1628
|
+
raise ValueError("sanitized metadata string value is too long")
|
|
1629
|
+
total_bytes += len(item.encode("utf-8"))
|
|
1630
|
+
else:
|
|
1631
|
+
total_bytes += len(str(item).encode("utf-8"))
|
|
1632
|
+
if total_bytes > _SANITIZED_METADATA_BYTES_MAX_LENGTH:
|
|
1633
|
+
raise ValueError("sanitized metadata payload is too large")
|
|
1634
|
+
return value
|
|
1635
|
+
|
|
1636
|
+
|
|
1637
|
+
class RedactionPolicy(BaseModel):
|
|
1638
|
+
"""Single bounded redaction policy for public summaries and diagnostics."""
|
|
1639
|
+
|
|
1640
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
1641
|
+
|
|
1642
|
+
max_depth: int = Field(
|
|
1643
|
+
default=_REDACTION_DEFAULT_DEPTH, ge=1, le=_REDACTION_MAX_DEPTH
|
|
1644
|
+
)
|
|
1645
|
+
max_collection_items: int = Field(
|
|
1646
|
+
default=_REDACTION_DEFAULT_COLLECTION_ITEMS,
|
|
1647
|
+
ge=1,
|
|
1648
|
+
le=_REDACTION_MAX_COLLECTION_ITEMS,
|
|
1649
|
+
)
|
|
1650
|
+
max_string_length: int = Field(
|
|
1651
|
+
default=_REDACTION_DEFAULT_STRING_LENGTH,
|
|
1652
|
+
ge=1,
|
|
1653
|
+
le=_REDACTION_MAX_STRING_LENGTH,
|
|
1654
|
+
)
|
|
1655
|
+
max_total_bytes: int = Field(
|
|
1656
|
+
default=_REDACTION_DEFAULT_TOTAL_BYTES,
|
|
1657
|
+
ge=1,
|
|
1658
|
+
le=_REDACTION_MAX_TOTAL_BYTES,
|
|
1659
|
+
)
|
|
1660
|
+
replacement: str = "**redacted**"
|
|
1661
|
+
sensitive_field_markers: Tuple[str, ...] = (
|
|
1662
|
+
"authorization",
|
|
1663
|
+
"api-key",
|
|
1664
|
+
"apikey",
|
|
1665
|
+
"token",
|
|
1666
|
+
"secret",
|
|
1667
|
+
"password",
|
|
1668
|
+
"credential",
|
|
1669
|
+
"cookie",
|
|
1670
|
+
"set-cookie",
|
|
1671
|
+
)
|
|
1672
|
+
|
|
1673
|
+
@field_validator("replacement")
|
|
1674
|
+
@classmethod
|
|
1675
|
+
def _replacement_nonblank(cls, value: str) -> str:
|
|
1676
|
+
return _nonblank(value, "replacement")
|
|
1677
|
+
|
|
1678
|
+
@field_validator("sensitive_field_markers")
|
|
1679
|
+
@classmethod
|
|
1680
|
+
def _markers_valid(cls, value: tuple[str, ...]) -> tuple[str, ...]:
|
|
1681
|
+
normalized = tuple(
|
|
1682
|
+
_nonblank(item, "sensitive marker").lower() for item in value
|
|
1683
|
+
)
|
|
1684
|
+
_unique(normalized, "sensitive marker")
|
|
1685
|
+
return normalized
|
|
1686
|
+
|
|
1687
|
+
|
|
1688
|
+
class _RedactionBudget:
|
|
1689
|
+
def __init__(self, limit: int) -> None:
|
|
1690
|
+
self._limit = limit
|
|
1691
|
+
self._used = 0
|
|
1692
|
+
|
|
1693
|
+
def text(self, value: str) -> str:
|
|
1694
|
+
remaining = self._limit - self._used
|
|
1695
|
+
if remaining <= 0:
|
|
1696
|
+
return "[truncated]"
|
|
1697
|
+
encoded = value.encode("utf-8")
|
|
1698
|
+
if len(encoded) <= remaining:
|
|
1699
|
+
self._used += len(encoded)
|
|
1700
|
+
return value
|
|
1701
|
+
truncated = encoded[:remaining].decode("utf-8", errors="ignore")
|
|
1702
|
+
self._used = self._limit
|
|
1703
|
+
return f"{truncated}[truncated]"
|
|
1704
|
+
|
|
1705
|
+
|
|
1706
|
+
def _redact_url(value: str, policy: RedactionPolicy) -> str:
|
|
1707
|
+
try:
|
|
1708
|
+
split = urlsplit(value)
|
|
1709
|
+
except ValueError:
|
|
1710
|
+
return _redact_secret_patterns(value, policy)
|
|
1711
|
+
host = split.hostname or ""
|
|
1712
|
+
if split.port is not None:
|
|
1713
|
+
host = f"{host}:{split.port}"
|
|
1714
|
+
query_parts = parse_qsl(split.query, keep_blank_values=True)
|
|
1715
|
+
query = "&".join(
|
|
1716
|
+
f"{key}={policy.replacement}"
|
|
1717
|
+
if _is_sensitive_field_name(key, policy)
|
|
1718
|
+
else f"{key}={value}"
|
|
1719
|
+
for key, value in query_parts
|
|
1720
|
+
)
|
|
1721
|
+
return urlunsplit(
|
|
1722
|
+
(
|
|
1723
|
+
split.scheme,
|
|
1724
|
+
host,
|
|
1725
|
+
split.path,
|
|
1726
|
+
query,
|
|
1727
|
+
policy.replacement if split.fragment else "",
|
|
1728
|
+
)
|
|
1729
|
+
)
|
|
1730
|
+
|
|
1731
|
+
|
|
1732
|
+
def _redact_secret_patterns(text: str, policy: RedactionPolicy) -> str:
|
|
1733
|
+
text = _SECRET_PATTERNS[0].sub(
|
|
1734
|
+
lambda match: (
|
|
1735
|
+
match.group(0).split("=", 1)[0] + "=" + policy.replacement
|
|
1736
|
+
if match.group(1) != policy.replacement
|
|
1737
|
+
else match.group(0)
|
|
1738
|
+
),
|
|
1739
|
+
text,
|
|
1740
|
+
)
|
|
1741
|
+
text = _SECRET_PATTERNS[1].sub(
|
|
1742
|
+
lambda match: (
|
|
1743
|
+
match.group(0)
|
|
1744
|
+
if match.group(2) == policy.replacement
|
|
1745
|
+
else f"{match.group(1)}{policy.replacement}"
|
|
1746
|
+
),
|
|
1747
|
+
text,
|
|
1748
|
+
)
|
|
1749
|
+
for pattern in _SECRET_PATTERNS[2:]:
|
|
1750
|
+
text = pattern.sub(lambda match: f"{match.group(1)}{policy.replacement}", text)
|
|
1751
|
+
return text
|
|
1752
|
+
|
|
1753
|
+
|
|
1754
|
+
def redact_diagnostic_text(
|
|
1755
|
+
value: str,
|
|
1756
|
+
*,
|
|
1757
|
+
policy: RedactionPolicy | None = None,
|
|
1758
|
+
secret_values: tuple[str, ...] = (),
|
|
1759
|
+
) -> str:
|
|
1760
|
+
"""Apply the shared deterministic redaction policy to text."""
|
|
1761
|
+
active_policy = policy or RedactionPolicy()
|
|
1762
|
+
text = value
|
|
1763
|
+
for secret in secret_values:
|
|
1764
|
+
if secret:
|
|
1765
|
+
text = text.replace(secret, active_policy.replacement)
|
|
1766
|
+
text = _URL_PATTERN.sub(
|
|
1767
|
+
lambda match: _redact_url(match.group(0), active_policy), text
|
|
1768
|
+
)
|
|
1769
|
+
text = _redact_secret_patterns(text, active_policy)
|
|
1770
|
+
if len(text) > active_policy.max_string_length:
|
|
1771
|
+
text = f"{text[: active_policy.max_string_length]}[truncated]"
|
|
1772
|
+
return text
|
|
1773
|
+
|
|
1774
|
+
|
|
1775
|
+
def redact_diagnostic_value(
|
|
1776
|
+
value: object,
|
|
1777
|
+
*,
|
|
1778
|
+
policy: RedactionPolicy | None = None,
|
|
1779
|
+
secret_values: tuple[str, ...] = (),
|
|
1780
|
+
) -> JsonValue:
|
|
1781
|
+
"""Return a bounded JSON-safe diagnostic value without arbitrary repr calls."""
|
|
1782
|
+
active_policy = policy or RedactionPolicy()
|
|
1783
|
+
budget = _RedactionBudget(active_policy.max_total_bytes)
|
|
1784
|
+
return _redact_value(
|
|
1785
|
+
value,
|
|
1786
|
+
policy=active_policy,
|
|
1787
|
+
secret_values=secret_values,
|
|
1788
|
+
budget=budget,
|
|
1789
|
+
depth=0,
|
|
1790
|
+
seen=set(),
|
|
1791
|
+
sensitive_key=False,
|
|
1792
|
+
)
|
|
1793
|
+
|
|
1794
|
+
|
|
1795
|
+
def redact_diagnostic_mapping(
|
|
1796
|
+
values: Mapping[str, object],
|
|
1797
|
+
*,
|
|
1798
|
+
policy: RedactionPolicy | None = None,
|
|
1799
|
+
secret_values: tuple[str, ...] = (),
|
|
1800
|
+
) -> dict[str, JsonValue]:
|
|
1801
|
+
"""Redact a diagnostic mapping using one bounded recursive policy."""
|
|
1802
|
+
redacted = redact_diagnostic_value(
|
|
1803
|
+
values,
|
|
1804
|
+
policy=policy,
|
|
1805
|
+
secret_values=secret_values,
|
|
1806
|
+
)
|
|
1807
|
+
return redacted if isinstance(redacted, dict) else {}
|
|
1808
|
+
|
|
1809
|
+
|
|
1810
|
+
def _safe_text(value: object) -> str:
|
|
1811
|
+
if isinstance(value, str):
|
|
1812
|
+
return value
|
|
1813
|
+
if isinstance(value, int | float | bool) or value is None:
|
|
1814
|
+
return str(value)
|
|
1815
|
+
if isinstance(value, Path):
|
|
1816
|
+
return value.as_posix()
|
|
1817
|
+
if isinstance(value, BaseException):
|
|
1818
|
+
parts = [
|
|
1819
|
+
_safe_text(arg)
|
|
1820
|
+
for arg in value.args
|
|
1821
|
+
if isinstance(arg, str | int | float | bool) or arg is None
|
|
1822
|
+
]
|
|
1823
|
+
detail = ": " + " ".join(parts) if parts else ""
|
|
1824
|
+
return f"{type(value).__name__}{detail}"
|
|
1825
|
+
return f"<{type(value).__module__}.{type(value).__qualname__}>"
|
|
1826
|
+
|
|
1827
|
+
|
|
1828
|
+
def _redact_key(key: object, policy: RedactionPolicy, budget: _RedactionBudget) -> str:
|
|
1829
|
+
text = redact_diagnostic_text(_safe_text(key), policy=policy)
|
|
1830
|
+
return budget.text(text[: policy.max_string_length])
|
|
1831
|
+
|
|
1832
|
+
|
|
1833
|
+
def _redact_value(
|
|
1834
|
+
value: object,
|
|
1835
|
+
*,
|
|
1836
|
+
policy: RedactionPolicy,
|
|
1837
|
+
secret_values: tuple[str, ...],
|
|
1838
|
+
budget: _RedactionBudget,
|
|
1839
|
+
depth: int,
|
|
1840
|
+
seen: set[int],
|
|
1841
|
+
sensitive_key: bool,
|
|
1842
|
+
) -> JsonValue:
|
|
1843
|
+
if sensitive_key:
|
|
1844
|
+
return budget.text(policy.replacement)
|
|
1845
|
+
if depth >= policy.max_depth:
|
|
1846
|
+
return budget.text("[max_depth]")
|
|
1847
|
+
if isinstance(value, str):
|
|
1848
|
+
return budget.text(
|
|
1849
|
+
redact_diagnostic_text(
|
|
1850
|
+
value,
|
|
1851
|
+
policy=policy,
|
|
1852
|
+
secret_values=secret_values,
|
|
1853
|
+
)
|
|
1854
|
+
)
|
|
1855
|
+
if isinstance(value, int | float | bool) or value is None:
|
|
1856
|
+
return value
|
|
1857
|
+
if isinstance(value, Path | BaseException):
|
|
1858
|
+
return budget.text(
|
|
1859
|
+
redact_diagnostic_text(
|
|
1860
|
+
_safe_text(value),
|
|
1861
|
+
policy=policy,
|
|
1862
|
+
secret_values=secret_values,
|
|
1863
|
+
)
|
|
1864
|
+
)
|
|
1865
|
+
if isinstance(value, Mapping):
|
|
1866
|
+
identity = id(value)
|
|
1867
|
+
if identity in seen:
|
|
1868
|
+
return budget.text("[cycle]")
|
|
1869
|
+
seen.add(identity)
|
|
1870
|
+
result: dict[str, JsonValue] = {}
|
|
1871
|
+
for index, (key, item) in enumerate(value.items()):
|
|
1872
|
+
if index >= policy.max_collection_items:
|
|
1873
|
+
result["[truncated]"] = budget.text("[truncated]")
|
|
1874
|
+
break
|
|
1875
|
+
clean_key = _redact_key(key, policy, budget)
|
|
1876
|
+
lowered = clean_key.lower()
|
|
1877
|
+
child_sensitive = _is_sensitive_field_name(lowered, policy)
|
|
1878
|
+
result[clean_key] = _redact_value(
|
|
1879
|
+
item,
|
|
1880
|
+
policy=policy,
|
|
1881
|
+
secret_values=secret_values,
|
|
1882
|
+
budget=budget,
|
|
1883
|
+
depth=depth + 1,
|
|
1884
|
+
seen=seen,
|
|
1885
|
+
sensitive_key=child_sensitive,
|
|
1886
|
+
)
|
|
1887
|
+
seen.remove(identity)
|
|
1888
|
+
return result
|
|
1889
|
+
if isinstance(value, tuple | list | set | frozenset):
|
|
1890
|
+
identity = id(value)
|
|
1891
|
+
if identity in seen:
|
|
1892
|
+
return budget.text("[cycle]")
|
|
1893
|
+
seen.add(identity)
|
|
1894
|
+
sequence_result: list[JsonValue] = [
|
|
1895
|
+
_redact_value(
|
|
1896
|
+
item,
|
|
1897
|
+
policy=policy,
|
|
1898
|
+
secret_values=secret_values,
|
|
1899
|
+
budget=budget,
|
|
1900
|
+
depth=depth + 1,
|
|
1901
|
+
seen=seen,
|
|
1902
|
+
sensitive_key=False,
|
|
1903
|
+
)
|
|
1904
|
+
for index, item in enumerate(value)
|
|
1905
|
+
if index < policy.max_collection_items
|
|
1906
|
+
]
|
|
1907
|
+
if len(value) > policy.max_collection_items:
|
|
1908
|
+
sequence_result.append(budget.text("[truncated]"))
|
|
1909
|
+
seen.remove(identity)
|
|
1910
|
+
return sequence_result
|
|
1911
|
+
return budget.text(
|
|
1912
|
+
redact_diagnostic_text(
|
|
1913
|
+
_safe_text(value),
|
|
1914
|
+
policy=policy,
|
|
1915
|
+
secret_values=secret_values,
|
|
1916
|
+
)
|
|
1917
|
+
)
|
|
1918
|
+
|
|
1919
|
+
|
|
1920
|
+
class ParsedToolArguments(BaseModel):
|
|
1921
|
+
"""Parsed JSON object arguments for a model-requested tool call."""
|
|
1922
|
+
|
|
1923
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
1924
|
+
|
|
1925
|
+
kind: Literal["parsed"] = "parsed"
|
|
1926
|
+
value: JsonObject = Field(default_factory=dict)
|
|
1927
|
+
|
|
1928
|
+
@property
|
|
1929
|
+
def values(self) -> JsonObject:
|
|
1930
|
+
"""Compatibility accessor; ``value`` is the serialized contract field."""
|
|
1931
|
+
return self.value
|
|
1932
|
+
|
|
1933
|
+
|
|
1934
|
+
class InvalidToolArguments(BaseModel):
|
|
1935
|
+
"""A malformed tool-argument payload for the public model bridge."""
|
|
1936
|
+
|
|
1937
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
1938
|
+
|
|
1939
|
+
kind: Literal["invalid"] = "invalid"
|
|
1940
|
+
raw: JsonValue
|
|
1941
|
+
error_code: str
|
|
1942
|
+
|
|
1943
|
+
@field_validator("error_code")
|
|
1944
|
+
@classmethod
|
|
1945
|
+
def _error_code_nonblank(cls, value: str) -> str:
|
|
1946
|
+
return _nonblank(value, "error_code")
|
|
1947
|
+
|
|
1948
|
+
|
|
1949
|
+
ToolArguments = Annotated[
|
|
1950
|
+
ParsedToolArguments | InvalidToolArguments, Field(discriminator="kind")
|
|
1951
|
+
]
|
|
1952
|
+
|
|
1953
|
+
|
|
1954
|
+
class ModelToolCall(BaseModel):
|
|
1955
|
+
"""Owned typed representation of an assistant-requested tool call."""
|
|
1956
|
+
|
|
1957
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
1958
|
+
|
|
1959
|
+
call_id: str
|
|
1960
|
+
name: str
|
|
1961
|
+
arguments: ToolArguments
|
|
1962
|
+
|
|
1963
|
+
@field_validator("arguments", mode="before")
|
|
1964
|
+
@classmethod
|
|
1965
|
+
def _coerce_arguments(cls, value: Any) -> Any:
|
|
1966
|
+
if isinstance(value, dict) and not (
|
|
1967
|
+
value.get("kind") in {"parsed", "invalid"} or set(value) == {"value"}
|
|
1968
|
+
):
|
|
1969
|
+
return ParsedToolArguments(value=value)
|
|
1970
|
+
return value
|
|
1971
|
+
|
|
1972
|
+
@property
|
|
1973
|
+
def id(self) -> str:
|
|
1974
|
+
"""Compatibility accessor; ``call_id`` is the serialized contract field."""
|
|
1975
|
+
return self.call_id
|
|
1976
|
+
|
|
1977
|
+
@field_validator("call_id", "name")
|
|
1978
|
+
@classmethod
|
|
1979
|
+
def _strings_nonblank(cls, value: str, info: Any) -> str:
|
|
1980
|
+
return _nonblank(value, info.field_name)
|
|
1981
|
+
|
|
1982
|
+
|
|
1983
|
+
class SystemMessage(BaseModel):
|
|
1984
|
+
"""System instructions sent through the model bridge."""
|
|
1985
|
+
|
|
1986
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
1987
|
+
|
|
1988
|
+
role: Literal["system"] = "system"
|
|
1989
|
+
content: str
|
|
1990
|
+
|
|
1991
|
+
@property
|
|
1992
|
+
def kind(self) -> str:
|
|
1993
|
+
"""Compatibility accessor; ``role`` is the serialized discriminator."""
|
|
1994
|
+
return self.role
|
|
1995
|
+
|
|
1996
|
+
@field_validator("content")
|
|
1997
|
+
@classmethod
|
|
1998
|
+
def _content_nonblank(cls, value: str) -> str:
|
|
1999
|
+
return _nonblank(value, "content")
|
|
2000
|
+
|
|
2001
|
+
|
|
2002
|
+
class UserMessage(BaseModel):
|
|
2003
|
+
"""User message sent through the model bridge."""
|
|
2004
|
+
|
|
2005
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
2006
|
+
|
|
2007
|
+
role: Literal["user"] = "user"
|
|
2008
|
+
content: str
|
|
2009
|
+
|
|
2010
|
+
@property
|
|
2011
|
+
def kind(self) -> str:
|
|
2012
|
+
"""Compatibility accessor; ``role`` is the serialized discriminator."""
|
|
2013
|
+
return self.role
|
|
2014
|
+
|
|
2015
|
+
@field_validator("content")
|
|
2016
|
+
@classmethod
|
|
2017
|
+
def _content_nonblank(cls, value: str) -> str:
|
|
2018
|
+
return _nonblank(value, "content")
|
|
2019
|
+
|
|
2020
|
+
|
|
2021
|
+
class AssistantMessage(BaseModel):
|
|
2022
|
+
"""Assistant response message containing text and/or tool calls."""
|
|
2023
|
+
|
|
2024
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
2025
|
+
|
|
2026
|
+
role: Literal["assistant"] = "assistant"
|
|
2027
|
+
content: str | None = None
|
|
2028
|
+
tool_calls: Tuple[ModelToolCall, ...] = Field(default_factory=tuple)
|
|
2029
|
+
reasoning_content: str | None = Field(default=None, repr=False)
|
|
2030
|
+
|
|
2031
|
+
@property
|
|
2032
|
+
def kind(self) -> str:
|
|
2033
|
+
"""Compatibility accessor; ``role`` is the serialized discriminator."""
|
|
2034
|
+
return self.role
|
|
2035
|
+
|
|
2036
|
+
@field_validator("content", "reasoning_content")
|
|
2037
|
+
@classmethod
|
|
2038
|
+
def _content_nonblank(cls, value: str | None, info: Any) -> str | None:
|
|
2039
|
+
return None if value is None else _nonblank(value, info.field_name)
|
|
2040
|
+
|
|
2041
|
+
@model_serializer(mode="wrap")
|
|
2042
|
+
def _omit_absent_reasoning_content(self, handler: Any) -> dict[str, Any]:
|
|
2043
|
+
payload = handler(self)
|
|
2044
|
+
if self.reasoning_content is None:
|
|
2045
|
+
payload.pop("reasoning_content", None)
|
|
2046
|
+
return payload
|
|
2047
|
+
|
|
2048
|
+
@model_validator(mode="after")
|
|
2049
|
+
def _assistant_has_content_or_tools(self) -> AssistantMessage:
|
|
2050
|
+
_unique(tuple(call.call_id for call in self.tool_calls), "assistant call_id")
|
|
2051
|
+
if self.content is None and not self.tool_calls:
|
|
2052
|
+
raise ValueError("assistant message requires content or tool_calls")
|
|
2053
|
+
return self
|
|
2054
|
+
|
|
2055
|
+
|
|
2056
|
+
class ToolResultMessage(BaseModel):
|
|
2057
|
+
"""Model-visible result for a prior assistant tool call."""
|
|
2058
|
+
|
|
2059
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
2060
|
+
|
|
2061
|
+
role: Literal["tool"] = "tool"
|
|
2062
|
+
tool_call_id: str
|
|
2063
|
+
tool_name: str
|
|
2064
|
+
content: str
|
|
2065
|
+
|
|
2066
|
+
@property
|
|
2067
|
+
def kind(self) -> str:
|
|
2068
|
+
"""Compatibility accessor; ``role`` is the serialized discriminator."""
|
|
2069
|
+
return "tool_result"
|
|
2070
|
+
|
|
2071
|
+
@field_validator("tool_call_id", "tool_name", "content")
|
|
2072
|
+
@classmethod
|
|
2073
|
+
def _strings_nonblank(cls, value: str, info: Any) -> str:
|
|
2074
|
+
return _nonblank(value, info.field_name)
|
|
2075
|
+
|
|
2076
|
+
|
|
2077
|
+
ModelMessage = Annotated[
|
|
2078
|
+
SystemMessage | UserMessage | AssistantMessage | ToolResultMessage,
|
|
2079
|
+
Field(discriminator="role"),
|
|
2080
|
+
]
|
|
2081
|
+
|
|
2082
|
+
|
|
2083
|
+
class ModelToolDefinition(BaseModel):
|
|
2084
|
+
"""Owned model-visible tool definition."""
|
|
2085
|
+
|
|
2086
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
2087
|
+
|
|
2088
|
+
name: str
|
|
2089
|
+
description: str
|
|
2090
|
+
input_schema: JsonObject
|
|
2091
|
+
|
|
2092
|
+
@field_validator("name", "description")
|
|
2093
|
+
@classmethod
|
|
2094
|
+
def _strings_nonblank(cls, value: str, info: Any) -> str:
|
|
2095
|
+
return _nonblank(value, info.field_name)
|
|
2096
|
+
|
|
2097
|
+
|
|
2098
|
+
class ModelCompletionRequest(BaseModel):
|
|
2099
|
+
"""Immutable validated model inference request."""
|
|
2100
|
+
|
|
2101
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
2102
|
+
|
|
2103
|
+
request_id: str
|
|
2104
|
+
run_id: str
|
|
2105
|
+
model_profile_id: str
|
|
2106
|
+
messages: Tuple[ModelMessage, ...] = Field(description="Typed chat messages")
|
|
2107
|
+
tools: Tuple[ModelToolDefinition, ...] = Field(
|
|
2108
|
+
default_factory=tuple, description="Available tool definitions"
|
|
2109
|
+
)
|
|
2110
|
+
required_capabilities: ModelCapabilityRequirements = Field(
|
|
2111
|
+
default_factory=ModelCapabilityRequirements
|
|
2112
|
+
)
|
|
2113
|
+
sampling_overrides: SamplingRequest = Field(default_factory=SamplingRequest)
|
|
2114
|
+
maximum_output_tokens_override: int | None = Field(default=None, gt=0)
|
|
2115
|
+
request_options: JsonObject = Field(default_factory=dict)
|
|
2116
|
+
deadline: Deadline
|
|
2117
|
+
cancellation: CancellationRef
|
|
2118
|
+
secret_refs: Tuple[SecretRef, ...] = Field(default_factory=tuple)
|
|
2119
|
+
|
|
2120
|
+
@property
|
|
2121
|
+
def model(self) -> str:
|
|
2122
|
+
"""Compatibility accessor; ``model_profile_id`` is the contract field."""
|
|
2123
|
+
return self.model_profile_id
|
|
2124
|
+
|
|
2125
|
+
@field_validator("request_id", "run_id", "model_profile_id")
|
|
2126
|
+
@classmethod
|
|
2127
|
+
def _strings_nonblank(cls, value: str, info: Any) -> str:
|
|
2128
|
+
return _nonblank(value, info.field_name)
|
|
2129
|
+
|
|
2130
|
+
@model_validator(mode="after")
|
|
2131
|
+
def _request_invariants(self) -> ModelCompletionRequest:
|
|
2132
|
+
protected_options = {
|
|
2133
|
+
"model",
|
|
2134
|
+
"messages",
|
|
2135
|
+
"tools",
|
|
2136
|
+
"stream",
|
|
2137
|
+
"endpoint",
|
|
2138
|
+
"authentication",
|
|
2139
|
+
"timeout",
|
|
2140
|
+
"headers",
|
|
2141
|
+
"host",
|
|
2142
|
+
"content_type",
|
|
2143
|
+
"user_agent",
|
|
2144
|
+
"max_tokens",
|
|
2145
|
+
"maximum_output_tokens",
|
|
2146
|
+
"temperature",
|
|
2147
|
+
"top_p",
|
|
2148
|
+
"presence_penalty",
|
|
2149
|
+
"frequency_penalty",
|
|
2150
|
+
"seed",
|
|
2151
|
+
"stop",
|
|
2152
|
+
}
|
|
2153
|
+
for option_name in self.request_options:
|
|
2154
|
+
if option_name in protected_options:
|
|
2155
|
+
raise ValueError(f"request option {option_name!r} is protected")
|
|
2156
|
+
_unique(tuple(tool.name for tool in self.tools), "tool name")
|
|
2157
|
+
_unique(tuple(secret.secret_id for secret in self.secret_refs), "secret_id")
|
|
2158
|
+
pending_tool_calls: dict[str, str] = {}
|
|
2159
|
+
answered_tool_call_ids: set[str] = set()
|
|
2160
|
+
for message in self.messages:
|
|
2161
|
+
if isinstance(message, AssistantMessage):
|
|
2162
|
+
for call in message.tool_calls:
|
|
2163
|
+
if (
|
|
2164
|
+
call.call_id in pending_tool_calls
|
|
2165
|
+
or call.call_id in answered_tool_call_ids
|
|
2166
|
+
):
|
|
2167
|
+
raise ValueError("assistant tool-call IDs must be unique")
|
|
2168
|
+
pending_tool_calls[call.call_id] = call.name
|
|
2169
|
+
elif isinstance(message, ToolResultMessage):
|
|
2170
|
+
expected_tool_name = pending_tool_calls.get(message.tool_call_id)
|
|
2171
|
+
if expected_tool_name is None:
|
|
2172
|
+
raise ValueError("tool-result message has no matching tool call")
|
|
2173
|
+
if message.tool_name != expected_tool_name:
|
|
2174
|
+
raise ValueError("tool-result message tool_name does not match")
|
|
2175
|
+
if message.tool_call_id in answered_tool_call_ids:
|
|
2176
|
+
raise ValueError("tool-result message duplicates a tool call")
|
|
2177
|
+
answered_tool_call_ids.add(message.tool_call_id)
|
|
2178
|
+
return self
|
|
2179
|
+
|
|
2180
|
+
|
|
2181
|
+
class UsageMetadata(BaseModel):
|
|
2182
|
+
"""Token usage metadata for a model request/response pair."""
|
|
2183
|
+
|
|
2184
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
2185
|
+
|
|
2186
|
+
model_calls: int = Field(ge=0, description="Number of model calls made")
|
|
2187
|
+
tool_calls: int = Field(ge=0, description="Number of tool calls made")
|
|
2188
|
+
token_usage: TokenUsage | None = Field(
|
|
2189
|
+
default=None, description="Detailed token usage breakdown"
|
|
2190
|
+
)
|
|
2191
|
+
|
|
2192
|
+
|
|
2193
|
+
class ModelCompletionResponse(BaseModel):
|
|
2194
|
+
"""Immutable validated model inference response."""
|
|
2195
|
+
|
|
2196
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
2197
|
+
|
|
2198
|
+
provider_request_id: str | None = None
|
|
2199
|
+
model_id: str
|
|
2200
|
+
message: AssistantMessage
|
|
2201
|
+
finish_reason: Literal[
|
|
2202
|
+
"stop",
|
|
2203
|
+
"tool_calls",
|
|
2204
|
+
"length",
|
|
2205
|
+
"content_filter",
|
|
2206
|
+
"cancelled",
|
|
2207
|
+
"unknown",
|
|
2208
|
+
]
|
|
2209
|
+
usage: TokenUsage | None = Field(default=None, description="Token usage metadata")
|
|
2210
|
+
provider_metadata: SanitizedMetadata | None = None
|
|
2211
|
+
|
|
2212
|
+
@property
|
|
2213
|
+
def model(self) -> str:
|
|
2214
|
+
"""Compatibility accessor; ``model_id`` is the contract field."""
|
|
2215
|
+
return self.model_id
|
|
2216
|
+
|
|
2217
|
+
@property
|
|
2218
|
+
def content(self) -> str | None:
|
|
2219
|
+
"""Compatibility accessor for the assistant message content."""
|
|
2220
|
+
return self.message.content
|
|
2221
|
+
|
|
2222
|
+
@property
|
|
2223
|
+
def tool_calls(self) -> tuple[ModelToolCall, ...]:
|
|
2224
|
+
"""Compatibility accessor for the assistant message tool calls."""
|
|
2225
|
+
return self.message.tool_calls
|
|
2226
|
+
|
|
2227
|
+
@field_validator("provider_request_id", "model_id")
|
|
2228
|
+
@classmethod
|
|
2229
|
+
def _strings_nonblank(cls, value: str | None, info: Any) -> str | None:
|
|
2230
|
+
return None if value is None else _nonblank(value, info.field_name)
|
|
2231
|
+
|
|
2232
|
+
@model_validator(mode="after")
|
|
2233
|
+
def _finish_reason_matches_message(self) -> ModelCompletionResponse:
|
|
2234
|
+
if self.message.tool_calls and self.finish_reason != "tool_calls":
|
|
2235
|
+
raise ValueError("tool call responses require finish_reason='tool_calls'")
|
|
2236
|
+
return self
|
|
2237
|
+
|
|
2238
|
+
|
|
2239
|
+
class ValidatedToolCall(BaseModel):
|
|
2240
|
+
"""Immutable validated tool call from a model response."""
|
|
2241
|
+
|
|
2242
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
2243
|
+
|
|
2244
|
+
call_id: str = Field(description="Unique tool call identifier")
|
|
2245
|
+
node_id: str = Field(description="Compiled node identifier")
|
|
2246
|
+
binding: ToolBindingRef = Field(description="Resolved tool binding")
|
|
2247
|
+
arguments: JsonObject = Field(description="Canonical JSON object arguments")
|
|
2248
|
+
|
|
2249
|
+
@property
|
|
2250
|
+
def id(self) -> str:
|
|
2251
|
+
"""Compatibility accessor; ``call_id`` is the serialized contract field."""
|
|
2252
|
+
return self.call_id
|
|
2253
|
+
|
|
2254
|
+
@property
|
|
2255
|
+
def name(self) -> str:
|
|
2256
|
+
"""Compatibility accessor for legacy fakes; not a serialized field."""
|
|
2257
|
+
return self.binding.tool_id
|
|
2258
|
+
|
|
2259
|
+
@field_validator("call_id", "node_id")
|
|
2260
|
+
@classmethod
|
|
2261
|
+
def _strings_nonblank(cls, value: str, info: Any) -> str:
|
|
2262
|
+
return _nonblank(value, info.field_name)
|
|
2263
|
+
|
|
2264
|
+
|
|
2265
|
+
class ToolExecutionResult(BaseModel):
|
|
2266
|
+
"""Immutable validated tool execution result."""
|
|
2267
|
+
|
|
2268
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
2269
|
+
|
|
2270
|
+
call_id: str = Field(description="Identifier of the originating tool call")
|
|
2271
|
+
status: ToolExecutionStatus = Field(description="Closed tool execution status")
|
|
2272
|
+
summary: str = Field(description="Bounded model-visible summary")
|
|
2273
|
+
structured_data: JsonValue = None
|
|
2274
|
+
artifact_refs: Tuple[ArtifactRef, ...] = Field(default_factory=tuple)
|
|
2275
|
+
error_code: str | None = Field(
|
|
2276
|
+
default=None, description="Stable error code when execution failed"
|
|
2277
|
+
)
|
|
2278
|
+
retryable: bool = Field(
|
|
2279
|
+
default=False, description="Whether retrying this tool call is safe"
|
|
2280
|
+
)
|
|
2281
|
+
side_effect_class: SideEffectClass
|
|
2282
|
+
idempotency: IdempotencyClass
|
|
2283
|
+
side_effect_certainty: SideEffectCertainty
|
|
2284
|
+
side_effect_record: SideEffectRecord | None = Field(
|
|
2285
|
+
default=None,
|
|
2286
|
+
description="Typed side-effect detail when certainty needs explanation",
|
|
2287
|
+
)
|
|
2288
|
+
input_sha256: str
|
|
2289
|
+
output_sha256: str | None = Field(
|
|
2290
|
+
default=None, description="SHA-256 hash of the serialized safe output"
|
|
2291
|
+
)
|
|
2292
|
+
timing: TimingMetadata = Field(description="Canonical timing metadata")
|
|
2293
|
+
|
|
2294
|
+
@property
|
|
2295
|
+
def duration_ms(self) -> float:
|
|
2296
|
+
"""Compatibility accessor; ``timing`` is the serialized contract field."""
|
|
2297
|
+
return self.timing.duration_ms
|
|
2298
|
+
|
|
2299
|
+
@field_validator("call_id", "summary")
|
|
2300
|
+
@classmethod
|
|
2301
|
+
def _strings_nonblank(cls, value: str, info: Any) -> str:
|
|
2302
|
+
return _nonblank(value, info.field_name)
|
|
2303
|
+
|
|
2304
|
+
@field_validator("error_code")
|
|
2305
|
+
@classmethod
|
|
2306
|
+
def _error_code_nonblank(cls, value: str | None) -> str | None:
|
|
2307
|
+
return None if value is None else _nonblank(value, "error_code")
|
|
2308
|
+
|
|
2309
|
+
@field_validator("input_sha256")
|
|
2310
|
+
@classmethod
|
|
2311
|
+
def _input_sha256_valid(cls, value: str) -> str:
|
|
2312
|
+
return _validate_sha256(value, "input_sha256")
|
|
2313
|
+
|
|
2314
|
+
@field_validator("output_sha256")
|
|
2315
|
+
@classmethod
|
|
2316
|
+
def _output_sha256_valid(cls, value: str | None) -> str | None:
|
|
2317
|
+
return None if value is None else _validate_sha256(value, "output_sha256")
|
|
2318
|
+
|
|
2319
|
+
@model_validator(mode="after")
|
|
2320
|
+
def _result_consistency(self) -> ToolExecutionResult:
|
|
2321
|
+
if self.status == ToolExecutionStatus.SUCCESS:
|
|
2322
|
+
if self.error_code is not None:
|
|
2323
|
+
raise ValueError("successful tool results must not include error_code")
|
|
2324
|
+
if self.retryable:
|
|
2325
|
+
raise ValueError("successful tool results must not be retryable")
|
|
2326
|
+
else:
|
|
2327
|
+
if self.error_code is None:
|
|
2328
|
+
raise ValueError("failed tool results require error_code")
|
|
2329
|
+
if (
|
|
2330
|
+
self.side_effect_certainty == SideEffectCertainty.COMPLETION_UNKNOWN
|
|
2331
|
+
and self.idempotency
|
|
2332
|
+
in {IdempotencyClass.NON_IDEMPOTENT, IdempotencyClass.UNKNOWN}
|
|
2333
|
+
and self.retryable
|
|
2334
|
+
):
|
|
2335
|
+
raise ValueError(
|
|
2336
|
+
"completion_unknown side effects are not retryable for "
|
|
2337
|
+
"non-idempotent or unknown-idempotency tool work"
|
|
2338
|
+
)
|
|
2339
|
+
if self.side_effect_record is not None:
|
|
2340
|
+
if self.side_effect_record.certainty != self.side_effect_certainty:
|
|
2341
|
+
raise ValueError(
|
|
2342
|
+
"side_effect_record certainty must match side_effect_certainty"
|
|
2343
|
+
)
|
|
2344
|
+
if self.side_effect_record.retry_allowed != self.retryable:
|
|
2345
|
+
raise ValueError(
|
|
2346
|
+
"side_effect_record retry_allowed must match retryable"
|
|
2347
|
+
)
|
|
2348
|
+
return self
|
|
2349
|
+
|
|
2350
|
+
|
|
2351
|
+
class SideEffectRecord(BaseModel):
|
|
2352
|
+
"""Typed side-effect detail for uncertain, rolled back, or absent tool effects."""
|
|
2353
|
+
|
|
2354
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
2355
|
+
|
|
2356
|
+
certainty: SideEffectCertainty
|
|
2357
|
+
detail_code: str
|
|
2358
|
+
summary: str
|
|
2359
|
+
retry_allowed: bool
|
|
2360
|
+
|
|
2361
|
+
@field_validator("detail_code", "summary")
|
|
2362
|
+
@classmethod
|
|
2363
|
+
def _strings_nonblank(cls, value: str, info: Any) -> str:
|
|
2364
|
+
return _nonblank(value, info.field_name)
|
|
2365
|
+
|
|
2366
|
+
|
|
2367
|
+
# ---------------------------------------------------------------------------
|
|
2368
|
+
# Session models (mutable working models)
|
|
2369
|
+
# ---------------------------------------------------------------------------
|
|
2370
|
+
|
|
2371
|
+
|
|
2372
|
+
class GuardedSessionRequest(BaseModel):
|
|
2373
|
+
"""Request wrapped in a guarded session.
|
|
2374
|
+
|
|
2375
|
+
**Immutable** — once constructed, the session request is not modified.
|
|
2376
|
+
Includes a deadline for session-level time bounds.
|
|
2377
|
+
"""
|
|
2378
|
+
|
|
2379
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
2380
|
+
|
|
2381
|
+
session_id: str = Field(description="Unique session identifier")
|
|
2382
|
+
execution_request: HarnessExecutionRequest = Field(
|
|
2383
|
+
description="The harness execution request"
|
|
2384
|
+
)
|
|
2385
|
+
deadline: Deadline = Field(description="Deadline for the guarded session")
|
|
2386
|
+
tool_execution_context: ToolExecutionContext | None = Field(
|
|
2387
|
+
default=None,
|
|
2388
|
+
description="Runtime-owned context passed through to tool execution",
|
|
2389
|
+
)
|
|
2390
|
+
|
|
2391
|
+
|
|
2392
|
+
class GuardedSessionResult(BaseModel):
|
|
2393
|
+
"""Result from a guarded session.
|
|
2394
|
+
|
|
2395
|
+
**Immutable** — once produced, the result is not modified.
|
|
2396
|
+
Includes structured event and tool trace records alongside
|
|
2397
|
+
the existing terminal intent, usage, timing, and diagnostic fields.
|
|
2398
|
+
"""
|
|
2399
|
+
|
|
2400
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
2401
|
+
|
|
2402
|
+
session_id: str = Field(description="Unique session identifier")
|
|
2403
|
+
status: GuardedSessionStatus = Field(description="Session status")
|
|
2404
|
+
terminal_intent: TerminalIntent | None = Field(
|
|
2405
|
+
default=None, description="Terminal intent if session completed"
|
|
2406
|
+
)
|
|
2407
|
+
artifact_refs: Tuple[ArtifactRef, ...] = Field(
|
|
2408
|
+
default_factory=tuple, description="Artifact references produced"
|
|
2409
|
+
)
|
|
2410
|
+
usage: UsageMetadata | None = Field(
|
|
2411
|
+
default=None, description="Token usage metadata"
|
|
2412
|
+
)
|
|
2413
|
+
timing: TimingMetadata | None = Field(default=None, description="Timing metadata")
|
|
2414
|
+
diagnostic: DiagnosticMetadata | None = Field(
|
|
2415
|
+
default=None, description="Diagnostic metadata"
|
|
2416
|
+
)
|
|
2417
|
+
events: Tuple[SessionEvent, ...] = Field(
|
|
2418
|
+
default_factory=tuple, description="Session events recorded"
|
|
2419
|
+
)
|
|
2420
|
+
tool_trace: Tuple[ToolTraceRecord, ...] = Field(
|
|
2421
|
+
default_factory=tuple, description="Tool trace records"
|
|
2422
|
+
)
|
|
2423
|
+
|
|
2424
|
+
|
|
2425
|
+
# ---------------------------------------------------------------------------
|
|
2426
|
+
# Intent and result models (immutable snapshots)
|
|
2427
|
+
# ---------------------------------------------------------------------------
|
|
2428
|
+
|
|
2429
|
+
|
|
2430
|
+
class TerminalIntent(BaseModel):
|
|
2431
|
+
"""Terminal intent expressing a provider-local stage disposition.
|
|
2432
|
+
|
|
2433
|
+
Extended for 02B shape — includes request identity, stage
|
|
2434
|
+
identity, terminal node, closed disposition, summary, and
|
|
2435
|
+
artifact references. Immutable snapshot — once emitted, the
|
|
2436
|
+
intent is not modified. Correlation values are echoed from the execution
|
|
2437
|
+
request and do not grant caller workflow or terminal authority.
|
|
2438
|
+
"""
|
|
2439
|
+
|
|
2440
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
2441
|
+
|
|
2442
|
+
request_id: str = Field(description="Unique request identifier")
|
|
2443
|
+
run_id: str = Field(description="Run this request belongs to")
|
|
2444
|
+
stage: StageIdentity = Field(description="Stage identity")
|
|
2445
|
+
terminal_node_id: str = Field(description="Terminal node identifier")
|
|
2446
|
+
terminal_result: str = Field(description="Terminal result string")
|
|
2447
|
+
disposition: Literal["success", "blocked", "rejected", "escalated"] = Field(
|
|
2448
|
+
description="Terminal disposition (closed)"
|
|
2449
|
+
)
|
|
2450
|
+
summary: str = Field(description="Human-readable summary")
|
|
2451
|
+
artifact_refs: Tuple[ArtifactRef, ...] = Field(
|
|
2452
|
+
default_factory=tuple, description="Artifact references"
|
|
2453
|
+
)
|
|
2454
|
+
selected_output: SelectedOutput | None = Field(
|
|
2455
|
+
default=None,
|
|
2456
|
+
exclude_if=lambda value: value is None,
|
|
2457
|
+
description="Admitted selected JSON output, explicitly present or absent",
|
|
2458
|
+
)
|
|
2459
|
+
selected_output_schema_sha256: str | None = Field(
|
|
2460
|
+
default=None,
|
|
2461
|
+
exclude_if=lambda value: value is None,
|
|
2462
|
+
description="Digest of the invocation-local selected schema authority",
|
|
2463
|
+
)
|
|
2464
|
+
|
|
2465
|
+
@field_validator("request_id")
|
|
2466
|
+
@classmethod
|
|
2467
|
+
def _request_id_must_be_non_empty(cls, v: str) -> str:
|
|
2468
|
+
if not v.strip():
|
|
2469
|
+
raise ValueError("request_id must be a non-empty string")
|
|
2470
|
+
return v
|
|
2471
|
+
|
|
2472
|
+
@field_validator("terminal_node_id")
|
|
2473
|
+
@classmethod
|
|
2474
|
+
def _terminal_node_id_must_be_non_empty(cls, v: str) -> str:
|
|
2475
|
+
if not v.strip():
|
|
2476
|
+
raise ValueError("terminal_node_id must be a non-empty string")
|
|
2477
|
+
return v
|
|
2478
|
+
|
|
2479
|
+
@field_validator("terminal_result")
|
|
2480
|
+
@classmethod
|
|
2481
|
+
def _terminal_result_must_be_non_empty(cls, v: str) -> str:
|
|
2482
|
+
if not v.strip():
|
|
2483
|
+
raise ValueError("terminal_result must be a non-empty string")
|
|
2484
|
+
return v
|
|
2485
|
+
|
|
2486
|
+
@field_validator("summary")
|
|
2487
|
+
@classmethod
|
|
2488
|
+
def _summary_must_be_non_empty(cls, v: str) -> str:
|
|
2489
|
+
if not v.strip():
|
|
2490
|
+
raise ValueError("summary must be a non-empty string")
|
|
2491
|
+
return v
|
|
2492
|
+
|
|
2493
|
+
@field_validator("selected_output_schema_sha256")
|
|
2494
|
+
@classmethod
|
|
2495
|
+
def _selected_schema_digest_valid(cls, value: str | None) -> str | None:
|
|
2496
|
+
if value is None:
|
|
2497
|
+
return None
|
|
2498
|
+
return _validate_sha256(value, "selected_output_schema_sha256")
|
|
2499
|
+
|
|
2500
|
+
@model_validator(mode="after")
|
|
2501
|
+
def _selected_output_authority_is_paired(self) -> TerminalIntent:
|
|
2502
|
+
if (self.selected_output is None) != (
|
|
2503
|
+
self.selected_output_schema_sha256 is None
|
|
2504
|
+
):
|
|
2505
|
+
raise ValueError(
|
|
2506
|
+
"selected_output and selected_output_schema_sha256 must be paired"
|
|
2507
|
+
)
|
|
2508
|
+
return self
|
|
2509
|
+
|
|
2510
|
+
|
|
2511
|
+
class HarnessExecutionResult(BaseModel):
|
|
2512
|
+
"""Result of a harness execution.
|
|
2513
|
+
|
|
2514
|
+
Immutable snapshot with semantic result classification and
|
|
2515
|
+
structured metadata — 02B semantic shape replaces legacy
|
|
2516
|
+
process-shaped fields. Its stage and terminal intent remain provider-local;
|
|
2517
|
+
request and run identifiers remain opaque caller correlation values.
|
|
2518
|
+
"""
|
|
2519
|
+
|
|
2520
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
2521
|
+
|
|
2522
|
+
status: ExecutionStatus = Field(description="Execution status")
|
|
2523
|
+
result_class: ExecutionResultClass = Field(description="Result classification")
|
|
2524
|
+
request_id: str = Field(description="Unique request identifier")
|
|
2525
|
+
run_id: str = Field(description="Run this request belongs to")
|
|
2526
|
+
stage: StageIdentity = Field(description="Stage identity")
|
|
2527
|
+
terminal_intent: TerminalIntent | None = Field(
|
|
2528
|
+
default=None, description="Terminal intent if session completed"
|
|
2529
|
+
)
|
|
2530
|
+
artifact_refs: Tuple[ArtifactRef, ...] = Field(
|
|
2531
|
+
default_factory=tuple, description="Artifact references produced"
|
|
2532
|
+
)
|
|
2533
|
+
compiled_harness: CompiledHarnessRef = Field(
|
|
2534
|
+
description="Reference to the compiled harness"
|
|
2535
|
+
)
|
|
2536
|
+
usage: UsageMetadata | None = Field(
|
|
2537
|
+
default=None, description="Token usage metadata"
|
|
2538
|
+
)
|
|
2539
|
+
timing: TimingMetadata = Field(description="Timing metadata")
|
|
2540
|
+
diagnostic: DiagnosticMetadata | None = Field(
|
|
2541
|
+
default=None, description="Diagnostic metadata"
|
|
2542
|
+
)
|
|
2543
|
+
terminal_certainty: TerminalCertainty = Field(
|
|
2544
|
+
default=TerminalCertainty.NOT_APPLICABLE,
|
|
2545
|
+
description="Certainty of terminal-result commit ordering",
|
|
2546
|
+
)
|
|
2547
|
+
selected_output: SelectedOutput | None = Field(
|
|
2548
|
+
default=None,
|
|
2549
|
+
exclude_if=lambda value: value is None,
|
|
2550
|
+
description="Admitted selected JSON output, explicitly present or absent",
|
|
2551
|
+
)
|
|
2552
|
+
selected_output_schema_sha256: str | None = Field(
|
|
2553
|
+
default=None,
|
|
2554
|
+
exclude_if=lambda value: value is None,
|
|
2555
|
+
description="Digest of the invocation-local selected schema authority",
|
|
2556
|
+
)
|
|
2557
|
+
|
|
2558
|
+
@field_validator("selected_output_schema_sha256")
|
|
2559
|
+
@classmethod
|
|
2560
|
+
def _selected_schema_digest_valid(cls, value: str | None) -> str | None:
|
|
2561
|
+
if value is None:
|
|
2562
|
+
return None
|
|
2563
|
+
return _validate_sha256(value, "selected_output_schema_sha256")
|
|
2564
|
+
|
|
2565
|
+
@model_validator(mode="after")
|
|
2566
|
+
def _check_result_class_invariants(self) -> HarnessExecutionResult:
|
|
2567
|
+
if (self.selected_output is None) != (
|
|
2568
|
+
self.selected_output_schema_sha256 is None
|
|
2569
|
+
):
|
|
2570
|
+
raise ValueError(
|
|
2571
|
+
"selected_output and selected_output_schema_sha256 must be paired"
|
|
2572
|
+
)
|
|
2573
|
+
completed_classes = {
|
|
2574
|
+
ExecutionResultClass.DOMAIN_TERMINAL,
|
|
2575
|
+
ExecutionResultClass.DOMAIN_REJECTED,
|
|
2576
|
+
}
|
|
2577
|
+
if self.status == ExecutionStatus.COMPLETED:
|
|
2578
|
+
if self.result_class not in completed_classes:
|
|
2579
|
+
raise ValueError(
|
|
2580
|
+
"status=completed is only valid for domain_terminal "
|
|
2581
|
+
"or domain_rejected"
|
|
2582
|
+
)
|
|
2583
|
+
elif self.result_class in completed_classes:
|
|
2584
|
+
raise ValueError("domain result classes must use status=completed")
|
|
2585
|
+
|
|
2586
|
+
if self.terminal_intent is not None:
|
|
2587
|
+
if (
|
|
2588
|
+
self.status != ExecutionStatus.COMPLETED
|
|
2589
|
+
or self.result_class not in completed_classes
|
|
2590
|
+
):
|
|
2591
|
+
raise ValueError(
|
|
2592
|
+
"terminal_intent is only valid for completed domain results"
|
|
2593
|
+
)
|
|
2594
|
+
if self.terminal_intent.request_id != self.request_id:
|
|
2595
|
+
raise ValueError("terminal_intent.request_id must match result")
|
|
2596
|
+
if self.terminal_intent.run_id != self.run_id:
|
|
2597
|
+
raise ValueError("terminal_intent.run_id must match result")
|
|
2598
|
+
if self.terminal_intent.stage != self.stage:
|
|
2599
|
+
raise ValueError("terminal_intent.stage must match result")
|
|
2600
|
+
if self.terminal_intent.selected_output != self.selected_output:
|
|
2601
|
+
raise ValueError("terminal_intent.selected_output must match result")
|
|
2602
|
+
if (
|
|
2603
|
+
self.terminal_intent.selected_output_schema_sha256
|
|
2604
|
+
!= self.selected_output_schema_sha256
|
|
2605
|
+
):
|
|
2606
|
+
raise ValueError(
|
|
2607
|
+
"terminal_intent selected schema digest must match result"
|
|
2608
|
+
)
|
|
2609
|
+
elif (
|
|
2610
|
+
self.terminal_certainty == TerminalCertainty.COMMITTED
|
|
2611
|
+
and self.result_class
|
|
2612
|
+
not in {
|
|
2613
|
+
ExecutionResultClass.DOMAIN_TERMINAL,
|
|
2614
|
+
ExecutionResultClass.DOMAIN_REJECTED,
|
|
2615
|
+
}
|
|
2616
|
+
):
|
|
2617
|
+
raise ValueError("committed terminal_certainty requires a domain result")
|
|
2618
|
+
if self.selected_output is not None and self.terminal_intent is None:
|
|
2619
|
+
raise ValueError(
|
|
2620
|
+
"selected_output authority requires a matching terminal_intent"
|
|
2621
|
+
)
|
|
2622
|
+
return self
|
|
2623
|
+
|
|
2624
|
+
|
|
2625
|
+
# ---------------------------------------------------------------------------
|
|
2626
|
+
# Timing and diagnostic models (immutable snapshots)
|
|
2627
|
+
# ---------------------------------------------------------------------------
|
|
2628
|
+
|
|
2629
|
+
|
|
2630
|
+
class TimingMetadata(BaseModel):
|
|
2631
|
+
"""Timing and duration metadata.
|
|
2632
|
+
|
|
2633
|
+
All fields are required — ``completed_at`` is now mandatory.
|
|
2634
|
+
"""
|
|
2635
|
+
|
|
2636
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
2637
|
+
|
|
2638
|
+
started_at: str = Field(description="ISO-8601 start timestamp")
|
|
2639
|
+
completed_at: str = Field(description="ISO-8601 completion timestamp")
|
|
2640
|
+
duration_ms: float = Field(ge=0, description="Duration in milliseconds")
|
|
2641
|
+
|
|
2642
|
+
|
|
2643
|
+
class DiagnosticMetadata(BaseModel):
|
|
2644
|
+
"""Immutable diagnostic metadata with structured field-level diagnostics."""
|
|
2645
|
+
|
|
2646
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
2647
|
+
|
|
2648
|
+
error_code: str = Field(description="Top-level error code identifier")
|
|
2649
|
+
category: Literal[
|
|
2650
|
+
"binding",
|
|
2651
|
+
"compiled_harness",
|
|
2652
|
+
"backend",
|
|
2653
|
+
"model",
|
|
2654
|
+
"tool",
|
|
2655
|
+
"budget",
|
|
2656
|
+
"timeout",
|
|
2657
|
+
"cancellation",
|
|
2658
|
+
"artifact",
|
|
2659
|
+
"internal",
|
|
2660
|
+
] = Field(description="Closed diagnostic category")
|
|
2661
|
+
message: str = Field(description="Human-readable diagnostic message")
|
|
2662
|
+
retryable: bool = Field(description="Whether retrying may resolve this diagnostic")
|
|
2663
|
+
origin: str | TimeoutOrigin = Field(description="Failure origin or subsystem")
|
|
2664
|
+
fields: tuple[DiagnosticField, ...] = Field(
|
|
2665
|
+
default_factory=tuple,
|
|
2666
|
+
description="Tuple of bounded scalar diagnostic entries",
|
|
2667
|
+
)
|
|
2668
|
+
|
|
2669
|
+
@model_validator(mode="after")
|
|
2670
|
+
def _field_keys_unique(self) -> DiagnosticMetadata:
|
|
2671
|
+
keys = [field.key for field in self.fields]
|
|
2672
|
+
if len(set(keys)) != len(keys):
|
|
2673
|
+
raise ValueError("Diagnostic field keys must be unique")
|
|
2674
|
+
return self
|
|
2675
|
+
|
|
2676
|
+
|
|
2677
|
+
class TerminalResultArtifact(BaseModel):
|
|
2678
|
+
"""Validated ``terminal_result.json`` artifact payload."""
|
|
2679
|
+
|
|
2680
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
2681
|
+
|
|
2682
|
+
schema_version: Literal["1.0"]
|
|
2683
|
+
request_id: str
|
|
2684
|
+
run_id: str
|
|
2685
|
+
stage: StageIdentity
|
|
2686
|
+
terminal_result: str
|
|
2687
|
+
result_class: Literal[
|
|
2688
|
+
ExecutionResultClass.DOMAIN_TERMINAL,
|
|
2689
|
+
ExecutionResultClass.DOMAIN_REJECTED,
|
|
2690
|
+
]
|
|
2691
|
+
summary_artifact_paths: Tuple[str, ...] = Field(default_factory=tuple)
|
|
2692
|
+
compiled_harness_sha256: str
|
|
2693
|
+
terminal_certainty: TerminalCertainty = TerminalCertainty.COMMITTED
|
|
2694
|
+
|
|
2695
|
+
@field_validator("request_id", "run_id", "terminal_result")
|
|
2696
|
+
@classmethod
|
|
2697
|
+
def _strings_nonblank(cls, value: str, info: Any) -> str:
|
|
2698
|
+
if not value.strip():
|
|
2699
|
+
raise ValueError(f"{info.field_name} must be a non-empty string")
|
|
2700
|
+
return value
|
|
2701
|
+
|
|
2702
|
+
@field_validator("summary_artifact_paths")
|
|
2703
|
+
@classmethod
|
|
2704
|
+
def _summary_paths_relative(cls, value: Tuple[str, ...]) -> Tuple[str, ...]:
|
|
2705
|
+
seen: set[str] = set()
|
|
2706
|
+
for item in value:
|
|
2707
|
+
if not item.strip():
|
|
2708
|
+
raise ValueError("summary_artifact_paths values must be non-empty")
|
|
2709
|
+
path = Path(item)
|
|
2710
|
+
if path.is_absolute() or ".." in path.parts:
|
|
2711
|
+
raise ValueError(
|
|
2712
|
+
"summary_artifact_paths values must be safe relative paths"
|
|
2713
|
+
)
|
|
2714
|
+
if item in seen:
|
|
2715
|
+
raise ValueError("summary_artifact_paths values must be unique")
|
|
2716
|
+
seen.add(item)
|
|
2717
|
+
return value
|
|
2718
|
+
|
|
2719
|
+
@field_validator("compiled_harness_sha256")
|
|
2720
|
+
@classmethod
|
|
2721
|
+
def _compiled_harness_sha256_valid(cls, value: str) -> str:
|
|
2722
|
+
if not re.fullmatch(r"[0-9a-f]{64}", value):
|
|
2723
|
+
raise ValueError(
|
|
2724
|
+
"compiled_harness_sha256 must be exactly 64 lowercase hex characters"
|
|
2725
|
+
)
|
|
2726
|
+
return value
|
|
2727
|
+
|
|
2728
|
+
|
|
2729
|
+
class ExecutionSummaryArtifact(BaseModel):
|
|
2730
|
+
"""Validated ``execution_summary.json`` artifact payload."""
|
|
2731
|
+
|
|
2732
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
2733
|
+
|
|
2734
|
+
schema_version: Literal["1.0"]
|
|
2735
|
+
request_id: str
|
|
2736
|
+
run_id: str
|
|
2737
|
+
stage: StageIdentity
|
|
2738
|
+
status: ExecutionStatus
|
|
2739
|
+
result_class: ExecutionResultClass
|
|
2740
|
+
diagnostic_error_code: str | None = None
|
|
2741
|
+
terminal_certainty: TerminalCertainty = TerminalCertainty.NOT_APPLICABLE
|
|
2742
|
+
|
|
2743
|
+
@field_validator("request_id", "run_id", "diagnostic_error_code")
|
|
2744
|
+
@classmethod
|
|
2745
|
+
def _optional_strings_nonblank(cls, value: str | None, info: Any) -> str | None:
|
|
2746
|
+
if value is not None and not value.strip():
|
|
2747
|
+
raise ValueError(f"{info.field_name} must be a non-empty string")
|
|
2748
|
+
return value
|
|
2749
|
+
|
|
2750
|
+
|
|
2751
|
+
class MetricsArtifact(BaseModel):
|
|
2752
|
+
"""Validated ``metrics.json`` artifact payload."""
|
|
2753
|
+
|
|
2754
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
2755
|
+
|
|
2756
|
+
schema_version: Literal["1.0"]
|
|
2757
|
+
request_id: str
|
|
2758
|
+
run_id: str
|
|
2759
|
+
session_id: str | None = None
|
|
2760
|
+
status: GuardedSessionStatus | ExecutionStatus
|
|
2761
|
+
usage: UsageMetadata | None = None
|
|
2762
|
+
|
|
2763
|
+
@field_validator("request_id", "run_id", "session_id")
|
|
2764
|
+
@classmethod
|
|
2765
|
+
def _optional_strings_nonblank(cls, value: str | None, info: Any) -> str | None:
|
|
2766
|
+
if value is not None and not value.strip():
|
|
2767
|
+
raise ValueError(f"{info.field_name} must be a non-empty string")
|
|
2768
|
+
return value
|
|
2769
|
+
|
|
2770
|
+
|
|
2771
|
+
class DiagnosticArtifact(BaseModel):
|
|
2772
|
+
"""Validated sanitized ``diagnostic.json`` artifact payload."""
|
|
2773
|
+
|
|
2774
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
2775
|
+
|
|
2776
|
+
schema_version: Literal["1.0"]
|
|
2777
|
+
diagnostic: DiagnosticMetadata
|
|
2778
|
+
|
|
2779
|
+
|
|
2780
|
+
class ArtifactManifestEntry(BaseModel):
|
|
2781
|
+
"""Single validated artifact entry in ``artifact_manifest.json``."""
|
|
2782
|
+
|
|
2783
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
2784
|
+
|
|
2785
|
+
artifact_id: str
|
|
2786
|
+
path: str
|
|
2787
|
+
media_type: str
|
|
2788
|
+
byte_size: int = Field(ge=0)
|
|
2789
|
+
sha256_hex: str
|
|
2790
|
+
complete: bool
|
|
2791
|
+
producer: str
|
|
2792
|
+
failure_code: str | None = Field(
|
|
2793
|
+
default=None,
|
|
2794
|
+
description="Stable failure code when this artifact is incomplete",
|
|
2795
|
+
)
|
|
2796
|
+
|
|
2797
|
+
@field_validator("artifact_id", "path", "media_type", "producer")
|
|
2798
|
+
@classmethod
|
|
2799
|
+
def _strings_nonblank(cls, value: str, info: Any) -> str:
|
|
2800
|
+
if not value.strip():
|
|
2801
|
+
raise ValueError(f"{info.field_name} must be a non-empty string")
|
|
2802
|
+
return value
|
|
2803
|
+
|
|
2804
|
+
@field_validator("failure_code")
|
|
2805
|
+
@classmethod
|
|
2806
|
+
def _failure_code_nonblank(cls, value: str | None) -> str | None:
|
|
2807
|
+
return None if value is None else _nonblank(value, "failure_code")
|
|
2808
|
+
|
|
2809
|
+
@field_validator("path")
|
|
2810
|
+
@classmethod
|
|
2811
|
+
def _path_relative_safe(cls, value: str) -> str:
|
|
2812
|
+
path = Path(value)
|
|
2813
|
+
if path.is_absolute() or ".." in path.parts:
|
|
2814
|
+
raise ValueError("manifest artifact paths must be safe relative paths")
|
|
2815
|
+
return value
|
|
2816
|
+
|
|
2817
|
+
@field_validator("sha256_hex")
|
|
2818
|
+
@classmethod
|
|
2819
|
+
def _sha256_valid(cls, value: str) -> str:
|
|
2820
|
+
if not re.fullmatch(r"[0-9a-f]{64}", value):
|
|
2821
|
+
raise ValueError("sha256_hex must be exactly 64 lowercase hex characters")
|
|
2822
|
+
return value
|
|
2823
|
+
|
|
2824
|
+
@model_validator(mode="after")
|
|
2825
|
+
def _completion_failure_consistent(self) -> ArtifactManifestEntry:
|
|
2826
|
+
if self.complete and self.failure_code is not None:
|
|
2827
|
+
raise ValueError("complete artifacts must not include failure_code")
|
|
2828
|
+
if not self.complete and self.failure_code is None:
|
|
2829
|
+
raise ValueError("incomplete artifacts require failure_code")
|
|
2830
|
+
return self
|
|
2831
|
+
|
|
2832
|
+
|
|
2833
|
+
class ArtifactManifestArtifact(BaseModel):
|
|
2834
|
+
"""Validated ``artifact_manifest.json`` payload."""
|
|
2835
|
+
|
|
2836
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
2837
|
+
|
|
2838
|
+
schema_version: Literal["1.0"]
|
|
2839
|
+
request_id: str
|
|
2840
|
+
run_id: str
|
|
2841
|
+
artifacts: Tuple[ArtifactManifestEntry, ...]
|
|
2842
|
+
|
|
2843
|
+
@field_validator("request_id", "run_id")
|
|
2844
|
+
@classmethod
|
|
2845
|
+
def _strings_nonblank(cls, value: str, info: Any) -> str:
|
|
2846
|
+
if not value.strip():
|
|
2847
|
+
raise ValueError(f"{info.field_name} must be a non-empty string")
|
|
2848
|
+
return value
|
|
2849
|
+
|
|
2850
|
+
@field_validator("artifacts")
|
|
2851
|
+
@classmethod
|
|
2852
|
+
def _artifact_ids_unique(
|
|
2853
|
+
cls, value: Tuple[ArtifactManifestEntry, ...]
|
|
2854
|
+
) -> Tuple[ArtifactManifestEntry, ...]:
|
|
2855
|
+
artifact_ids = [entry.artifact_id for entry in value]
|
|
2856
|
+
if len(set(artifact_ids)) != len(artifact_ids):
|
|
2857
|
+
raise ValueError("manifest artifact_id values must be unique")
|
|
2858
|
+
if "artifact_manifest" in artifact_ids:
|
|
2859
|
+
raise ValueError("artifact_manifest must not reference itself")
|
|
2860
|
+
return value
|