mostlyright-data 0.9.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (314) hide show
  1. mostlyright/data_harness/__init__.py +158 -0
  2. mostlyright/data_harness/acquisition/__init__.py +55 -0
  3. mostlyright/data_harness/acquisition/http.py +2773 -0
  4. mostlyright/data_harness/acquisition/parsing.py +809 -0
  5. mostlyright/data_harness/acquisition/ranges.py +495 -0
  6. mostlyright/data_harness/acquisition/result_download.py +360 -0
  7. mostlyright/data_harness/acquisition/retention_admission.py +248 -0
  8. mostlyright/data_harness/acquisition/sandbox.py +4888 -0
  9. mostlyright/data_harness/acquisition/url_policy.py +530 -0
  10. mostlyright/data_harness/agent_runtime.py +2743 -0
  11. mostlyright/data_harness/assets/logo-ink.svg +31 -0
  12. mostlyright/data_harness/backends/__init__.py +28 -0
  13. mostlyright/data_harness/backends/pandas_backend.py +350 -0
  14. mostlyright/data_harness/backends/polars_backend.py +366 -0
  15. mostlyright/data_harness/backends/protocol.py +124 -0
  16. mostlyright/data_harness/backends/reference.py +83 -0
  17. mostlyright/data_harness/backends/registry.py +55 -0
  18. mostlyright/data_harness/backends/restrictions.py +126 -0
  19. mostlyright/data_harness/canonical.py +333 -0
  20. mostlyright/data_harness/catalog_job.py +625 -0
  21. mostlyright/data_harness/cli.py +5398 -0
  22. mostlyright/data_harness/contracts.py +53 -0
  23. mostlyright/data_harness/coordinator.py +1307 -0
  24. mostlyright/data_harness/deploy.py +924 -0
  25. mostlyright/data_harness/deploy_target.py +312 -0
  26. mostlyright/data_harness/deployment_evidence.py +1067 -0
  27. mostlyright/data_harness/event_presentation.py +576 -0
  28. mostlyright/data_harness/events.py +2152 -0
  29. mostlyright/data_harness/fast_delimited.py +239 -0
  30. mostlyright/data_harness/fleet.py +237 -0
  31. mostlyright/data_harness/formats.py +236 -0
  32. mostlyright/data_harness/governors.py +1163 -0
  33. mostlyright/data_harness/hosted_bootstrap.py +972 -0
  34. mostlyright/data_harness/hosted_crawler.py +1115 -0
  35. mostlyright/data_harness/hosted_crawler_container_smoke.py +351 -0
  36. mostlyright/data_harness/hosted_crawler_fetch.py +423 -0
  37. mostlyright/data_harness/hosted_crawler_job.py +1277 -0
  38. mostlyright/data_harness/hosted_crawler_protocol.py +676 -0
  39. mostlyright/data_harness/hosted_dataset.py +1500 -0
  40. mostlyright/data_harness/hosted_deploy.py +3037 -0
  41. mostlyright/data_harness/hosted_handoff.py +62 -0
  42. mostlyright/data_harness/hosted_ingestion_contract.py +504 -0
  43. mostlyright/data_harness/hosted_ingestion_job.py +356 -0
  44. mostlyright/data_harness/hosted_ingestion_job_smoke.py +40 -0
  45. mostlyright/data_harness/hosted_session_container_smoke.py +194 -0
  46. mostlyright/data_harness/hosted_session_worker.py +3554 -0
  47. mostlyright/data_harness/hosted_session_worker_job_smoke.py +46 -0
  48. mostlyright/data_harness/hosted_worker.py +6784 -0
  49. mostlyright/data_harness/ingestion/__init__.py +56 -0
  50. mostlyright/data_harness/ingestion/contracts.py +461 -0
  51. mostlyright/data_harness/ingestion/faults.py +42 -0
  52. mostlyright/data_harness/ingestion/gcs_store.py +1162 -0
  53. mostlyright/data_harness/ingestion/spool.py +130 -0
  54. mostlyright/data_harness/ingestion/store.py +885 -0
  55. mostlyright/data_harness/key_seam.py +434 -0
  56. mostlyright/data_harness/linux_process_boundary.py +262 -0
  57. mostlyright/data_harness/local_contracts.py +2880 -0
  58. mostlyright/data_harness/local_search/__init__.py +5 -0
  59. mostlyright/data_harness/local_search/build_index.py +1087 -0
  60. mostlyright/data_harness/local_search/contracts.py +920 -0
  61. mostlyright/data_harness/local_search/query_trace.py +266 -0
  62. mostlyright/data_harness/local_search/retrieval.py +700 -0
  63. mostlyright/data_harness/local_search/sealed.py +474 -0
  64. mostlyright/data_harness/local_search/service.py +784 -0
  65. mostlyright/data_harness/nbrender/CONTRACT.md +212 -0
  66. mostlyright/data_harness/nbrender/__init__.py +12 -0
  67. mostlyright/data_harness/nbrender/chrome.py +359 -0
  68. mostlyright/data_harness/nbrender/code_body.py +266 -0
  69. mostlyright/data_harness/nbrender/document.py +407 -0
  70. mostlyright/data_harness/nbrender/frame.py +275 -0
  71. mostlyright/data_harness/nbrender/interactive.py +337 -0
  72. mostlyright/data_harness/nbrender/markdown_body.py +477 -0
  73. mostlyright/data_harness/nbrender/mr_components.py +134 -0
  74. mostlyright/data_harness/nbrender/outputs_data.py +595 -0
  75. mostlyright/data_harness/nbrender/outputs_rich.py +906 -0
  76. mostlyright/data_harness/nbrender/outputs_source.py +260 -0
  77. mostlyright/data_harness/nbrender/outputs_stage.py +176 -0
  78. mostlyright/data_harness/nbrender/outputs_text.py +400 -0
  79. mostlyright/data_harness/nbrender/parse.py +394 -0
  80. mostlyright/data_harness/nbrender/status.py +40 -0
  81. mostlyright/data_harness/nbrender/tokens.py +1295 -0
  82. mostlyright/data_harness/notebook.py +1710 -0
  83. mostlyright/data_harness/offline.py +2049 -0
  84. mostlyright/data_harness/operation_registry.py +1007 -0
  85. mostlyright/data_harness/operator_setup.py +239 -0
  86. mostlyright/data_harness/pipeline.py +6428 -0
  87. mostlyright/data_harness/plan_graph.py +2026 -0
  88. mostlyright/data_harness/preparation/__init__.py +104 -0
  89. mostlyright/data_harness/preparation/contracts.py +1017 -0
  90. mostlyright/data_harness/preparation/engine.py +221 -0
  91. mostlyright/data_harness/preparation/errors.py +14 -0
  92. mostlyright/data_harness/preparation/gates.py +751 -0
  93. mostlyright/data_harness/preparation/joins.py +574 -0
  94. mostlyright/data_harness/preparation/profile.py +384 -0
  95. mostlyright/data_harness/preparation/table.py +217 -0
  96. mostlyright/data_harness/preparation/transforms.py +568 -0
  97. mostlyright/data_harness/progress_events.py +534 -0
  98. mostlyright/data_harness/readers/__init__.py +46 -0
  99. mostlyright/data_harness/readers/containers.py +963 -0
  100. mostlyright/data_harness/readers/contracts.py +542 -0
  101. mostlyright/data_harness/readers/delimited.py +257 -0
  102. mostlyright/data_harness/readers/grib2/__init__.py +33 -0
  103. mostlyright/data_harness/readers/grib2/admission.py +722 -0
  104. mostlyright/data_harness/readers/grib2/decode.py +1009 -0
  105. mostlyright/data_harness/readers/grib2/geometry.py +1133 -0
  106. mostlyright/data_harness/readers/grib2/portable_math.py +501 -0
  107. mostlyright/data_harness/readers/json_tabular.py +485 -0
  108. mostlyright/data_harness/readers/registry.py +514 -0
  109. mostlyright/data_harness/readers/samples/README.md +110 -0
  110. mostlyright/data_harness/readers/samples/archive.gzip/1.0.0/cities_one_stream/cities.csv.gz +0 -0
  111. mostlyright/data_harness/readers/samples/archive.gzip/1.0.0/cities_one_stream/expected.json +24 -0
  112. mostlyright/data_harness/readers/samples/archive.gzip/1.1.0/cities_one_stream/cities.csv.gz +0 -0
  113. mostlyright/data_harness/readers/samples/archive.gzip/1.1.0/cities_one_stream/expected.json +24 -0
  114. mostlyright/data_harness/readers/samples/archive.tar/1.0.0/cities_beside_a_directory_entry/cities.tar +0 -0
  115. mostlyright/data_harness/readers/samples/archive.tar/1.0.0/cities_beside_a_directory_entry/expected.json +24 -0
  116. mostlyright/data_harness/readers/samples/archive.tar/1.1.0/cities_beside_a_directory_entry/cities.tar +0 -0
  117. mostlyright/data_harness/readers/samples/archive.tar/1.1.0/cities_beside_a_directory_entry/expected.json +24 -0
  118. mostlyright/data_harness/readers/samples/archive.zip/1.0.0/cities_beside_a_second_member/cities.zip +0 -0
  119. mostlyright/data_harness/readers/samples/archive.zip/1.0.0/cities_beside_a_second_member/expected.json +25 -0
  120. mostlyright/data_harness/readers/samples/archive.zip/1.1.0/dwd_semicolon_station_member/dwd-station.zip +0 -0
  121. mostlyright/data_harness/readers/samples/archive.zip/1.1.0/dwd_semicolon_station_member/expected.json +25 -0
  122. mostlyright/data_harness/readers/samples/archive.zip/1.2.0/dwd_semicolon_station_member/dwd-station.zip +0 -0
  123. mostlyright/data_harness/readers/samples/archive.zip/1.2.0/dwd_semicolon_station_member/expected.json +25 -0
  124. mostlyright/data_harness/readers/samples/delimited_text/1.0.0/an_ordinary_comma_separated_table/cities.csv +3 -0
  125. mostlyright/data_harness/readers/samples/delimited_text/1.0.0/an_ordinary_comma_separated_table/expected.json +23 -0
  126. mostlyright/data_harness/readers/samples/delimited_text/1.0.0/quoted_fields_holding_the_delimiter/cities.tsv +5 -0
  127. mostlyright/data_harness/readers/samples/delimited_text/1.0.0/quoted_fields_holding_the_delimiter/expected.json +25 -0
  128. mostlyright/data_harness/readers/samples/delimited_text/1.1.0/an_hourly_observation_table_served_as_plain_text/expected.json +30 -0
  129. mostlyright/data_harness/readers/samples/delimited_text/1.1.0/an_hourly_observation_table_served_as_plain_text/observations.csv +5 -0
  130. mostlyright/data_harness/readers/samples/json.tabular/1.0.0/nested_hourly_observations/expected.json +44 -0
  131. mostlyright/data_harness/readers/samples/json.tabular/1.0.0/nested_hourly_observations/stations.json +1 -0
  132. mostlyright/data_harness/readers/samples/json.tabular/1.1.0/an_observation_stream_served_as_plain_text/expected.json +48 -0
  133. mostlyright/data_harness/readers/samples/json.tabular/1.1.0/an_observation_stream_served_as_plain_text/observations.ndjson +4 -0
  134. mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/an_ordinary_table_beside_a_second_sheet/cities.xlsx +0 -0
  135. mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/an_ordinary_table_beside_a_second_sheet/expected.json +24 -0
  136. mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/shares_the_workbook_had_already_computed/expected.json +27 -0
  137. mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/shares_the_workbook_had_already_computed/shares.xlsx +0 -0
  138. mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.1.0/shares_the_workbook_had_already_computed/expected.json +27 -0
  139. mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.1.0/shares_the_workbook_had_already_computed/shares.xlsx +0 -0
  140. mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/README.md +20 -0
  141. mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/gfs_2m_temperature/expected.json +55 -0
  142. mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/gfs_2m_temperature/gfs-2m-temperature.grib2 +0 -0
  143. mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_2m_temperature/expected.json +54 -0
  144. mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_2m_temperature/hrrr-2m-temperature.grib2 +0 -0
  145. mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_categorical_rain/expected.json +54 -0
  146. mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_categorical_rain/hrrr-categorical-rain.grib2 +0 -0
  147. mostlyright/data_harness/readers/samples/weather.grib2/2.0.0/hrrr_2m_temperature/expected.json +54 -0
  148. mostlyright/data_harness/readers/samples/weather.grib2/2.0.0/hrrr_2m_temperature/hrrr-2m-temperature.grib2 +0 -0
  149. mostlyright/data_harness/readers/samples.py +582 -0
  150. mostlyright/data_harness/readers/spreadsheet.py +803 -0
  151. mostlyright/data_harness/readers/tabular.py +510 -0
  152. mostlyright/data_harness/recipe.py +5321 -0
  153. mostlyright/data_harness/repair/__init__.py +78 -0
  154. mostlyright/data_harness/repair/adapters.py +274 -0
  155. mostlyright/data_harness/repair/contracts.py +872 -0
  156. mostlyright/data_harness/repair/coordinator.py +1099 -0
  157. mostlyright/data_harness/repair/errors.py +16 -0
  158. mostlyright/data_harness/review.py +2533 -0
  159. mostlyright/data_harness/rowset.py +283 -0
  160. mostlyright/data_harness/serving.py +1975 -0
  161. mostlyright/data_harness/serving_edge.py +590 -0
  162. mostlyright/data_harness/serving_http.py +1031 -0
  163. mostlyright/data_harness/session_probes.py +759 -0
  164. mostlyright/data_harness/signing.py +101 -0
  165. mostlyright/data_harness/source_discovery.py +898 -0
  166. mostlyright/data_harness/sources/__init__.py +209 -0
  167. mostlyright/data_harness/sources/_adapter_steps.py +213 -0
  168. mostlyright/data_harness/sources/adapters.py +1214 -0
  169. mostlyright/data_harness/sources/cadence.py +1428 -0
  170. mostlyright/data_harness/sources/cadence_emission.py +453 -0
  171. mostlyright/data_harness/sources/cadence_history.py +546 -0
  172. mostlyright/data_harness/sources/catalog/__init__.py +17 -0
  173. mostlyright/data_harness/sources/catalog/admission.py +477 -0
  174. mostlyright/data_harness/sources/catalog/authoring.py +1701 -0
  175. mostlyright/data_harness/sources/catalog/authoring_policy.py +701 -0
  176. mostlyright/data_harness/sources/catalog/authoring_shards.py +1217 -0
  177. mostlyright/data_harness/sources/catalog/bounded_io.py +231 -0
  178. mostlyright/data_harness/sources/catalog/channel.py +523 -0
  179. mostlyright/data_harness/sources/catalog/channel_client.py +296 -0
  180. mostlyright/data_harness/sources/catalog/contracts.py +825 -0
  181. mostlyright/data_harness/sources/catalog/coverage.py +137 -0
  182. mostlyright/data_harness/sources/catalog/delta.py +1340 -0
  183. mostlyright/data_harness/sources/catalog/embedding.py +532 -0
  184. mostlyright/data_harness/sources/catalog/entry_v2.py +1182 -0
  185. mostlyright/data_harness/sources/catalog/fill.py +3889 -0
  186. mostlyright/data_harness/sources/catalog/fill_partitions.py +459 -0
  187. mostlyright/data_harness/sources/catalog/fill_staging.py +1105 -0
  188. mostlyright/data_harness/sources/catalog/gating.py +374 -0
  189. mostlyright/data_harness/sources/catalog/generation_receipt.py +1607 -0
  190. mostlyright/data_harness/sources/catalog/harvest/__init__.py +7 -0
  191. mostlyright/data_harness/sources/catalog/harvest/ckan.py +384 -0
  192. mostlyright/data_harness/sources/catalog/harvest/datagov_v4.py +798 -0
  193. mostlyright/data_harness/sources/catalog/harvest/protocol.py +964 -0
  194. mostlyright/data_harness/sources/catalog/harvest/sdmx.py +445 -0
  195. mostlyright/data_harness/sources/catalog/harvest/stac.py +384 -0
  196. mostlyright/data_harness/sources/catalog/health.py +447 -0
  197. mostlyright/data_harness/sources/catalog/hosted_catalog.py +105 -0
  198. mostlyright/data_harness/sources/catalog/identity_history.py +1549 -0
  199. mostlyright/data_harness/sources/catalog/neural.py +1618 -0
  200. mostlyright/data_harness/sources/catalog/packed_catalog.py +2345 -0
  201. mostlyright/data_harness/sources/catalog/packed_retrieval.py +1517 -0
  202. mostlyright/data_harness/sources/catalog/packed_writer.py +2802 -0
  203. mostlyright/data_harness/sources/catalog/query_trace.py +1037 -0
  204. mostlyright/data_harness/sources/catalog/recommend.py +171 -0
  205. mostlyright/data_harness/sources/catalog/retrieval.py +230 -0
  206. mostlyright/data_harness/sources/catalog/retrieval_manifest.py +995 -0
  207. mostlyright/data_harness/sources/catalog/rights_decisions.py +254 -0
  208. mostlyright/data_harness/sources/catalog/sealed.py +560 -0
  209. mostlyright/data_harness/sources/catalog/search.py +230 -0
  210. mostlyright/data_harness/sources/catalog/streaming_delta.py +1097 -0
  211. mostlyright/data_harness/sources/catalog/update.py +891 -0
  212. mostlyright/data_harness/sources/collections.py +815 -0
  213. mostlyright/data_harness/sources/contracts.py +2223 -0
  214. mostlyright/data_harness/sources/deletion.py +761 -0
  215. mostlyright/data_harness/sources/fitness.py +162 -0
  216. mostlyright/data_harness/sources/governance.py +163 -0
  217. mostlyright/data_harness/sources/hosted.py +173 -0
  218. mostlyright/data_harness/sources/integration.py +218 -0
  219. mostlyright/data_harness/sources/range_reader.py +418 -0
  220. mostlyright/data_harness/sources/registry.py +514 -0
  221. mostlyright/data_harness/sources/rights_rule.py +59 -0
  222. mostlyright/data_harness/sources/source_cadence_vectors.v1.json +1 -0
  223. mostlyright/data_harness/sources/sports.py +521 -0
  224. mostlyright/data_harness/sources/stream.py +524 -0
  225. mostlyright/data_harness/sources/stream_connector.py +418 -0
  226. mostlyright/data_harness/sources/stream_recorder.py +1404 -0
  227. mostlyright/data_harness/studio_boundary.py +2019 -0
  228. mostlyright/data_harness/thin/__init__.py +37 -0
  229. mostlyright/data_harness/thin/acquire.py +1137 -0
  230. mostlyright/data_harness/thin/acquire_cancel.py +579 -0
  231. mostlyright/data_harness/thin/approvals.py +617 -0
  232. mostlyright/data_harness/thin/commands.py +406 -0
  233. mostlyright/data_harness/thin/download.py +194 -0
  234. mostlyright/data_harness/thin/narrative.py +589 -0
  235. mostlyright/data_harness/thin/parity.py +1070 -0
  236. mostlyright/data_harness/thin/propose.py +2759 -0
  237. mostlyright/data_harness/thin/research.py +1663 -0
  238. mostlyright/data_harness/thin/router.py +924 -0
  239. mostlyright/data_harness/thin/runs.py +519 -0
  240. mostlyright/data_harness/thin/session.py +281 -0
  241. mostlyright/data_harness/thin/stream.py +501 -0
  242. mostlyright/data_harness/thin/transport.py +187 -0
  243. mostlyright/data_harness/thin/vocabulary.py +368 -0
  244. mostlyright/data_harness/thin/workers.py +164 -0
  245. mostlyright/data_harness/ucum/TABLE-PIN.json +40 -0
  246. mostlyright/data_harness/ucum/ucum-subset.v1.json +632 -0
  247. mostlyright/data_harness/unit_flow.py +927 -0
  248. mostlyright/data_harness/units.py +572 -0
  249. mostlyright/data_harness/ux/__init__.py +9 -0
  250. mostlyright/data_harness/ux/approve.py +485 -0
  251. mostlyright/data_harness/ux/author_yaml.py +597 -0
  252. mostlyright/data_harness/ux/cloud_auth.py +447 -0
  253. mostlyright/data_harness/ux/commands/__init__.py +260 -0
  254. mostlyright/data_harness/ux/commands/approve.py +136 -0
  255. mostlyright/data_harness/ux/commands/auth.py +744 -0
  256. mostlyright/data_harness/ux/commands/author.py +79 -0
  257. mostlyright/data_harness/ux/commands/catalog_author.py +403 -0
  258. mostlyright/data_harness/ux/commands/catalog_fill.py +523 -0
  259. mostlyright/data_harness/ux/commands/catalog_harvest.py +545 -0
  260. mostlyright/data_harness/ux/commands/catalog_publish.py +1838 -0
  261. mostlyright/data_harness/ux/commands/catalog_search.py +71 -0
  262. mostlyright/data_harness/ux/commands/catalog_update.py +437 -0
  263. mostlyright/data_harness/ux/commands/deploy.py +134 -0
  264. mostlyright/data_harness/ux/commands/deploy_dataset.py +98 -0
  265. mostlyright/data_harness/ux/commands/deploy_plan.py +105 -0
  266. mostlyright/data_harness/ux/commands/deploy_status.py +104 -0
  267. mostlyright/data_harness/ux/commands/diff.py +74 -0
  268. mostlyright/data_harness/ux/commands/index.py +84 -0
  269. mostlyright/data_harness/ux/commands/inventory.py +47 -0
  270. mostlyright/data_harness/ux/commands/list_builds.py +143 -0
  271. mostlyright/data_harness/ux/commands/login.py +63 -0
  272. mostlyright/data_harness/ux/commands/peek.py +236 -0
  273. mostlyright/data_harness/ux/commands/plan_check.py +90 -0
  274. mostlyright/data_harness/ux/commands/preflight.py +97 -0
  275. mostlyright/data_harness/ux/commands/record.py +107 -0
  276. mostlyright/data_harness/ux/commands/review_setup.py +47 -0
  277. mostlyright/data_harness/ux/commands/search.py +440 -0
  278. mostlyright/data_harness/ux/commands/show.py +61 -0
  279. mostlyright/data_harness/ux/commands/whoami.py +37 -0
  280. mostlyright/data_harness/ux/credential_native.py +551 -0
  281. mostlyright/data_harness/ux/credential_store.py +1055 -0
  282. mostlyright/data_harness/ux/credentials.py +631 -0
  283. mostlyright/data_harness/ux/diffing.py +444 -0
  284. mostlyright/data_harness/ux/headline.py +671 -0
  285. mostlyright/data_harness/ux/hosted_acquisition.py +974 -0
  286. mostlyright/data_harness/ux/hosted_run_status.py +619 -0
  287. mostlyright/data_harness/ux/inventory.py +427 -0
  288. mostlyright/data_harness/ux/local_review.py +375 -0
  289. mostlyright/data_harness/ux/login.py +691 -0
  290. mostlyright/data_harness/ux/path_kind.py +147 -0
  291. mostlyright/data_harness/ux/peek.py +1000 -0
  292. mostlyright/data_harness/ux/plain_file.py +178 -0
  293. mostlyright/data_harness/ux/plan_check.py +311 -0
  294. mostlyright/data_harness/ux/preflight.py +918 -0
  295. mostlyright/data_harness/ux/readers.py +1124 -0
  296. mostlyright/data_harness/ux/remediation.py +2195 -0
  297. mostlyright/data_harness/ux/render.py +657 -0
  298. mostlyright/data_harness/ux/workload.py +1077 -0
  299. mostlyright/data_harness/viewer.py +3713 -0
  300. mostlyright/data_harness/visual_run/__init__.py +83 -0
  301. mostlyright/data_harness/visual_run/authoring.py +235 -0
  302. mostlyright/data_harness/visual_run/contracts.py +673 -0
  303. mostlyright/data_harness/visual_run/materialize.py +486 -0
  304. mostlyright/data_harness/visual_run/observations.py +874 -0
  305. mostlyright/data_harness/visual_run/query.py +259 -0
  306. mostlyright/data_harness/visual_run/reducer.py +280 -0
  307. mostlyright/data_harness/visual_run/sdk.py +892 -0
  308. mostlyright/data_harness/visual_run/store.py +584 -0
  309. mostlyright/data_harness/visual_run/transport.py +239 -0
  310. mostlyright/data_harness/watch.py +2999 -0
  311. mostlyright_data-0.9.0.dist-info/METADATA +607 -0
  312. mostlyright_data-0.9.0.dist-info/RECORD +314 -0
  313. mostlyright_data-0.9.0.dist-info/WHEEL +4 -0
  314. mostlyright_data-0.9.0.dist-info/entry_points.txt +12 -0
@@ -0,0 +1,2743 @@
1
+ """Provider-neutral, deterministic runtime for bounded data-agent proposals.
2
+
3
+ The runtime deliberately has no provider-package dependency. Provider adapters receive only a
4
+ strict, versioned request and a closed tool policy. They return untrusted structured data plus
5
+ integer accounting; this module validates both before returning anything to a caller.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ import hashlib
12
+ import json
13
+ import re
14
+ import stat
15
+ import threading
16
+ from collections.abc import Callable, Mapping, Sequence
17
+ from copy import deepcopy
18
+ from dataclasses import dataclass
19
+ from datetime import UTC, date, datetime
20
+ from pathlib import Path
21
+ from typing import Any, Protocol, runtime_checkable
22
+
23
+ AGENT_REQUEST_VERSION = "agent-request.v1"
24
+ AGENT_RESPONSE_VERSION = "agent-response.v2"
25
+ AGENT_EVIDENCE_VERSION = "agent-call-evidence.v1"
26
+ AGENT_RUNTIME_VERSION = "agent-runtime.v1"
27
+ REPLAY_CATALOG_VERSION = "agent-replay-catalog.v1"
28
+ TOOL_POLICY_VERSION = "agent-tools.v1"
29
+ SEMANTIC_REVIEW_INPUT_VERSION = "semantic-reviewer-input.v2"
30
+ SEMANTIC_REVIEW_OUTPUT_VERSION = "semantic-reviewer-output.v2"
31
+ MAX_ACCOUNTING_INTEGER = (1 << 63) - 1
32
+
33
+ # One role per (input, output) schema pair, except semantic_reviewer, which accepts two live
34
+ # input schemas rather than being split into two roles; the ROLE_PAYLOAD_SCHEMAS comment below
35
+ # is the detail.
36
+ AGENT_ROLES = frozenset(
37
+ {
38
+ "question_framer",
39
+ "feasibility_analyst",
40
+ "data_scout",
41
+ "source_auditor",
42
+ "plan_proposer",
43
+ "semantic_reviewer",
44
+ "repair_proposer",
45
+ }
46
+ )
47
+ # Every role uses an (input, output) schema pair derived from its own name. The semantic
48
+ # reviewer is the one exception: its output schema is v2, and its input accepts two live
49
+ # schemas. The v1 entry below is the identifier-only payload checked by _validate_role_input;
50
+ # SEMANTIC_REVIEW_INPUT_VERSION names the v2 payload, which additionally requires the full
51
+ # candidate evidence bundle and its digests. _validate_role_payload dispatches on whichever
52
+ # input version a request declares, so this table names only the v1 input.
53
+ ROLE_PAYLOAD_SCHEMAS: Mapping[str, tuple[str, str]] = {
54
+ role: (
55
+ f"{role.replace('_', '-')}-input.v1",
56
+ f"{role.replace('_', '-')}-output.v1",
57
+ )
58
+ for role in sorted(AGENT_ROLES)
59
+ }
60
+ ROLE_PAYLOAD_SCHEMAS = {
61
+ **ROLE_PAYLOAD_SCHEMAS,
62
+ "data_scout": (
63
+ "data-scout-input.v1",
64
+ "data-scout-output.v2",
65
+ ),
66
+ "semantic_reviewer": (
67
+ "semantic-reviewer-input.v1",
68
+ SEMANTIC_REVIEW_OUTPUT_VERSION,
69
+ ),
70
+ }
71
+ _OUTCOMES = frozenset(
72
+ {
73
+ "succeeded",
74
+ "budget_exhausted_before_call",
75
+ "budget_exhausted_after_call",
76
+ "provider_exception",
77
+ "malformed_response",
78
+ "timed_out",
79
+ "cancelled",
80
+ "tool_policy_denied",
81
+ }
82
+ )
83
+ _IDENTIFIER = re.compile(r"^[a-z][a-z0-9]*(?:[-_.][a-z0-9]+)*$")
84
+ _SECRET_HANDLE = re.compile(r"^shr_[A-Za-z0-9_-]{24,120}$")
85
+ _SCOPE_IDENTIFIER = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$")
86
+ _SECRET_PURPOSES = frozenset({"provider_auth", "source_acquisition", "web_extraction"})
87
+ _SHA256 = re.compile(r"^[0-9a-f]{64}$")
88
+ _WIRE_SHA256 = re.compile(r"^sha256:[0-9a-f]{64}$")
89
+ _UUID = re.compile(r"^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$")
90
+ _FINDING_CODE = re.compile(r"^[A-Z][A-Z0-9_]{2,127}$")
91
+ _ARTIFACT_KIND = re.compile(r"^[a-z][a-z0-9_]{1,63}$")
92
+ _SEMANTIC_VERSION = re.compile(
93
+ r"^(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)"
94
+ r"(?:-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?$"
95
+ )
96
+ _CANONICAL_UTC = re.compile(
97
+ r"^[0-9]{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12][0-9]|3[01])"
98
+ r"T(?:[01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](?:\.[0-9]{1,6})?Z$"
99
+ )
100
+ _CANONICAL_DATE = re.compile(r"^[0-9]{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12][0-9]|3[01])$")
101
+ _ACCOUNTING_STATUSES = frozenset({"reported", "unavailable_reserved", "not_applicable"})
102
+ _PRE_DISPATCH_OUTCOMES = frozenset({"budget_exhausted_before_call", "tool_policy_denied"})
103
+
104
+
105
+ class AgentRuntimeError(RuntimeError):
106
+ """Base class for typed runtime failures."""
107
+
108
+
109
+ class RuntimeContractError(AgentRuntimeError, ValueError):
110
+ """A request, response, fixture, or provider accounting value is invalid."""
111
+
112
+
113
+ class ToolPolicyError(RuntimeContractError):
114
+ """An agent request asks for a capability outside the closed policy."""
115
+
116
+
117
+ class ReplayError(AgentRuntimeError):
118
+ """Base class for replay-catalog failures."""
119
+
120
+
121
+ class ReplayMissError(ReplayError):
122
+ """No replay entry has the exact canonical request identity."""
123
+
124
+
125
+ class ReplayAmbiguityError(ReplayError):
126
+ """More than one replay entry claims the same canonical request identity."""
127
+
128
+
129
+ class ReplayTamperError(ReplayError):
130
+ """A replay member no longer matches its recorded hash."""
131
+
132
+
133
+ class ReplayVersionError(ReplayError):
134
+ """A replay catalog or member uses an unsupported schema/policy version."""
135
+
136
+
137
+ class ProviderTimeoutError(TimeoutError):
138
+ """Provider adapter reports a deterministic timeout."""
139
+
140
+
141
+ @dataclass(frozen=True)
142
+ class TimingEvidence:
143
+ """Integer-only timing metadata supplied by an injected monotonic clock."""
144
+
145
+ started_micros: int
146
+ finished_micros: int
147
+ elapsed_micros: int
148
+
149
+ def __post_init__(self) -> None:
150
+ _nonnegative_int(self.started_micros, "timing.started_micros")
151
+ _nonnegative_int(self.finished_micros, "timing.finished_micros")
152
+ _nonnegative_int(self.elapsed_micros, "timing.elapsed_micros")
153
+ if self.finished_micros < self.started_micros:
154
+ raise RuntimeContractError("timing.finished_micros cannot precede started_micros")
155
+ if self.elapsed_micros != self.finished_micros - self.started_micros:
156
+ raise RuntimeContractError("timing.elapsed_micros must equal finished minus started")
157
+
158
+ def to_dict(self) -> dict[str, int]:
159
+ return {
160
+ "started_micros": self.started_micros,
161
+ "finished_micros": self.finished_micros,
162
+ "elapsed_micros": self.elapsed_micros,
163
+ }
164
+
165
+
166
+ @dataclass(frozen=True)
167
+ class Usage:
168
+ """Validated, integer-safe provider usage."""
169
+
170
+ input_tokens: int
171
+ output_tokens: int
172
+ cost_micros: int
173
+ cached_input_tokens: int = 0
174
+ reasoning_tokens: int = 0
175
+ accounting_status: str = "reported"
176
+
177
+ def __post_init__(self) -> None:
178
+ _accounting_int(self.input_tokens, "usage.input_tokens")
179
+ _accounting_int(self.output_tokens, "usage.output_tokens")
180
+ _accounting_int(self.cost_micros, "usage.cost_micros")
181
+ _accounting_int(self.cached_input_tokens, "usage.cached_input_tokens")
182
+ _accounting_int(self.reasoning_tokens, "usage.reasoning_tokens")
183
+ if self.accounting_status not in _ACCOUNTING_STATUSES:
184
+ raise RuntimeContractError("usage.accounting_status is unsupported")
185
+ if self.cached_input_tokens > self.input_tokens:
186
+ raise RuntimeContractError("usage.cached_input_tokens cannot exceed input_tokens")
187
+ if self.reasoning_tokens > self.output_tokens:
188
+ raise RuntimeContractError("usage.reasoning_tokens cannot exceed output_tokens")
189
+ _checked_add(self.input_tokens, self.output_tokens, "usage.total_tokens")
190
+
191
+ @property
192
+ def total_tokens(self) -> int:
193
+ return self.input_tokens + self.output_tokens
194
+
195
+ @property
196
+ def cost_usd_micros(self) -> int:
197
+ return self.cost_micros
198
+
199
+ def to_dict(self) -> dict[str, int]:
200
+ return {
201
+ "input_tokens": self.input_tokens,
202
+ "output_tokens": self.output_tokens,
203
+ "total_tokens": self.total_tokens,
204
+ "cost_usd_micros": self.cost_usd_micros,
205
+ "cached_input_tokens": self.cached_input_tokens,
206
+ "reasoning_tokens": self.reasoning_tokens,
207
+ "accounting_status": self.accounting_status,
208
+ }
209
+
210
+
211
+ @dataclass(frozen=True)
212
+ class SecretReference:
213
+ """A server-issued, scoped handle; it can never contain a provider storage coordinate."""
214
+
215
+ name: str
216
+ handle_id: str
217
+ workspace_id: str
218
+ connector_id: str
219
+ attempt_id: str
220
+ purpose: str
221
+
222
+ def __post_init__(self) -> None:
223
+ _identifier(self.name, "secret_reference.name")
224
+ if not isinstance(self.handle_id, str) or not _SECRET_HANDLE.fullmatch(self.handle_id):
225
+ raise RuntimeContractError("secret_reference.handle_id must be a server-issued handle")
226
+ for field_name in ("workspace_id", "connector_id", "attempt_id"):
227
+ value = getattr(self, field_name)
228
+ if not isinstance(value, str) or not _SCOPE_IDENTIFIER.fullmatch(value):
229
+ raise RuntimeContractError(
230
+ f"secret_reference.{field_name} must be a canonical scoped identifier"
231
+ )
232
+ if self.purpose not in _SECRET_PURPOSES:
233
+ raise RuntimeContractError("secret_reference.purpose is unsupported")
234
+
235
+ def to_dict(self) -> dict[str, str]:
236
+ return {
237
+ "name": self.name,
238
+ "handle_id": self.handle_id,
239
+ "workspace_id": self.workspace_id,
240
+ "connector_id": self.connector_id,
241
+ "attempt_id": self.attempt_id,
242
+ "purpose": self.purpose,
243
+ }
244
+
245
+ @classmethod
246
+ def from_value(cls, value: Any, path: str) -> SecretReference:
247
+ data = _object(value, path)
248
+ _exact_fields(
249
+ data,
250
+ {
251
+ "name",
252
+ "handle_id",
253
+ "workspace_id",
254
+ "connector_id",
255
+ "attempt_id",
256
+ "purpose",
257
+ },
258
+ path,
259
+ )
260
+ return cls(
261
+ name=_required_string(data["name"], f"{path}.name", maximum=64),
262
+ handle_id=_required_string(data["handle_id"], f"{path}.handle_id", maximum=128),
263
+ workspace_id=_required_string(
264
+ data["workspace_id"], f"{path}.workspace_id", maximum=128
265
+ ),
266
+ connector_id=_required_string(
267
+ data["connector_id"], f"{path}.connector_id", maximum=128
268
+ ),
269
+ attempt_id=_required_string(data["attempt_id"], f"{path}.attempt_id", maximum=128),
270
+ purpose=_required_string(data["purpose"], f"{path}.purpose", maximum=64),
271
+ )
272
+
273
+
274
+ @dataclass(frozen=True)
275
+ class ToolInvocation:
276
+ """A bounded invocation of a capability registered by the trusted coordinator."""
277
+
278
+ name: str
279
+ arguments: Mapping[str, Any]
280
+
281
+ def __post_init__(self) -> None:
282
+ _identifier(self.name, "tool.name")
283
+ normalized = _strict_json_object(self.arguments, "tool.arguments")
284
+ object.__setattr__(self, "arguments", normalized)
285
+
286
+ def to_dict(self) -> dict[str, Any]:
287
+ return {"name": self.name, "arguments": deepcopy(dict(self.arguments))}
288
+
289
+ @classmethod
290
+ def from_value(cls, value: Any, path: str) -> ToolInvocation:
291
+ data = _object(value, path)
292
+ _exact_fields(data, {"name", "arguments"}, path)
293
+ return cls(
294
+ name=_required_string(data["name"], f"{path}.name", maximum=64),
295
+ arguments=_object(data["arguments"], f"{path}.arguments"),
296
+ )
297
+
298
+
299
+ @dataclass(frozen=True)
300
+ class ToolCallEvidence:
301
+ """Exact digest binding for one sanctioned tool invocation."""
302
+
303
+ sequence: int
304
+ name: str
305
+ arguments_sha256: str
306
+ invocation_sha256: str
307
+
308
+ def __post_init__(self) -> None:
309
+ _positive_int(self.sequence, "tool_evidence.sequence")
310
+ _identifier(self.name, "tool_evidence.name")
311
+ _digest(self.arguments_sha256, "tool_evidence.arguments_sha256")
312
+ _digest(self.invocation_sha256, "tool_evidence.invocation_sha256")
313
+
314
+ @classmethod
315
+ def from_invocation(cls, sequence: int, invocation: ToolInvocation) -> ToolCallEvidence:
316
+ arguments_sha256 = _sha256(_canonical_json_bytes(invocation.arguments))
317
+ invocation_sha256 = _sha256(_canonical_json_bytes(invocation.to_dict()))
318
+ return cls(
319
+ sequence=sequence,
320
+ name=invocation.name,
321
+ arguments_sha256=arguments_sha256,
322
+ invocation_sha256=invocation_sha256,
323
+ )
324
+
325
+ def to_dict(self) -> dict[str, Any]:
326
+ return {
327
+ "sequence": self.sequence,
328
+ "name": self.name,
329
+ "arguments_sha256": self.arguments_sha256,
330
+ "invocation_sha256": self.invocation_sha256,
331
+ }
332
+
333
+
334
+ @dataclass(frozen=True)
335
+ class ToolPolicy:
336
+ """Closed V1 policy: registered search and result-reference browsing only."""
337
+
338
+ version: str = TOOL_POLICY_VERSION
339
+ max_search_query_chars: int = 500
340
+ max_search_results: int = 20
341
+ max_browse_chars: int = 100_000
342
+
343
+ def __post_init__(self) -> None:
344
+ if self.version != TOOL_POLICY_VERSION:
345
+ raise RuntimeContractError(f"tool policy version must be {TOOL_POLICY_VERSION!r}")
346
+ _positive_int(self.max_search_query_chars, "max_search_query_chars")
347
+ _positive_int(self.max_search_results, "max_search_results")
348
+ _positive_int(self.max_browse_chars, "max_browse_chars")
349
+
350
+ def validate(self, invocation: ToolInvocation, *, path: str = "tool") -> None:
351
+ try:
352
+ if invocation.name == "search":
353
+ self._validate_search(invocation.arguments, path)
354
+ return
355
+ if invocation.name == "browse":
356
+ self._validate_browse(invocation.arguments, path)
357
+ return
358
+ raise ToolPolicyError(
359
+ f"{path}.name is not sanctioned; only registered search and browse are available"
360
+ )
361
+ except ToolPolicyError:
362
+ raise
363
+ except RuntimeContractError as exc:
364
+ raise ToolPolicyError(str(exc)) from None
365
+
366
+ def _validate_search(self, arguments: Mapping[str, Any], path: str) -> None:
367
+ _exact_fields(arguments, {"query", "limit"}, f"{path}.arguments")
368
+ _required_string(
369
+ arguments["query"],
370
+ f"{path}.arguments.query",
371
+ maximum=self.max_search_query_chars,
372
+ )
373
+ limit = _positive_int(arguments["limit"], f"{path}.arguments.limit")
374
+ if limit > self.max_search_results:
375
+ raise ToolPolicyError(
376
+ f"{path}.arguments.limit exceeds the sanctioned search result limit"
377
+ )
378
+
379
+ def _validate_browse(self, arguments: Mapping[str, Any], path: str) -> None:
380
+ required = {"result_ref", "max_chars"}
381
+ optional = {"cursor"}
382
+ _exact_fields(arguments, required, f"{path}.arguments", optional=optional)
383
+ result_ref = _required_string(
384
+ arguments["result_ref"], f"{path}.arguments.result_ref", maximum=256
385
+ )
386
+ if not result_ref.startswith("searchref://") or "://" in result_ref[12:]:
387
+ raise ToolPolicyError(
388
+ f"{path}.arguments.result_ref must be an opaque searchref:// reference"
389
+ )
390
+ max_chars = _positive_int(arguments["max_chars"], f"{path}.arguments.max_chars")
391
+ if max_chars > self.max_browse_chars:
392
+ raise ToolPolicyError(f"{path}.arguments.max_chars exceeds the browse limit")
393
+ if "cursor" in arguments:
394
+ _required_string(arguments["cursor"], f"{path}.arguments.cursor", maximum=256)
395
+
396
+
397
+ @dataclass(frozen=True)
398
+ class AgentRequest:
399
+ """Strict provider-neutral request envelope."""
400
+
401
+ role: str
402
+ prompt_template_version: str
403
+ payload_schema_version: str
404
+ payload: Mapping[str, Any]
405
+ tools: tuple[ToolInvocation, ...] = ()
406
+ secret_references: tuple[SecretReference, ...] = ()
407
+ input_token_budget: int = 1
408
+ max_output_tokens: int = 1
409
+ timeout_micros: int = 1
410
+ max_cost_micros: int = 0
411
+ schema_version: str = AGENT_REQUEST_VERSION
412
+
413
+ def __post_init__(self) -> None:
414
+ if self.schema_version != AGENT_REQUEST_VERSION:
415
+ raise RuntimeContractError(f"request.schema_version must be {AGENT_REQUEST_VERSION!r}")
416
+ if self.role not in AGENT_ROLES:
417
+ raise RuntimeContractError(f"request.role is unsupported: {self.role!r}")
418
+ _version(self.prompt_template_version, "request.prompt_template_version")
419
+ _version(self.payload_schema_version, "request.payload_schema_version")
420
+ object.__setattr__(self, "payload", _strict_json_object(self.payload, "request.payload"))
421
+ _validate_role_payload(
422
+ self.role,
423
+ self.payload_schema_version,
424
+ self.payload,
425
+ direction="input",
426
+ )
427
+ if not isinstance(self.tools, tuple):
428
+ raise RuntimeContractError("request.tools must be a tuple")
429
+ if not all(isinstance(item, ToolInvocation) for item in self.tools):
430
+ raise RuntimeContractError("request.tools contains an invalid invocation")
431
+ if not isinstance(self.secret_references, tuple):
432
+ raise RuntimeContractError("request.secret_references must be a tuple")
433
+ if not all(isinstance(item, SecretReference) for item in self.secret_references):
434
+ raise RuntimeContractError("request.secret_references contains an invalid reference")
435
+ _require_unique((item.name for item in self.secret_references), "secret reference names")
436
+ _require_unique((item.handle_id for item in self.secret_references), "secret handle IDs")
437
+ _positive_accounting_int(self.input_token_budget, "request.input_token_budget")
438
+ _positive_accounting_int(self.max_output_tokens, "request.max_output_tokens")
439
+ _positive_accounting_int(self.timeout_micros, "request.timeout_micros")
440
+ _accounting_int(self.max_cost_micros, "request.max_cost_micros")
441
+
442
+ @property
443
+ def canonical_bytes(self) -> bytes:
444
+ return _canonical_json_bytes(self.to_dict())
445
+
446
+ @property
447
+ def request_sha256(self) -> str:
448
+ return _sha256(self.canonical_bytes)
449
+
450
+ def to_dict(self) -> dict[str, Any]:
451
+ return {
452
+ "schema_version": self.schema_version,
453
+ "role": self.role,
454
+ "prompt_template_version": self.prompt_template_version,
455
+ "payload_schema_version": self.payload_schema_version,
456
+ "payload": deepcopy(dict(self.payload)),
457
+ "tools": [item.to_dict() for item in self.tools],
458
+ "secret_references": [item.to_dict() for item in self.secret_references],
459
+ "input_token_budget": self.input_token_budget,
460
+ "max_output_tokens": self.max_output_tokens,
461
+ "timeout_micros": self.timeout_micros,
462
+ "max_cost_micros": self.max_cost_micros,
463
+ }
464
+
465
+ @classmethod
466
+ def from_value(cls, value: Any) -> AgentRequest:
467
+ data = _object(value, "request")
468
+ _exact_fields(
469
+ data,
470
+ {
471
+ "schema_version",
472
+ "role",
473
+ "prompt_template_version",
474
+ "payload_schema_version",
475
+ "payload",
476
+ "tools",
477
+ "secret_references",
478
+ "input_token_budget",
479
+ "max_output_tokens",
480
+ "timeout_micros",
481
+ "max_cost_micros",
482
+ },
483
+ "request",
484
+ )
485
+ tools = _array(data["tools"], "request.tools", maximum=16)
486
+ secrets = _array(data["secret_references"], "request.secret_references", maximum=32)
487
+ return cls(
488
+ schema_version=_required_string(
489
+ data["schema_version"], "request.schema_version", maximum=64
490
+ ),
491
+ role=_required_string(data["role"], "request.role", maximum=64),
492
+ prompt_template_version=_required_string(
493
+ data["prompt_template_version"],
494
+ "request.prompt_template_version",
495
+ maximum=128,
496
+ ),
497
+ payload_schema_version=_required_string(
498
+ data["payload_schema_version"],
499
+ "request.payload_schema_version",
500
+ maximum=128,
501
+ ),
502
+ payload=_object(data["payload"], "request.payload"),
503
+ tools=tuple(
504
+ ToolInvocation.from_value(item, f"request.tools[{index}]")
505
+ for index, item in enumerate(tools)
506
+ ),
507
+ secret_references=tuple(
508
+ SecretReference.from_value(item, f"request.secret_references[{index}]")
509
+ for index, item in enumerate(secrets)
510
+ ),
511
+ input_token_budget=data["input_token_budget"],
512
+ max_output_tokens=data["max_output_tokens"],
513
+ timeout_micros=data["timeout_micros"],
514
+ max_cost_micros=data["max_cost_micros"],
515
+ )
516
+
517
+
518
+ @dataclass(frozen=True)
519
+ class AgentResponse:
520
+ """Strict structured response envelope; role output remains an untrusted proposal."""
521
+
522
+ role: str
523
+ payload_schema_version: str
524
+ payload: Mapping[str, Any]
525
+ schema_version: str = AGENT_RESPONSE_VERSION
526
+
527
+ def __post_init__(self) -> None:
528
+ if self.schema_version != AGENT_RESPONSE_VERSION:
529
+ raise RuntimeContractError(
530
+ f"response.schema_version must be {AGENT_RESPONSE_VERSION!r}"
531
+ )
532
+ if self.role not in AGENT_ROLES:
533
+ raise RuntimeContractError(f"response.role is unsupported: {self.role!r}")
534
+ _version(self.payload_schema_version, "response.payload_schema_version")
535
+ object.__setattr__(self, "payload", _strict_json_object(self.payload, "response.payload"))
536
+ _validate_role_payload(
537
+ self.role,
538
+ self.payload_schema_version,
539
+ self.payload,
540
+ direction="output",
541
+ )
542
+
543
+ @property
544
+ def canonical_bytes(self) -> bytes:
545
+ return _canonical_json_bytes(self.to_dict())
546
+
547
+ @property
548
+ def response_sha256(self) -> str:
549
+ return _sha256(self.canonical_bytes)
550
+
551
+ def to_dict(self) -> dict[str, Any]:
552
+ return {
553
+ "schema_version": self.schema_version,
554
+ "role": self.role,
555
+ "payload_schema_version": self.payload_schema_version,
556
+ "payload": deepcopy(dict(self.payload)),
557
+ }
558
+
559
+ @classmethod
560
+ def from_value(cls, value: Any) -> AgentResponse:
561
+ data = _object(value, "response")
562
+ _exact_fields(
563
+ data,
564
+ {"schema_version", "role", "payload_schema_version", "payload"},
565
+ "response",
566
+ )
567
+ return cls(
568
+ schema_version=_required_string(
569
+ data["schema_version"], "response.schema_version", maximum=64
570
+ ),
571
+ role=_required_string(data["role"], "response.role", maximum=64),
572
+ payload_schema_version=_required_string(
573
+ data["payload_schema_version"],
574
+ "response.payload_schema_version",
575
+ maximum=128,
576
+ ),
577
+ payload=_object(data["payload"], "response.payload"),
578
+ )
579
+
580
+
581
+ @dataclass(frozen=True)
582
+ class ProviderResult:
583
+ """Untrusted structured response and provider-reported integer usage."""
584
+
585
+ response: Any
586
+ input_tokens: Any
587
+ output_tokens: Any
588
+ cost_micros: Any
589
+
590
+
591
+ @runtime_checkable
592
+ class ProviderAdapter(Protocol):
593
+ """Minimal provider boundary implemented outside the deterministic core."""
594
+
595
+ @property
596
+ def provider(self) -> str: ...
597
+
598
+ @property
599
+ def model_identifier(self) -> str: ...
600
+
601
+ def invoke(self, request: AgentRequest, tool_policy: ToolPolicy) -> ProviderResult: ...
602
+
603
+
604
+ @runtime_checkable
605
+ class AgentRuntime(Protocol):
606
+ """Common protocol for live adapters and checked-in replay."""
607
+
608
+ def call(self, request: AgentRequest) -> AgentCallResult: ...
609
+
610
+
611
+ @dataclass(frozen=True)
612
+ class BudgetLimits:
613
+ """Per-call and aggregate run limits. All amounts are inclusive."""
614
+
615
+ max_calls: int
616
+ max_input_tokens_per_call: int
617
+ max_output_tokens_per_call: int
618
+ max_total_tokens_per_call: int
619
+ max_cost_micros_per_call: int
620
+ max_time_micros_per_call: int
621
+ max_input_tokens_per_run: int
622
+ max_output_tokens_per_run: int
623
+ max_total_tokens_per_run: int
624
+ max_cost_micros_per_run: int
625
+ max_time_micros_per_run: int
626
+
627
+ def __post_init__(self) -> None:
628
+ _positive_accounting_int(self.max_calls, "budget.max_calls")
629
+ for name, value in self.to_dict().items():
630
+ if name != "max_calls":
631
+ _accounting_int(value, f"budget.{name}")
632
+ if self.max_total_tokens_per_call < max(
633
+ self.max_input_tokens_per_call, self.max_output_tokens_per_call
634
+ ):
635
+ raise RuntimeContractError(
636
+ "budget.max_total_tokens_per_call cannot be smaller than a component limit"
637
+ )
638
+ if self.max_total_tokens_per_run < max(
639
+ self.max_input_tokens_per_run, self.max_output_tokens_per_run
640
+ ):
641
+ raise RuntimeContractError(
642
+ "budget.max_total_tokens_per_run cannot be smaller than a component limit"
643
+ )
644
+
645
+ def to_dict(self) -> dict[str, int]:
646
+ return {
647
+ "max_calls": self.max_calls,
648
+ "max_input_tokens_per_call": self.max_input_tokens_per_call,
649
+ "max_output_tokens_per_call": self.max_output_tokens_per_call,
650
+ "max_total_tokens_per_call": self.max_total_tokens_per_call,
651
+ "max_cost_micros_per_call": self.max_cost_micros_per_call,
652
+ "max_time_micros_per_call": self.max_time_micros_per_call,
653
+ "max_input_tokens_per_run": self.max_input_tokens_per_run,
654
+ "max_output_tokens_per_run": self.max_output_tokens_per_run,
655
+ "max_total_tokens_per_run": self.max_total_tokens_per_run,
656
+ "max_cost_micros_per_run": self.max_cost_micros_per_run,
657
+ "max_time_micros_per_run": self.max_time_micros_per_run,
658
+ }
659
+
660
+ @classmethod
661
+ def generous(cls) -> BudgetLimits:
662
+ """Wide limits that back the default ledger when a runtime is built without a budget.
663
+
664
+ ProviderAgentRuntime uses these whenever its budget argument is None. Pass an explicit
665
+ BudgetLedger to enforce narrower limits.
666
+ """
667
+
668
+ return cls(
669
+ max_calls=1_000,
670
+ max_input_tokens_per_call=1_000_000,
671
+ max_output_tokens_per_call=1_000_000,
672
+ max_total_tokens_per_call=2_000_000,
673
+ max_cost_micros_per_call=1_000_000_000,
674
+ max_time_micros_per_call=3_600_000_000,
675
+ max_input_tokens_per_run=100_000_000,
676
+ max_output_tokens_per_run=100_000_000,
677
+ max_total_tokens_per_run=200_000_000,
678
+ max_cost_micros_per_run=100_000_000_000,
679
+ max_time_micros_per_run=86_400_000_000,
680
+ )
681
+
682
+
683
+ @dataclass(frozen=True)
684
+ class BudgetSnapshot:
685
+ calls: int
686
+ input_tokens: int
687
+ output_tokens: int
688
+ cost_micros: int
689
+ time_micros: int
690
+ active_reservations: int
691
+
692
+ @property
693
+ def total_tokens(self) -> int:
694
+ return self.input_tokens + self.output_tokens
695
+
696
+
697
+ @dataclass(frozen=True)
698
+ class _Reservation:
699
+ reservation_id: int
700
+ input_tokens: int
701
+ output_tokens: int
702
+ cost_micros: int
703
+ time_micros: int
704
+
705
+
706
+ class BudgetLedger:
707
+ """Concurrency-safe reservation and final accounting for a single run."""
708
+
709
+ def __init__(self, limits: BudgetLimits) -> None:
710
+ self._limits = limits
711
+ self._lock = threading.Lock()
712
+ self._next_id = 1
713
+ self._reservations: dict[int, _Reservation] = {}
714
+ self._calls = 0
715
+ self._input_tokens = 0
716
+ self._output_tokens = 0
717
+ self._cost_micros = 0
718
+ self._time_micros = 0
719
+
720
+ @property
721
+ def limits(self) -> BudgetLimits:
722
+ return self._limits
723
+
724
+ def snapshot(self) -> BudgetSnapshot:
725
+ with self._lock:
726
+ return BudgetSnapshot(
727
+ calls=self._calls,
728
+ input_tokens=self._input_tokens,
729
+ output_tokens=self._output_tokens,
730
+ cost_micros=self._cost_micros,
731
+ time_micros=self._time_micros,
732
+ active_reservations=len(self._reservations),
733
+ )
734
+
735
+ def reserve(self, request: AgentRequest) -> _Reservation:
736
+ requested_total = _checked_add(
737
+ request.input_token_budget,
738
+ request.max_output_tokens,
739
+ "request reserved total tokens",
740
+ )
741
+ per_call_checks = (
742
+ (
743
+ request.input_token_budget,
744
+ self._limits.max_input_tokens_per_call,
745
+ "input_tokens_per_call",
746
+ ),
747
+ (
748
+ request.max_output_tokens,
749
+ self._limits.max_output_tokens_per_call,
750
+ "output_tokens_per_call",
751
+ ),
752
+ (
753
+ requested_total,
754
+ self._limits.max_total_tokens_per_call,
755
+ "total_tokens_per_call",
756
+ ),
757
+ (
758
+ request.max_cost_micros,
759
+ self._limits.max_cost_micros_per_call,
760
+ "cost_micros_per_call",
761
+ ),
762
+ (
763
+ request.timeout_micros,
764
+ self._limits.max_time_micros_per_call,
765
+ "time_micros_per_call",
766
+ ),
767
+ )
768
+ for requested, limit, label in per_call_checks:
769
+ if requested > limit:
770
+ raise _ReservationDenied(f"budget {label} would be exceeded")
771
+
772
+ with self._lock:
773
+ reserved = self._reserved_totals_locked()
774
+ run_checks = (
775
+ (self._calls + reserved.calls + 1, self._limits.max_calls, "calls"),
776
+ (
777
+ self._input_tokens + reserved.input_tokens + request.input_token_budget,
778
+ self._limits.max_input_tokens_per_run,
779
+ "input_tokens_per_run",
780
+ ),
781
+ (
782
+ self._output_tokens + reserved.output_tokens + request.max_output_tokens,
783
+ self._limits.max_output_tokens_per_run,
784
+ "output_tokens_per_run",
785
+ ),
786
+ (
787
+ self._input_tokens
788
+ + self._output_tokens
789
+ + reserved.total_tokens
790
+ + requested_total,
791
+ self._limits.max_total_tokens_per_run,
792
+ "total_tokens_per_run",
793
+ ),
794
+ (
795
+ self._cost_micros + reserved.cost_micros + request.max_cost_micros,
796
+ self._limits.max_cost_micros_per_run,
797
+ "cost_micros_per_run",
798
+ ),
799
+ (
800
+ self._time_micros + reserved.time_micros + request.timeout_micros,
801
+ self._limits.max_time_micros_per_run,
802
+ "time_micros_per_run",
803
+ ),
804
+ )
805
+ for requested, limit, label in run_checks:
806
+ if requested > limit:
807
+ raise _ReservationDenied(f"budget {label} would be exceeded")
808
+ reservation = _Reservation(
809
+ reservation_id=self._next_id,
810
+ input_tokens=request.input_token_budget,
811
+ output_tokens=request.max_output_tokens,
812
+ cost_micros=request.max_cost_micros,
813
+ time_micros=request.timeout_micros,
814
+ )
815
+ self._next_id += 1
816
+ self._reservations[reservation.reservation_id] = reservation
817
+ return reservation
818
+
819
+ def finalize(
820
+ self,
821
+ reservation: _Reservation,
822
+ *,
823
+ input_tokens: Any,
824
+ output_tokens: Any,
825
+ cost_micros: Any,
826
+ time_micros: Any,
827
+ accounting_status: str = "reported",
828
+ ) -> tuple[Usage, tuple[str, ...]]:
829
+ input_value, input_overflow = _provider_accounting(input_tokens, "input_tokens")
830
+ output_value, output_overflow = _provider_accounting(output_tokens, "output_tokens")
831
+ cost_value, cost_overflow = _provider_accounting(cost_micros, "cost_micros")
832
+ time_value, time_overflow = _provider_accounting(time_micros, "time_micros")
833
+ overflow_fields = tuple(
834
+ name
835
+ for name, overflow in (
836
+ ("input_tokens", input_overflow),
837
+ ("output_tokens", output_overflow),
838
+ ("cost_micros", cost_overflow),
839
+ ("time_micros", time_overflow),
840
+ )
841
+ if overflow
842
+ )
843
+ if input_value + output_value > MAX_ACCOUNTING_INTEGER:
844
+ overflow_fields = (*overflow_fields, "total_tokens")
845
+ total_value = min(MAX_ACCOUNTING_INTEGER, input_value + output_value)
846
+ usage = Usage(
847
+ input_tokens=min(input_value, total_value),
848
+ output_tokens=min(output_value, MAX_ACCOUNTING_INTEGER - min(input_value, total_value)),
849
+ cost_micros=cost_value,
850
+ accounting_status=accounting_status,
851
+ )
852
+ with self._lock:
853
+ current = self._reservations.pop(reservation.reservation_id, None)
854
+ if current != reservation:
855
+ raise RuntimeContractError("budget reservation was not active")
856
+ self._calls = min(MAX_ACCOUNTING_INTEGER, self._calls + 1)
857
+ self._input_tokens = min(
858
+ MAX_ACCOUNTING_INTEGER, self._input_tokens + usage.input_tokens
859
+ )
860
+ self._output_tokens = min(
861
+ MAX_ACCOUNTING_INTEGER, self._output_tokens + usage.output_tokens
862
+ )
863
+ self._cost_micros = min(MAX_ACCOUNTING_INTEGER, self._cost_micros + usage.cost_micros)
864
+ self._time_micros = min(MAX_ACCOUNTING_INTEGER, self._time_micros + time_value)
865
+ exceeded = list(f"{field}_overflow" for field in overflow_fields)
866
+ actual_total = min(MAX_ACCOUNTING_INTEGER, usage.input_tokens + usage.output_tokens)
867
+ after_checks = (
868
+ (
869
+ usage.input_tokens,
870
+ reservation.input_tokens,
871
+ "reserved_input_tokens",
872
+ ),
873
+ (
874
+ usage.output_tokens,
875
+ reservation.output_tokens,
876
+ "reserved_output_tokens",
877
+ ),
878
+ (
879
+ usage.cost_micros,
880
+ reservation.cost_micros,
881
+ "reserved_cost_micros",
882
+ ),
883
+ (
884
+ time_value,
885
+ reservation.time_micros,
886
+ "reserved_time_micros",
887
+ ),
888
+ (
889
+ usage.input_tokens,
890
+ self._limits.max_input_tokens_per_call,
891
+ "input_tokens_per_call",
892
+ ),
893
+ (
894
+ usage.output_tokens,
895
+ self._limits.max_output_tokens_per_call,
896
+ "output_tokens_per_call",
897
+ ),
898
+ (
899
+ actual_total,
900
+ self._limits.max_total_tokens_per_call,
901
+ "total_tokens_per_call",
902
+ ),
903
+ (
904
+ usage.cost_micros,
905
+ self._limits.max_cost_micros_per_call,
906
+ "cost_micros_per_call",
907
+ ),
908
+ (
909
+ time_value,
910
+ self._limits.max_time_micros_per_call,
911
+ "time_micros_per_call",
912
+ ),
913
+ (self._calls, self._limits.max_calls, "calls"),
914
+ (
915
+ self._input_tokens,
916
+ self._limits.max_input_tokens_per_run,
917
+ "input_tokens_per_run",
918
+ ),
919
+ (
920
+ self._output_tokens,
921
+ self._limits.max_output_tokens_per_run,
922
+ "output_tokens_per_run",
923
+ ),
924
+ (
925
+ min(
926
+ MAX_ACCOUNTING_INTEGER,
927
+ self._input_tokens + self._output_tokens,
928
+ ),
929
+ self._limits.max_total_tokens_per_run,
930
+ "total_tokens_per_run",
931
+ ),
932
+ (
933
+ self._cost_micros,
934
+ self._limits.max_cost_micros_per_run,
935
+ "cost_micros_per_run",
936
+ ),
937
+ (
938
+ self._time_micros,
939
+ self._limits.max_time_micros_per_run,
940
+ "time_micros_per_run",
941
+ ),
942
+ )
943
+ for actual, limit, label in after_checks:
944
+ if actual > limit:
945
+ exceeded.append(label)
946
+ return usage, tuple(sorted(set(exceeded)))
947
+
948
+ def _reserved_totals_locked(self) -> BudgetSnapshot:
949
+ reservations = tuple(self._reservations.values())
950
+ return BudgetSnapshot(
951
+ calls=len(reservations),
952
+ input_tokens=sum(item.input_tokens for item in reservations),
953
+ output_tokens=sum(item.output_tokens for item in reservations),
954
+ cost_micros=sum(item.cost_micros for item in reservations),
955
+ time_micros=sum(item.time_micros for item in reservations),
956
+ active_reservations=len(reservations),
957
+ )
958
+
959
+
960
+ class _ReservationDenied(RuntimeError):
961
+ pass
962
+
963
+
964
+ @dataclass(frozen=True)
965
+ class AgentCallEvidence:
966
+ """Complete, canonical evidence for every terminal call outcome."""
967
+
968
+ runtime_version: str
969
+ request_contract_version: str
970
+ response_contract_version: str
971
+ replay_fixture_version: str | None
972
+ provider: str
973
+ model_identifier: str
974
+ provider_configuration_sha256: str
975
+ role: str
976
+ prompt_template_version: str
977
+ prompt_sha256: str
978
+ tool_policy_version: str
979
+ request_sha256: str
980
+ response_sha256: str | None
981
+ raw_response_sha256: str | None
982
+ secret_references: tuple[SecretReference, ...]
983
+ tool_evidence: tuple[ToolCallEvidence, ...]
984
+ outcome: str
985
+ usage: Usage
986
+ timing: TimingEvidence
987
+ deadline_micros: int
988
+ failure_code: str | None = None
989
+ schema_version: str = AGENT_EVIDENCE_VERSION
990
+
991
+ def __post_init__(self) -> None:
992
+ if self.schema_version != AGENT_EVIDENCE_VERSION:
993
+ raise RuntimeContractError(
994
+ f"evidence.schema_version must be {AGENT_EVIDENCE_VERSION!r}"
995
+ )
996
+ if self.runtime_version != AGENT_RUNTIME_VERSION:
997
+ raise RuntimeContractError(
998
+ f"evidence.runtime_version must be {AGENT_RUNTIME_VERSION!r}"
999
+ )
1000
+ if self.request_contract_version != AGENT_REQUEST_VERSION:
1001
+ raise RuntimeContractError(
1002
+ f"evidence.request_contract_version must be {AGENT_REQUEST_VERSION!r}"
1003
+ )
1004
+ if self.response_contract_version != AGENT_RESPONSE_VERSION:
1005
+ raise RuntimeContractError(
1006
+ f"evidence.response_contract_version must be {AGENT_RESPONSE_VERSION!r}"
1007
+ )
1008
+ if self.replay_fixture_version is not None:
1009
+ _version(self.replay_fixture_version, "evidence.replay_fixture_version")
1010
+ _identifier(self.provider, "evidence.provider")
1011
+ _required_string(self.model_identifier, "evidence.model_identifier", maximum=256)
1012
+ _digest(
1013
+ self.provider_configuration_sha256,
1014
+ "evidence.provider_configuration_sha256",
1015
+ )
1016
+ if self.role not in AGENT_ROLES:
1017
+ raise RuntimeContractError("evidence.role is unsupported")
1018
+ _version(self.prompt_template_version, "evidence.prompt_template_version")
1019
+ _digest(self.prompt_sha256, "evidence.prompt_sha256")
1020
+ _version(self.tool_policy_version, "evidence.tool_policy_version")
1021
+ _digest(self.request_sha256, "evidence.request_sha256")
1022
+ if self.response_sha256 is not None:
1023
+ _digest(self.response_sha256, "evidence.response_sha256")
1024
+ if self.raw_response_sha256 is not None:
1025
+ _digest(self.raw_response_sha256, "evidence.raw_response_sha256")
1026
+ if not isinstance(self.secret_references, tuple) or not all(
1027
+ isinstance(item, SecretReference) for item in self.secret_references
1028
+ ):
1029
+ raise RuntimeContractError("evidence.secret_references must be scoped handles")
1030
+ if not isinstance(self.tool_evidence, tuple) or not all(
1031
+ isinstance(item, ToolCallEvidence) for item in self.tool_evidence
1032
+ ):
1033
+ raise RuntimeContractError("evidence.tool_evidence must contain tool call evidence")
1034
+ _require_unique(
1035
+ (item.handle_id for item in self.secret_references),
1036
+ "evidence secret handle IDs",
1037
+ )
1038
+ expected_sequences = tuple(range(1, len(self.tool_evidence) + 1))
1039
+ if tuple(item.sequence for item in self.tool_evidence) != expected_sequences:
1040
+ raise RuntimeContractError("evidence.tool_evidence sequences must be contiguous")
1041
+ _nonnegative_int(self.deadline_micros, "evidence.deadline_micros")
1042
+ if self.deadline_micros < self.timing.started_micros:
1043
+ raise RuntimeContractError("evidence.deadline_micros precedes call start")
1044
+ if self.outcome not in _OUTCOMES:
1045
+ raise RuntimeContractError(f"evidence.outcome is unsupported: {self.outcome!r}")
1046
+ if self.outcome in _PRE_DISPATCH_OUTCOMES:
1047
+ if self.usage.accounting_status != "not_applicable":
1048
+ raise RuntimeContractError("pre-dispatch evidence usage must be not_applicable")
1049
+ if self.usage.input_tokens or self.usage.output_tokens or self.usage.cost_micros:
1050
+ raise RuntimeContractError("pre-dispatch evidence usage must be zero")
1051
+ elif self.usage.accounting_status == "not_applicable":
1052
+ raise RuntimeContractError("post-dispatch evidence usage cannot be not_applicable")
1053
+ if self.outcome == "succeeded" and self.usage.accounting_status != "reported":
1054
+ raise RuntimeContractError("successful evidence usage must be provider-reported")
1055
+ if self.outcome == "succeeded":
1056
+ if self.response_sha256 is None or self.failure_code is not None:
1057
+ raise RuntimeContractError("successful evidence needs a response and no failure")
1058
+ elif self.failure_code is None:
1059
+ raise RuntimeContractError("failed evidence needs a stable failure_code")
1060
+ if self.failure_code is not None:
1061
+ _identifier(self.failure_code, "evidence.failure_code")
1062
+
1063
+ @property
1064
+ def evidence_sha256(self) -> str:
1065
+ return _sha256(_canonical_json_bytes(self.to_dict()))
1066
+
1067
+ def to_dict(self) -> dict[str, Any]:
1068
+ return {
1069
+ "schema_version": self.schema_version,
1070
+ "runtime_version": self.runtime_version,
1071
+ "request_contract_version": self.request_contract_version,
1072
+ "response_contract_version": self.response_contract_version,
1073
+ "replay_fixture_version": self.replay_fixture_version,
1074
+ "provider": self.provider,
1075
+ "model_identifier": self.model_identifier,
1076
+ "provider_configuration_sha256": self.provider_configuration_sha256,
1077
+ "role": self.role,
1078
+ "prompt_template_version": self.prompt_template_version,
1079
+ "prompt_sha256": self.prompt_sha256,
1080
+ "tool_policy_version": self.tool_policy_version,
1081
+ "request_sha256": self.request_sha256,
1082
+ "response_sha256": self.response_sha256,
1083
+ "raw_response_sha256": self.raw_response_sha256,
1084
+ "secret_references": [item.to_dict() for item in self.secret_references],
1085
+ "tool_evidence": [item.to_dict() for item in self.tool_evidence],
1086
+ "outcome": self.outcome,
1087
+ "usage": self.usage.to_dict(),
1088
+ "timing": self.timing.to_dict(),
1089
+ "deadline_micros": self.deadline_micros,
1090
+ "failure_code": self.failure_code,
1091
+ }
1092
+
1093
+
1094
+ @dataclass(frozen=True)
1095
+ class AgentCallResult:
1096
+ response: AgentResponse
1097
+ evidence: AgentCallEvidence
1098
+
1099
+
1100
+ class AgentCallFailure(AgentRuntimeError):
1101
+ """A typed terminal failure carrying complete deterministic evidence."""
1102
+
1103
+ def __init__(self, evidence: AgentCallEvidence) -> None:
1104
+ self.evidence = evidence
1105
+ super().__init__(f"agent call failed: {evidence.failure_code}")
1106
+
1107
+
1108
+ class BudgetExceededBeforeCall(AgentCallFailure):
1109
+ """The call was not sent because its reservation could not be made."""
1110
+
1111
+
1112
+ class BudgetExceededAfterCall(AgentCallFailure):
1113
+ """Provider work was accounted, but the actual result exceeded a limit."""
1114
+
1115
+
1116
+ class ProviderCallFailed(AgentCallFailure):
1117
+ """Provider raised an exception before yielding a structured result."""
1118
+
1119
+
1120
+ class MalformedProviderResponse(AgentCallFailure):
1121
+ """Provider output or accounting failed strict validation."""
1122
+
1123
+
1124
+ class AgentCallTimedOut(AgentCallFailure):
1125
+ """The adapter reported or consumed more than its bounded call time."""
1126
+
1127
+
1128
+ class AgentCallCancelled(AgentCallFailure):
1129
+ """The provider call was cancelled; the reservation was still finalized."""
1130
+
1131
+
1132
+ class ToolPolicyDenied(AgentCallFailure, ToolPolicyError):
1133
+ """The invocation was rejected before provider execution with complete evidence."""
1134
+
1135
+
1136
+ class ProviderAgentRuntime:
1137
+ """Deterministic coordinator around one provider adapter and one run ledger."""
1138
+
1139
+ def __init__(
1140
+ self,
1141
+ adapter: ProviderAdapter,
1142
+ *,
1143
+ budget: BudgetLedger | None = None,
1144
+ tool_policy: ToolPolicy | None = None,
1145
+ clock: Callable[[], int] | None = None,
1146
+ replay_fixture_version: str | None = None,
1147
+ ) -> None:
1148
+ if not isinstance(adapter, ProviderAdapter):
1149
+ raise RuntimeContractError("adapter does not implement ProviderAdapter")
1150
+ _identifier(adapter.provider, "provider")
1151
+ _required_string(adapter.model_identifier, "model_identifier", maximum=256)
1152
+ self._adapter = adapter
1153
+ self._budget = budget or BudgetLedger(BudgetLimits.generous())
1154
+ self._tool_policy = tool_policy or ToolPolicy()
1155
+ self._clock = clock or _monotonic_micros
1156
+ if replay_fixture_version is not None:
1157
+ _version(replay_fixture_version, "replay_fixture_version")
1158
+ self._replay_fixture_version = replay_fixture_version
1159
+
1160
+ @property
1161
+ def budget(self) -> BudgetLedger:
1162
+ return self._budget
1163
+
1164
+ def call(self, request: AgentRequest) -> AgentCallResult:
1165
+ if not isinstance(request, AgentRequest):
1166
+ raise RuntimeContractError("call request must be an AgentRequest")
1167
+ started = self._now()
1168
+ try:
1169
+ for index, tool in enumerate(request.tools):
1170
+ self._tool_policy.validate(tool, path=f"request.tools[{index}]")
1171
+ except ToolPolicyError:
1172
+ finished = self._now()
1173
+ evidence = self._evidence(
1174
+ request,
1175
+ outcome="tool_policy_denied",
1176
+ started=started,
1177
+ finished=finished,
1178
+ usage=Usage(0, 0, 0, accounting_status="not_applicable"),
1179
+ failure_code="tool_policy_denied",
1180
+ )
1181
+ raise ToolPolicyDenied(evidence) from None
1182
+ try:
1183
+ reservation = self._budget.reserve(request)
1184
+ except _ReservationDenied as exc:
1185
+ finished = self._now()
1186
+ evidence = self._evidence(
1187
+ request,
1188
+ outcome="budget_exhausted_before_call",
1189
+ started=started,
1190
+ finished=finished,
1191
+ usage=Usage(0, 0, 0, accounting_status="not_applicable"),
1192
+ failure_code=_stable_failure_code(exc),
1193
+ )
1194
+ raise BudgetExceededBeforeCall(evidence) from None
1195
+
1196
+ try:
1197
+ result = self._adapter.invoke(request, self._tool_policy)
1198
+ except asyncio.CancelledError:
1199
+ evidence = self._finish_exception(
1200
+ request,
1201
+ reservation,
1202
+ started,
1203
+ outcome="cancelled",
1204
+ failure_code="provider_cancelled",
1205
+ )
1206
+ raise AgentCallCancelled(evidence) from None
1207
+ except (ProviderTimeoutError, TimeoutError):
1208
+ evidence = self._finish_exception(
1209
+ request,
1210
+ reservation,
1211
+ started,
1212
+ outcome="timed_out",
1213
+ failure_code="provider_timeout",
1214
+ )
1215
+ raise AgentCallTimedOut(evidence) from None
1216
+ except Exception:
1217
+ evidence = self._finish_exception(
1218
+ request,
1219
+ reservation,
1220
+ started,
1221
+ outcome="provider_exception",
1222
+ failure_code="provider_exception",
1223
+ )
1224
+ raise ProviderCallFailed(evidence) from None
1225
+
1226
+ finished = self._now()
1227
+ elapsed = self._elapsed(started, finished)
1228
+ if not isinstance(result, ProviderResult):
1229
+ usage, exceeded = self._budget.finalize(
1230
+ reservation,
1231
+ input_tokens=reservation.input_tokens,
1232
+ output_tokens=reservation.output_tokens,
1233
+ cost_micros=reservation.cost_micros,
1234
+ time_micros=elapsed,
1235
+ accounting_status="unavailable_reserved",
1236
+ )
1237
+ if exceeded:
1238
+ evidence = self._evidence(
1239
+ request,
1240
+ outcome="budget_exhausted_after_call",
1241
+ started=started,
1242
+ finished=finished,
1243
+ usage=usage,
1244
+ raw_response_sha256=_safe_response_hash(result),
1245
+ failure_code=_after_budget_code(exceeded),
1246
+ )
1247
+ raise BudgetExceededAfterCall(evidence)
1248
+ evidence = self._evidence(
1249
+ request,
1250
+ outcome="malformed_response",
1251
+ started=started,
1252
+ finished=finished,
1253
+ usage=usage,
1254
+ raw_response_sha256=_safe_response_hash(result),
1255
+ failure_code="invalid_provider_result",
1256
+ )
1257
+ raise MalformedProviderResponse(evidence)
1258
+ raw_response_hash = _safe_response_hash(result.response)
1259
+ try:
1260
+ usage, exceeded = self._budget.finalize(
1261
+ reservation,
1262
+ input_tokens=result.input_tokens,
1263
+ output_tokens=result.output_tokens,
1264
+ cost_micros=result.cost_micros,
1265
+ time_micros=elapsed,
1266
+ )
1267
+ except RuntimeContractError:
1268
+ # Invalid accounting cannot be trusted. Conservatively charge the full reservation.
1269
+ usage, exceeded = self._budget.finalize(
1270
+ reservation,
1271
+ input_tokens=reservation.input_tokens,
1272
+ output_tokens=reservation.output_tokens,
1273
+ cost_micros=reservation.cost_micros,
1274
+ time_micros=elapsed,
1275
+ accounting_status="unavailable_reserved",
1276
+ )
1277
+ if exceeded:
1278
+ evidence = self._evidence(
1279
+ request,
1280
+ outcome="budget_exhausted_after_call",
1281
+ started=started,
1282
+ finished=finished,
1283
+ usage=usage,
1284
+ raw_response_sha256=raw_response_hash,
1285
+ failure_code=_after_budget_code(exceeded),
1286
+ )
1287
+ raise BudgetExceededAfterCall(evidence) from None
1288
+ evidence = self._evidence(
1289
+ request,
1290
+ outcome="malformed_response",
1291
+ started=started,
1292
+ finished=finished,
1293
+ usage=usage,
1294
+ raw_response_sha256=raw_response_hash,
1295
+ failure_code="invalid_provider_accounting",
1296
+ )
1297
+ raise MalformedProviderResponse(evidence) from None
1298
+
1299
+ if elapsed > request.timeout_micros:
1300
+ exceeded = tuple(sorted(set((*exceeded, "request_timeout"))))
1301
+ if exceeded:
1302
+ evidence = self._evidence(
1303
+ request,
1304
+ outcome="budget_exhausted_after_call",
1305
+ started=started,
1306
+ finished=finished,
1307
+ usage=usage,
1308
+ raw_response_sha256=raw_response_hash,
1309
+ failure_code=_after_budget_code(exceeded),
1310
+ )
1311
+ if "request_timeout" in exceeded:
1312
+ raise AgentCallTimedOut(evidence)
1313
+ raise BudgetExceededAfterCall(evidence)
1314
+
1315
+ try:
1316
+ response = AgentResponse.from_value(result.response)
1317
+ if response.role != request.role:
1318
+ raise RuntimeContractError("response.role does not match request.role")
1319
+ _validate_request_response_semantics(request, response)
1320
+ except RuntimeContractError:
1321
+ evidence = self._evidence(
1322
+ request,
1323
+ outcome="malformed_response",
1324
+ started=started,
1325
+ finished=finished,
1326
+ usage=usage,
1327
+ raw_response_sha256=raw_response_hash,
1328
+ failure_code="malformed_structured_output",
1329
+ )
1330
+ raise MalformedProviderResponse(evidence) from None
1331
+
1332
+ evidence = self._evidence(
1333
+ request,
1334
+ outcome="succeeded",
1335
+ started=started,
1336
+ finished=finished,
1337
+ usage=usage,
1338
+ response_sha256=response.response_sha256,
1339
+ raw_response_sha256=raw_response_hash,
1340
+ )
1341
+ return AgentCallResult(response=response, evidence=evidence)
1342
+
1343
+ def _finish_exception(
1344
+ self,
1345
+ request: AgentRequest,
1346
+ reservation: _Reservation,
1347
+ started: int,
1348
+ *,
1349
+ outcome: str,
1350
+ failure_code: str,
1351
+ ) -> AgentCallEvidence:
1352
+ finished = self._now()
1353
+ elapsed = self._elapsed(started, finished)
1354
+ usage, _ = self._budget.finalize(
1355
+ reservation,
1356
+ input_tokens=reservation.input_tokens,
1357
+ output_tokens=reservation.output_tokens,
1358
+ cost_micros=reservation.cost_micros,
1359
+ time_micros=elapsed,
1360
+ accounting_status="unavailable_reserved",
1361
+ )
1362
+ return self._evidence(
1363
+ request,
1364
+ outcome=outcome,
1365
+ started=started,
1366
+ finished=finished,
1367
+ usage=usage,
1368
+ failure_code=failure_code,
1369
+ )
1370
+
1371
+ def _evidence(
1372
+ self,
1373
+ request: AgentRequest,
1374
+ *,
1375
+ outcome: str,
1376
+ started: int,
1377
+ finished: int,
1378
+ usage: Usage,
1379
+ failure_code: str | None = None,
1380
+ response_sha256: str | None = None,
1381
+ raw_response_sha256: str | None = None,
1382
+ ) -> AgentCallEvidence:
1383
+ return AgentCallEvidence(
1384
+ runtime_version=AGENT_RUNTIME_VERSION,
1385
+ request_contract_version=AGENT_REQUEST_VERSION,
1386
+ response_contract_version=AGENT_RESPONSE_VERSION,
1387
+ replay_fixture_version=self._replay_fixture_version,
1388
+ provider=self._adapter.provider,
1389
+ model_identifier=self._adapter.model_identifier,
1390
+ provider_configuration_sha256=_provider_configuration_digest(self._adapter),
1391
+ role=request.role,
1392
+ prompt_template_version=request.prompt_template_version,
1393
+ prompt_sha256=_prompt_digest(request),
1394
+ tool_policy_version=self._tool_policy.version,
1395
+ request_sha256=request.request_sha256,
1396
+ response_sha256=response_sha256,
1397
+ raw_response_sha256=raw_response_sha256,
1398
+ secret_references=request.secret_references,
1399
+ tool_evidence=tuple(
1400
+ ToolCallEvidence.from_invocation(index, invocation)
1401
+ for index, invocation in enumerate(request.tools, start=1)
1402
+ ),
1403
+ outcome=outcome,
1404
+ usage=usage,
1405
+ timing=TimingEvidence(
1406
+ started_micros=started,
1407
+ finished_micros=finished,
1408
+ elapsed_micros=self._elapsed(started, finished),
1409
+ ),
1410
+ deadline_micros=_checked_add(
1411
+ started,
1412
+ request.timeout_micros,
1413
+ "evidence.deadline_micros",
1414
+ ),
1415
+ failure_code=failure_code,
1416
+ )
1417
+
1418
+ def _now(self) -> int:
1419
+ return _nonnegative_int(self._clock(), "clock result")
1420
+
1421
+ @staticmethod
1422
+ def _elapsed(started: int, finished: int) -> int:
1423
+ if finished < started:
1424
+ raise RuntimeContractError("injected clock moved backwards")
1425
+ return finished - started
1426
+
1427
+
1428
+ @dataclass(frozen=True)
1429
+ class _ReplayEntry:
1430
+ request: AgentRequest
1431
+ response: Mapping[str, Any]
1432
+ response_sha256: str
1433
+ provider: str
1434
+ model_identifier: str
1435
+ usage: Usage
1436
+
1437
+
1438
+ class _ReplayAdapter:
1439
+ def __init__(self, entries: Mapping[str, tuple[_ReplayEntry, ...]]) -> None:
1440
+ self._entries = entries
1441
+ self._local = threading.local()
1442
+
1443
+ @property
1444
+ def provider(self) -> str:
1445
+ return getattr(self._local, "provider", "recorded-replay")
1446
+
1447
+ @property
1448
+ def model_identifier(self) -> str:
1449
+ return getattr(self._local, "model_identifier", "fixture-catalog.v1")
1450
+
1451
+ def bind(self, entry: _ReplayEntry) -> None:
1452
+ """Bind recorded provider identity before reservation or invocation."""
1453
+
1454
+ self._local.provider = entry.provider
1455
+ self._local.model_identifier = entry.model_identifier
1456
+
1457
+ def invoke(self, request: AgentRequest, tool_policy: ToolPolicy) -> ProviderResult:
1458
+ del tool_policy
1459
+ matches = self._entries.get(request.request_sha256, ())
1460
+ if not matches:
1461
+ raise ReplayMissError(
1462
+ f"no recorded replay for canonical request {request.request_sha256}"
1463
+ )
1464
+ if len(matches) != 1:
1465
+ raise ReplayAmbiguityError(
1466
+ f"multiple recorded replays for canonical request {request.request_sha256}"
1467
+ )
1468
+ entry = matches[0]
1469
+ if entry.request.canonical_bytes != request.canonical_bytes:
1470
+ raise ReplayTamperError("replay request hash collision or request fixture drift")
1471
+ self.bind(entry)
1472
+ return ProviderResult(
1473
+ response=deepcopy(dict(entry.response)),
1474
+ input_tokens=entry.usage.input_tokens,
1475
+ output_tokens=entry.usage.output_tokens,
1476
+ cost_micros=entry.usage.cost_micros,
1477
+ )
1478
+
1479
+
1480
+ class ReplayAgentRuntime(ProviderAgentRuntime):
1481
+ """Offline runtime backed only by duplicate-strict canonical checked-in fixtures.
1482
+
1483
+ Construct it with from_catalog (a checked-in catalog file) or from_recorded (one
1484
+ request/response pair already in memory). The __init__ entries mapping is the
1485
+ already-validated internal form those two classmethods build.
1486
+ """
1487
+
1488
+ def __init__(
1489
+ self,
1490
+ entries: Mapping[str, tuple[_ReplayEntry, ...]],
1491
+ *,
1492
+ budget: BudgetLedger | None = None,
1493
+ tool_policy: ToolPolicy | None = None,
1494
+ clock: Callable[[], int] | None = None,
1495
+ ) -> None:
1496
+ self._entries = entries
1497
+ self._replay_adapter = _ReplayAdapter(entries)
1498
+ super().__init__(
1499
+ self._replay_adapter,
1500
+ budget=budget,
1501
+ tool_policy=tool_policy,
1502
+ clock=clock,
1503
+ replay_fixture_version=REPLAY_CATALOG_VERSION,
1504
+ )
1505
+
1506
+ def call(self, request: AgentRequest) -> AgentCallResult:
1507
+ if not isinstance(request, AgentRequest):
1508
+ raise RuntimeContractError("call request must be an AgentRequest")
1509
+ matches = self._entries.get(request.request_sha256, ())
1510
+ if not matches:
1511
+ raise ReplayMissError(
1512
+ f"no recorded replay for canonical request {request.request_sha256}"
1513
+ )
1514
+ if len(matches) != 1:
1515
+ raise ReplayAmbiguityError(
1516
+ f"multiple recorded replays for canonical request {request.request_sha256}"
1517
+ )
1518
+ if matches[0].request.canonical_bytes != request.canonical_bytes:
1519
+ raise ReplayTamperError("replay request hash collision or request fixture drift")
1520
+ self._replay_adapter.bind(matches[0])
1521
+ return super().call(request)
1522
+
1523
+ @classmethod
1524
+ def from_recorded(
1525
+ cls,
1526
+ *,
1527
+ request: AgentRequest,
1528
+ response: AgentResponse,
1529
+ provider: str,
1530
+ model_identifier: str,
1531
+ input_tokens: int,
1532
+ output_tokens: int,
1533
+ cost_micros: int,
1534
+ budget: BudgetLedger | None = None,
1535
+ tool_policy: ToolPolicy | None = None,
1536
+ clock: Callable[[], int] | None = None,
1537
+ ) -> ReplayAgentRuntime:
1538
+ """Create one exact in-memory replay after strict request/response binding."""
1539
+
1540
+ if not isinstance(request, AgentRequest) or not isinstance(response, AgentResponse):
1541
+ raise RuntimeContractError("recorded replay requires typed request and response")
1542
+ _validate_request_response_semantics(request, response)
1543
+ normalized_provider = _required_string(provider, "provider", maximum=64)
1544
+ _identifier(normalized_provider, "provider")
1545
+ normalized_model = _required_string(
1546
+ model_identifier,
1547
+ "model_identifier",
1548
+ maximum=256,
1549
+ )
1550
+ usage = Usage(
1551
+ input_tokens=input_tokens,
1552
+ output_tokens=output_tokens,
1553
+ cost_micros=cost_micros,
1554
+ )
1555
+ entry = _ReplayEntry(
1556
+ request=request,
1557
+ response=response.to_dict(),
1558
+ response_sha256=response.response_sha256,
1559
+ provider=normalized_provider,
1560
+ model_identifier=normalized_model,
1561
+ usage=usage,
1562
+ )
1563
+ return cls(
1564
+ {request.request_sha256: (entry,)},
1565
+ budget=budget,
1566
+ tool_policy=tool_policy,
1567
+ clock=clock,
1568
+ )
1569
+
1570
+ @classmethod
1571
+ def from_catalog(
1572
+ cls,
1573
+ catalog_path: Path | str,
1574
+ *,
1575
+ budget: BudgetLedger | None = None,
1576
+ tool_policy: ToolPolicy | None = None,
1577
+ clock: Callable[[], int] | None = None,
1578
+ ) -> ReplayAgentRuntime:
1579
+ path = Path(catalog_path)
1580
+ catalog = _load_canonical_json_file(path, "replay catalog")
1581
+ data = _object(catalog, "catalog")
1582
+ _exact_fields(data, {"schema_version", "tool_policy_version", "entries"}, "catalog")
1583
+ if data["schema_version"] != REPLAY_CATALOG_VERSION:
1584
+ raise ReplayVersionError(f"catalog.schema_version must be {REPLAY_CATALOG_VERSION!r}")
1585
+ if data["tool_policy_version"] != TOOL_POLICY_VERSION:
1586
+ raise ReplayVersionError(f"catalog.tool_policy_version must be {TOOL_POLICY_VERSION!r}")
1587
+ raw_entries = _array(data["entries"], "catalog.entries", nonempty=True, maximum=10_000)
1588
+ entries: dict[str, list[_ReplayEntry]] = {}
1589
+ root = path.parent.resolve()
1590
+ for index, raw_entry in enumerate(raw_entries):
1591
+ entry_path = f"catalog.entries[{index}]"
1592
+ item = _object(raw_entry, entry_path)
1593
+ _exact_fields(
1594
+ item,
1595
+ {
1596
+ "request_file",
1597
+ "response_file",
1598
+ "request_sha256",
1599
+ "response_sha256",
1600
+ "provider",
1601
+ "model_identifier",
1602
+ "input_tokens",
1603
+ "output_tokens",
1604
+ "cost_micros",
1605
+ },
1606
+ entry_path,
1607
+ )
1608
+ request_file = _fixture_member(
1609
+ root,
1610
+ _required_string(item["request_file"], f"{entry_path}.request_file", maximum=256),
1611
+ )
1612
+ response_file = _fixture_member(
1613
+ root,
1614
+ _required_string(item["response_file"], f"{entry_path}.response_file", maximum=256),
1615
+ )
1616
+ request_value = _load_canonical_json_file(request_file, "replay request")
1617
+ response_value = _load_canonical_json_file(response_file, "replay response")
1618
+ try:
1619
+ request = AgentRequest.from_value(request_value)
1620
+ response = AgentResponse.from_value(response_value)
1621
+ except RuntimeContractError as exc:
1622
+ error_type = ReplayVersionError if "schema" in str(exc) else ReplayTamperError
1623
+ raise error_type(f"{entry_path} member contract drift: {exc}") from None
1624
+ if response.role != request.role:
1625
+ raise ReplayTamperError(f"{entry_path} response role does not match request")
1626
+ try:
1627
+ _validate_request_response_semantics(request, response)
1628
+ except RuntimeContractError as exc:
1629
+ raise ReplayTamperError(
1630
+ f"{entry_path} response cross-reference mismatch: {exc}"
1631
+ ) from None
1632
+ expected_request = _digest(item["request_sha256"], f"{entry_path}.request_sha256")
1633
+ expected_response = _digest(item["response_sha256"], f"{entry_path}.response_sha256")
1634
+ if request.request_sha256 != expected_request:
1635
+ raise ReplayTamperError(f"{entry_path} request fixture hash mismatch")
1636
+ if response.response_sha256 != expected_response:
1637
+ raise ReplayTamperError(f"{entry_path} response fixture hash mismatch")
1638
+ replay_entry = _ReplayEntry(
1639
+ request=request,
1640
+ response=response.to_dict(),
1641
+ response_sha256=expected_response,
1642
+ provider=_required_string(item["provider"], f"{entry_path}.provider", maximum=64),
1643
+ model_identifier=_required_string(
1644
+ item["model_identifier"],
1645
+ f"{entry_path}.model_identifier",
1646
+ maximum=256,
1647
+ ),
1648
+ usage=Usage(
1649
+ input_tokens=item["input_tokens"],
1650
+ output_tokens=item["output_tokens"],
1651
+ cost_micros=item["cost_micros"],
1652
+ ),
1653
+ )
1654
+ _identifier(replay_entry.provider, f"{entry_path}.provider")
1655
+ entries.setdefault(expected_request, []).append(replay_entry)
1656
+ return cls(
1657
+ {key: tuple(value) for key, value in entries.items()},
1658
+ budget=budget,
1659
+ tool_policy=tool_policy,
1660
+ clock=clock,
1661
+ )
1662
+
1663
+
1664
+ def _load_canonical_json_file(path: Path, label: str) -> Any:
1665
+ try:
1666
+ file_stat = path.lstat()
1667
+ if stat.S_ISLNK(file_stat.st_mode) or not stat.S_ISREG(file_stat.st_mode):
1668
+ raise ReplayTamperError(f"{label} must be a regular non-symlink file")
1669
+ raw = path.read_bytes()
1670
+ except ReplayTamperError:
1671
+ raise
1672
+ except OSError as exc:
1673
+ raise ReplayTamperError(f"cannot read {label}: {path.name}") from exc
1674
+ try:
1675
+ value = json.loads(
1676
+ raw.decode("utf-8"),
1677
+ object_pairs_hook=_reject_duplicate_pairs,
1678
+ parse_constant=_reject_json_constant,
1679
+ )
1680
+ except (UnicodeDecodeError, json.JSONDecodeError, RuntimeContractError) as exc:
1681
+ raise ReplayTamperError(f"{label} is not duplicate-strict UTF-8 JSON") from exc
1682
+ try:
1683
+ canonical = _canonical_json_bytes(value)
1684
+ except RuntimeContractError as exc:
1685
+ raise ReplayTamperError(f"{label} contains unsupported values") from exc
1686
+ if raw != canonical:
1687
+ raise ReplayTamperError(f"{label} is not canonical JSON")
1688
+ return value
1689
+
1690
+
1691
+ def _fixture_member(root: Path, relative: str) -> Path:
1692
+ if "\\" in relative or relative.startswith("/") or "//" in relative:
1693
+ raise ReplayTamperError("replay member path must be canonical relative POSIX text")
1694
+ parts = relative.split("/")
1695
+ if not parts or any(part in {"", ".", ".."} for part in parts):
1696
+ raise ReplayTamperError("replay member path must be canonical relative POSIX text")
1697
+ candidate = root.joinpath(*parts)
1698
+ if candidate.parent != root or candidate.is_symlink():
1699
+ raise ReplayTamperError("replay members must be siblings of the catalog")
1700
+ return candidate
1701
+
1702
+
1703
+ def _strict_json_object(value: Any, path: str) -> dict[str, Any]:
1704
+ data = _object(value, path)
1705
+ state = [0]
1706
+ normalized = _strict_json_value(data, path, depth=0, state=state)
1707
+ assert isinstance(normalized, dict)
1708
+ return normalized
1709
+
1710
+
1711
+ def _validate_role_payload(
1712
+ role: str,
1713
+ schema_version: str,
1714
+ payload: Mapping[str, Any],
1715
+ *,
1716
+ direction: str,
1717
+ ) -> None:
1718
+ if (
1719
+ role == "semantic_reviewer"
1720
+ and direction == "input"
1721
+ and schema_version == SEMANTIC_REVIEW_INPUT_VERSION
1722
+ ):
1723
+ _validate_hosted_semantic_review_input(payload)
1724
+ return
1725
+ expected = ROLE_PAYLOAD_SCHEMAS[role][0 if direction == "input" else 1]
1726
+ if schema_version != expected:
1727
+ raise RuntimeContractError(f"{direction} payload schema for {role!r} must be {expected!r}")
1728
+ if direction == "input":
1729
+ _validate_role_input(role, payload)
1730
+ else:
1731
+ _validate_role_output(role, payload)
1732
+
1733
+
1734
+ def _validate_hosted_semantic_review_input(payload: Mapping[str, Any]) -> None:
1735
+ """Check a semantic-reviewer payload declared as SEMANTIC_REVIEW_INPUT_VERSION."""
1736
+
1737
+ _exact_fields(payload, {"candidate_evidence", "requirements"}, "request.payload")
1738
+ candidate = _object(payload["candidate_evidence"], "request.payload.candidate_evidence")
1739
+ _exact_fields(
1740
+ candidate,
1741
+ {
1742
+ "candidate_id",
1743
+ "candidate_digest",
1744
+ "plan_digest",
1745
+ "artifact_scope_digest",
1746
+ "candidate_contract_version",
1747
+ "artifact_digests",
1748
+ "inspection",
1749
+ "inspection_digest",
1750
+ },
1751
+ "request.payload.candidate_evidence",
1752
+ )
1753
+ _required_string(
1754
+ candidate["candidate_id"],
1755
+ "request.payload.candidate_evidence.candidate_id",
1756
+ maximum=128,
1757
+ )
1758
+ for name in (
1759
+ "candidate_digest",
1760
+ "plan_digest",
1761
+ "artifact_scope_digest",
1762
+ "inspection_digest",
1763
+ ):
1764
+ _digest(candidate[name], f"request.payload.candidate_evidence.{name}")
1765
+ candidate_contract_version = _required_string(
1766
+ candidate["candidate_contract_version"],
1767
+ "request.payload.candidate_evidence.candidate_contract_version",
1768
+ maximum=128,
1769
+ )
1770
+ if not _SEMANTIC_VERSION.fullmatch(candidate_contract_version):
1771
+ raise RuntimeContractError(
1772
+ "request.payload.candidate_evidence.candidate_contract_version "
1773
+ "must be a canonical semantic version"
1774
+ )
1775
+ artifact_digests = _object(
1776
+ candidate["artifact_digests"],
1777
+ "request.payload.candidate_evidence.artifact_digests",
1778
+ )
1779
+ expected_kinds = {
1780
+ "candidate_parquet",
1781
+ "table_preview",
1782
+ "profile",
1783
+ "quality_report",
1784
+ "lineage",
1785
+ "candidate_evidence",
1786
+ "raw_snapshot",
1787
+ "table_package",
1788
+ }
1789
+ _exact_fields(
1790
+ artifact_digests,
1791
+ expected_kinds,
1792
+ "request.payload.candidate_evidence.artifact_digests",
1793
+ )
1794
+ for kind in sorted(expected_kinds):
1795
+ _digest(
1796
+ artifact_digests[kind],
1797
+ f"request.payload.candidate_evidence.artifact_digests.{kind}",
1798
+ )
1799
+ inspection = _object(
1800
+ candidate["inspection"],
1801
+ "request.payload.candidate_evidence.inspection",
1802
+ )
1803
+ if _sha256(_canonical_json_bytes(inspection)) != candidate["inspection_digest"]:
1804
+ raise RuntimeContractError(
1805
+ "request.payload.candidate_evidence.inspection_digest mismatches inspection"
1806
+ )
1807
+
1808
+ requirements = _object(payload["requirements"], "request.payload.requirements")
1809
+ _exact_fields(
1810
+ requirements,
1811
+ {
1812
+ "requirements_id",
1813
+ "output_grain",
1814
+ "validation_policy_digest",
1815
+ "release_policy_digest",
1816
+ "required_check_ids",
1817
+ },
1818
+ "request.payload.requirements",
1819
+ )
1820
+ _required_string(
1821
+ requirements["requirements_id"],
1822
+ "request.payload.requirements.requirements_id",
1823
+ maximum=128,
1824
+ )
1825
+ _string_array(
1826
+ requirements["output_grain"],
1827
+ "request.payload.requirements.output_grain",
1828
+ nonempty=True,
1829
+ maximum=64,
1830
+ )
1831
+ _digest(
1832
+ requirements["validation_policy_digest"],
1833
+ "request.payload.requirements.validation_policy_digest",
1834
+ )
1835
+ _digest(
1836
+ requirements["release_policy_digest"],
1837
+ "request.payload.requirements.release_policy_digest",
1838
+ )
1839
+ _string_array(
1840
+ requirements["required_check_ids"],
1841
+ "request.payload.requirements.required_check_ids",
1842
+ nonempty=True,
1843
+ maximum=256,
1844
+ )
1845
+
1846
+
1847
+ def _validate_role_input(role: str, payload: Mapping[str, Any]) -> None:
1848
+ if role == "question_framer":
1849
+ _exact_fields(payload, {"question"}, "request.payload", optional={"context"})
1850
+ _required_string(payload["question"], "request.payload.question", maximum=10_000)
1851
+ if "context" in payload:
1852
+ _object(payload["context"], "request.payload.context")
1853
+ return
1854
+ input_fields = {
1855
+ "feasibility_analyst": ({"framed_question"}, {"context"}),
1856
+ "data_scout": ({"requirements"}, {"context"}),
1857
+ "source_auditor": ({"source", "requirements"}, set()),
1858
+ "plan_proposer": ({"requirements", "sources"}, set()),
1859
+ "semantic_reviewer": ({"candidate_evidence", "requirements"}, set()),
1860
+ "repair_proposer": ({"finding", "candidate_evidence"}, set()),
1861
+ }
1862
+ required, optional = input_fields[role]
1863
+ _exact_fields(payload, required, "request.payload", optional=optional)
1864
+ for name in required:
1865
+ value = payload[name]
1866
+ if name == "sources":
1867
+ sources = _array(value, f"request.payload.{name}", nonempty=True, maximum=256)
1868
+ for index, source in enumerate(sources):
1869
+ _identifier_object(
1870
+ source,
1871
+ f"request.payload.{name}[{index}]",
1872
+ "source_id",
1873
+ )
1874
+ else:
1875
+ identifier_field = {
1876
+ "framed_question": "question_id",
1877
+ "requirements": "requirements_id",
1878
+ "source": "source_id",
1879
+ "candidate_evidence": "candidate_id",
1880
+ "finding": "finding_id",
1881
+ }[name]
1882
+ _identifier_object(value, f"request.payload.{name}", identifier_field)
1883
+ if "context" in payload:
1884
+ _object(payload["context"], "request.payload.context")
1885
+
1886
+
1887
+ def _validate_request_response_semantics(
1888
+ request: AgentRequest,
1889
+ response: AgentResponse,
1890
+ ) -> None:
1891
+ if response.role != request.role:
1892
+ raise RuntimeContractError("response.role does not match request.role")
1893
+ if request.role == "source_auditor":
1894
+ requested_source_id = request.payload["source"]["source_id"]
1895
+ if response.payload["source_id"] != requested_source_id:
1896
+ raise RuntimeContractError(
1897
+ "source_auditor response.source_id does not match request source_id"
1898
+ )
1899
+ elif request.role == "plan_proposer":
1900
+ requested_source_ids = [source["source_id"] for source in request.payload["sources"]]
1901
+ if response.payload["plan"]["source_ids"] != requested_source_ids:
1902
+ raise RuntimeContractError(
1903
+ "plan_proposer response source_ids do not exactly match request sources"
1904
+ )
1905
+ elif request.role == "repair_proposer":
1906
+ requested_finding_id = request.payload["finding"]["finding_id"]
1907
+ if response.payload["repair"]["finding_ids"] != [requested_finding_id]:
1908
+ raise RuntimeContractError(
1909
+ "repair_proposer response finding_ids do not exactly match request finding"
1910
+ )
1911
+
1912
+
1913
+ def _validate_role_output(role: str, payload: Mapping[str, Any]) -> None:
1914
+ if role == "question_framer":
1915
+ _exact_fields(
1916
+ payload,
1917
+ {
1918
+ "question",
1919
+ "population",
1920
+ "time_range",
1921
+ "output_grain",
1922
+ "success_criteria",
1923
+ },
1924
+ "response.payload",
1925
+ )
1926
+ _required_string(payload["question"], "response.payload.question", maximum=10_000)
1927
+ _required_string(payload["population"], "response.payload.population", maximum=10_000)
1928
+ time_range = _object(payload["time_range"], "response.payload.time_range")
1929
+ _exact_fields(time_range, {"start", "end"}, "response.payload.time_range")
1930
+ _half_open_dates(
1931
+ time_range["start"],
1932
+ time_range["end"],
1933
+ "response.payload.time_range",
1934
+ )
1935
+ _string_array(
1936
+ payload["output_grain"],
1937
+ "response.payload.output_grain",
1938
+ nonempty=True,
1939
+ maximum=64,
1940
+ )
1941
+ _string_array(
1942
+ payload["success_criteria"],
1943
+ "response.payload.success_criteria",
1944
+ nonempty=True,
1945
+ maximum=128,
1946
+ )
1947
+ return
1948
+ output_fields = {
1949
+ "feasibility_analyst": {"decision", "rationale"},
1950
+ "data_scout": {"candidates"},
1951
+ "source_auditor": {"source_id", "assessment"},
1952
+ "plan_proposer": {"plan"},
1953
+ "semantic_reviewer": {"findings"},
1954
+ "repair_proposer": {"repair"},
1955
+ }
1956
+ _exact_fields(payload, output_fields[role], "response.payload")
1957
+ if role == "feasibility_analyst":
1958
+ decision = _required_string(payload["decision"], "response.payload.decision", maximum=64)
1959
+ if decision not in {"feasible", "conditionally_feasible", "not_feasible"}:
1960
+ raise RuntimeContractError("response.payload.decision is unsupported")
1961
+ _string_array(
1962
+ payload["rationale"],
1963
+ "response.payload.rationale",
1964
+ nonempty=True,
1965
+ maximum=128,
1966
+ )
1967
+ elif role == "data_scout":
1968
+ candidates = _array(
1969
+ payload["candidates"],
1970
+ "response.payload.candidates",
1971
+ nonempty=True,
1972
+ maximum=256,
1973
+ )
1974
+ source_ids: list[str] = []
1975
+ for index, candidate in enumerate(candidates):
1976
+ source_ids.append(
1977
+ _validate_source_candidate(
1978
+ candidate,
1979
+ f"response.payload.candidates[{index}]",
1980
+ )
1981
+ )
1982
+ _require_unique(source_ids, "response.payload.candidates source IDs")
1983
+ elif role == "source_auditor":
1984
+ _identifier(payload["source_id"], "response.payload.source_id")
1985
+ _validate_source_assessment(
1986
+ payload["assessment"],
1987
+ "response.payload.assessment",
1988
+ )
1989
+ elif role == "plan_proposer":
1990
+ _validate_plan(payload["plan"], "response.payload.plan")
1991
+ elif role == "semantic_reviewer":
1992
+ findings = _array(payload["findings"], "response.payload.findings", maximum=1_024)
1993
+ finding_ids: list[str] = []
1994
+ for index, finding in enumerate(findings):
1995
+ finding_ids.append(
1996
+ _validate_semantic_finding(
1997
+ finding,
1998
+ f"response.payload.findings[{index}]",
1999
+ )
2000
+ )
2001
+ _require_unique(finding_ids, "response.payload.findings finding IDs")
2002
+ else:
2003
+ _validate_repair(payload["repair"], "response.payload.repair")
2004
+
2005
+
2006
+ def _identifier_object(value: Any, path: str, field: str) -> Mapping[str, Any]:
2007
+ item = _object(value, path)
2008
+ _exact_fields(item, {field}, path)
2009
+ _required_string(item[field], f"{path}.{field}", maximum=128)
2010
+ return item
2011
+
2012
+
2013
+ def _half_open_dates(start: Any, end: Any, path: str) -> None:
2014
+ start_text = _required_string(start, f"{path}.start", maximum=10)
2015
+ end_text = _required_string(end, f"{path}.end", maximum=10)
2016
+ if not _CANONICAL_DATE.fullmatch(start_text) or not _CANONICAL_DATE.fullmatch(end_text):
2017
+ raise RuntimeContractError(f"{path} must contain canonical ISO dates")
2018
+ try:
2019
+ start_date = date.fromisoformat(start_text)
2020
+ end_date = date.fromisoformat(end_text)
2021
+ except ValueError as exc:
2022
+ raise RuntimeContractError(f"{path} must contain valid ISO dates") from exc
2023
+ if end_date <= start_date:
2024
+ raise RuntimeContractError(f"{path} must be a non-empty half-open range")
2025
+
2026
+
2027
+ def _canonical_utc_datetime(value: Any, path: str) -> datetime:
2028
+ text = _required_string(value, path, maximum=64)
2029
+ if not _CANONICAL_UTC.fullmatch(text):
2030
+ raise RuntimeContractError(f"{path} must be a canonical UTC timestamp ending in Z")
2031
+ try:
2032
+ parsed = datetime.fromisoformat(text[:-1] + "+00:00")
2033
+ except ValueError as exc:
2034
+ raise RuntimeContractError(f"{path} must be a valid UTC timestamp") from exc
2035
+ if parsed.tzinfo != UTC:
2036
+ raise RuntimeContractError(f"{path} must be UTC")
2037
+ return parsed
2038
+
2039
+
2040
+ def _enum_string(value: Any, path: str, allowed: frozenset[str]) -> str:
2041
+ result = _required_string(value, path, maximum=128)
2042
+ if result not in allowed:
2043
+ raise RuntimeContractError(f"{path} is unsupported")
2044
+ return result
2045
+
2046
+
2047
+ def _validate_source_candidate(value: Any, path: str) -> str:
2048
+ item = _object(value, path)
2049
+ _exact_fields(
2050
+ item,
2051
+ {
2052
+ "source_id",
2053
+ "name",
2054
+ "source_class",
2055
+ "locator",
2056
+ "observed_at",
2057
+ "coverage",
2058
+ "delay",
2059
+ "liveness",
2060
+ "rights_status",
2061
+ "evidence",
2062
+ },
2063
+ path,
2064
+ )
2065
+ source_id = _identifier(item["source_id"], f"{path}.source_id")
2066
+ _required_string(item["name"], f"{path}.name", maximum=512)
2067
+ # Coarse source-class family; see the vocabulary map in sources/contracts.py.
2068
+ _enum_string(
2069
+ item["source_class"],
2070
+ f"{path}.source_class",
2071
+ frozenset({"external", "user"}),
2072
+ )
2073
+ _required_string(item["locator"], f"{path}.locator", maximum=2_048)
2074
+ observed_at = _canonical_utc_datetime(item["observed_at"], f"{path}.observed_at")
2075
+ coverage = _object(item["coverage"], f"{path}.coverage")
2076
+ _exact_fields(coverage, {"start", "end", "population"}, f"{path}.coverage")
2077
+ _half_open_dates(coverage["start"], coverage["end"], f"{path}.coverage")
2078
+ _required_string(coverage["population"], f"{path}.coverage.population", maximum=1_000)
2079
+ delay_observed_at = _validate_delay(item["delay"], f"{path}.delay")
2080
+ liveness_checked_at = _validate_liveness(item["liveness"], f"{path}.liveness")
2081
+ if delay_observed_at > observed_at:
2082
+ raise RuntimeContractError(f"{path}.delay.observed_at cannot follow observed_at")
2083
+ if liveness_checked_at > observed_at:
2084
+ raise RuntimeContractError(f"{path}.liveness.checked_at cannot follow observed_at")
2085
+ # Spelling is shared with local_contracts and sources/contracts; see the vocabulary map.
2086
+ _enum_string(
2087
+ item["rights_status"],
2088
+ f"{path}.rights_status",
2089
+ frozenset({"approved", "conditional", "unclear", "prohibited"}),
2090
+ )
2091
+ _string_array(item["evidence"], f"{path}.evidence", nonempty=True, maximum=64)
2092
+ return source_id
2093
+
2094
+
2095
+ def _validate_delay(value: Any, path: str) -> datetime:
2096
+ delay = _object(value, path)
2097
+ _exact_fields(delay, {"typical_seconds", "maximum_seconds", "observed_at"}, path)
2098
+ typical = _nonnegative_int(delay["typical_seconds"], f"{path}.typical_seconds")
2099
+ maximum = _nonnegative_int(delay["maximum_seconds"], f"{path}.maximum_seconds")
2100
+ if maximum < typical:
2101
+ raise RuntimeContractError(f"{path}.maximum_seconds cannot be below typical_seconds")
2102
+ return _canonical_utc_datetime(delay["observed_at"], f"{path}.observed_at")
2103
+
2104
+
2105
+ def _validate_liveness(value: Any, path: str) -> datetime:
2106
+ liveness = _object(value, path)
2107
+ _exact_fields(liveness, {"status", "checked_at"}, path)
2108
+ # unknown has no local_contracts counterpart; see the vocabulary map in sources/contracts.py.
2109
+ _enum_string(
2110
+ liveness["status"],
2111
+ f"{path}.status",
2112
+ frozenset({"live", "delayed", "dead", "unknown"}),
2113
+ )
2114
+ return _canonical_utc_datetime(liveness["checked_at"], f"{path}.checked_at")
2115
+
2116
+
2117
+ def _validate_source_assessment(value: Any, path: str) -> None:
2118
+ assessment = _object(value, path)
2119
+ _exact_fields(
2120
+ assessment,
2121
+ {
2122
+ "fitness",
2123
+ "coverage",
2124
+ "delay",
2125
+ "liveness",
2126
+ "rights_status",
2127
+ "data_classification",
2128
+ "retention",
2129
+ "evidence",
2130
+ "rejection_codes",
2131
+ },
2132
+ path,
2133
+ )
2134
+ # Stage-specific triplet; see the vocabulary map in sources/contracts.py.
2135
+ _enum_string(
2136
+ assessment["fitness"],
2137
+ f"{path}.fitness",
2138
+ frozenset({"fit", "conditional", "reject"}),
2139
+ )
2140
+ coverage = _object(assessment["coverage"], f"{path}.coverage")
2141
+ _exact_fields(coverage, {"start", "end", "population"}, f"{path}.coverage")
2142
+ _half_open_dates(coverage["start"], coverage["end"], f"{path}.coverage")
2143
+ _required_string(coverage["population"], f"{path}.coverage.population", maximum=1_000)
2144
+ delay_observed_at = _validate_delay(assessment["delay"], f"{path}.delay")
2145
+ liveness_checked_at = _validate_liveness(assessment["liveness"], f"{path}.liveness")
2146
+ if delay_observed_at > liveness_checked_at:
2147
+ raise RuntimeContractError(f"{path}.delay.observed_at cannot follow liveness.checked_at")
2148
+ # Spelling is shared with local_contracts and sources/contracts; see the vocabulary map.
2149
+ _enum_string(
2150
+ assessment["rights_status"],
2151
+ f"{path}.rights_status",
2152
+ frozenset({"approved", "conditional", "unclear", "prohibited"}),
2153
+ )
2154
+ # sources/contracts adds a terminal rejected state; see the vocabulary map.
2155
+ _enum_string(
2156
+ assessment["data_classification"],
2157
+ f"{path}.data_classification",
2158
+ frozenset({"public", "internal", "confidential", "restricted"}),
2159
+ )
2160
+ retention = _object(assessment["retention"], f"{path}.retention")
2161
+ _exact_fields(
2162
+ retention,
2163
+ {"raw_days", "derived_days", "tombstone_required"},
2164
+ f"{path}.retention",
2165
+ )
2166
+ _nonnegative_int(retention["raw_days"], f"{path}.retention.raw_days")
2167
+ _nonnegative_int(retention["derived_days"], f"{path}.retention.derived_days")
2168
+ if type(retention["tombstone_required"]) is not bool:
2169
+ raise RuntimeContractError(f"{path}.retention.tombstone_required must be a boolean")
2170
+ _string_array(assessment["evidence"], f"{path}.evidence", nonempty=True, maximum=128)
2171
+ _string_array(
2172
+ assessment["rejection_codes"],
2173
+ f"{path}.rejection_codes",
2174
+ nonempty=False,
2175
+ maximum=64,
2176
+ )
2177
+
2178
+
2179
+ def _validate_operation(value: Any, path: str) -> tuple[str, tuple[str, ...], tuple[str, ...]]:
2180
+ operation = _object(value, path)
2181
+ _exact_fields(
2182
+ operation,
2183
+ {"operation_id", "kind", "inputs", "outputs", "parameters"},
2184
+ path,
2185
+ )
2186
+ operation_id = _identifier(operation["operation_id"], f"{path}.operation_id")
2187
+ _enum_string(
2188
+ operation["kind"],
2189
+ f"{path}.kind",
2190
+ frozenset(
2191
+ {
2192
+ "profile",
2193
+ "clean",
2194
+ "normalize",
2195
+ "resolve",
2196
+ "join",
2197
+ "label",
2198
+ "validate",
2199
+ }
2200
+ ),
2201
+ )
2202
+ inputs = _string_array(operation["inputs"], f"{path}.inputs", nonempty=True, maximum=128)
2203
+ outputs = _string_array(operation["outputs"], f"{path}.outputs", nonempty=True, maximum=128)
2204
+ _object(operation["parameters"], f"{path}.parameters")
2205
+ return operation_id, inputs, outputs
2206
+
2207
+
2208
+ def _validate_plan(value: Any, path: str) -> None:
2209
+ plan = _object(value, path)
2210
+ _exact_fields(
2211
+ plan,
2212
+ {
2213
+ "plan_id",
2214
+ "source_ids",
2215
+ "output_grain",
2216
+ "operations",
2217
+ "validation_rules",
2218
+ "temporal_policy",
2219
+ },
2220
+ path,
2221
+ )
2222
+ _identifier(plan["plan_id"], f"{path}.plan_id")
2223
+ source_ids = _string_array(plan["source_ids"], f"{path}.source_ids", nonempty=True, maximum=256)
2224
+ for index, source_id in enumerate(source_ids):
2225
+ _identifier(source_id, f"{path}.source_ids[{index}]")
2226
+ _string_array(plan["output_grain"], f"{path}.output_grain", nonempty=True, maximum=128)
2227
+ operations = _array(plan["operations"], f"{path}.operations", nonempty=True, maximum=512)
2228
+ operation_ids: list[str] = []
2229
+ available_inputs = set(source_ids)
2230
+ referenced_sources: set[str] = set()
2231
+ for index, operation in enumerate(operations):
2232
+ operation_id, inputs, outputs = _validate_operation(
2233
+ operation, f"{path}.operations[{index}]"
2234
+ )
2235
+ if operation_id in operation_ids:
2236
+ raise RuntimeContractError(f"{path}.operations operation IDs must be unique")
2237
+ operation_ids.append(operation_id)
2238
+ unknown_inputs = set(inputs) - available_inputs
2239
+ if unknown_inputs:
2240
+ raise RuntimeContractError(
2241
+ f"{path}.operations[{index}].inputs has unknown references {sorted(unknown_inputs)}"
2242
+ )
2243
+ referenced_sources.update(set(inputs) & set(source_ids))
2244
+ collisions = set(outputs) & available_inputs
2245
+ if collisions:
2246
+ raise RuntimeContractError(
2247
+ f"{path}.operations[{index}].outputs redefines references {sorted(collisions)}"
2248
+ )
2249
+ available_inputs.update(outputs)
2250
+ _require_unique(operation_ids, f"{path}.operations operation IDs")
2251
+ if referenced_sources != set(source_ids):
2252
+ raise RuntimeContractError(f"{path}.source_ids must all be referenced by operations")
2253
+ rules = _array(
2254
+ plan["validation_rules"],
2255
+ f"{path}.validation_rules",
2256
+ nonempty=True,
2257
+ maximum=512,
2258
+ )
2259
+ rule_ids: list[str] = []
2260
+ for index, value_rule in enumerate(rules):
2261
+ rule_path = f"{path}.validation_rules[{index}]"
2262
+ rule = _object(value_rule, rule_path)
2263
+ _exact_fields(rule, {"rule_id", "kind", "columns", "severity"}, rule_path)
2264
+ rule_ids.append(_identifier(rule["rule_id"], f"{rule_path}.rule_id"))
2265
+ _required_string(rule["kind"], f"{rule_path}.kind", maximum=128)
2266
+ _string_array(rule["columns"], f"{rule_path}.columns", nonempty=True, maximum=128)
2267
+ _enum_string(
2268
+ rule["severity"],
2269
+ f"{rule_path}.severity",
2270
+ frozenset({"blocker", "critical", "high", "medium", "low"}),
2271
+ )
2272
+ _require_unique(rule_ids, f"{path}.validation_rules rule IDs")
2273
+ temporal = _object(plan["temporal_policy"], f"{path}.temporal_policy")
2274
+ _exact_fields(
2275
+ temporal,
2276
+ {"cutoff", "event_time_column", "knowledge_time_column"},
2277
+ f"{path}.temporal_policy",
2278
+ )
2279
+ _canonical_utc_datetime(temporal["cutoff"], f"{path}.temporal_policy.cutoff")
2280
+ event_time_column = _required_string(
2281
+ temporal["event_time_column"],
2282
+ f"{path}.temporal_policy.event_time_column",
2283
+ maximum=128,
2284
+ )
2285
+ knowledge_time_column = _required_string(
2286
+ temporal["knowledge_time_column"],
2287
+ f"{path}.temporal_policy.knowledge_time_column",
2288
+ maximum=128,
2289
+ )
2290
+ if event_time_column == knowledge_time_column:
2291
+ raise RuntimeContractError(f"{path}.temporal_policy time columns must be distinct")
2292
+
2293
+
2294
+ def _validate_semantic_finding(value: Any, path: str) -> str:
2295
+ finding = _object(value, path)
2296
+ _exact_fields(
2297
+ finding,
2298
+ {
2299
+ "evaluator_finding_id",
2300
+ "severity",
2301
+ "category",
2302
+ "summary",
2303
+ "evidence_digest",
2304
+ "affected_artifact",
2305
+ "acceptance_conditions",
2306
+ "code",
2307
+ },
2308
+ path,
2309
+ )
2310
+ finding_id = _required_string(
2311
+ finding["evaluator_finding_id"],
2312
+ f"{path}.evaluator_finding_id",
2313
+ maximum=128,
2314
+ )
2315
+ if not _SCOPE_IDENTIFIER.fullmatch(finding_id):
2316
+ raise RuntimeContractError(f"{path}.evaluator_finding_id is not canonical")
2317
+ code = _required_string(finding["code"], f"{path}.code", maximum=128)
2318
+ if not _FINDING_CODE.fullmatch(code):
2319
+ raise RuntimeContractError(f"{path}.code is not canonical")
2320
+ _enum_string(
2321
+ finding["severity"],
2322
+ f"{path}.severity",
2323
+ frozenset({"blocker", "critical", "high", "medium", "low"}),
2324
+ )
2325
+ _enum_string(
2326
+ finding["category"],
2327
+ f"{path}.category",
2328
+ frozenset(
2329
+ {
2330
+ "source_fitness",
2331
+ "schema",
2332
+ "data_quality",
2333
+ "temporal_integrity",
2334
+ "join_cardinality",
2335
+ "lineage",
2336
+ "rights",
2337
+ "security",
2338
+ "reproducibility",
2339
+ "other",
2340
+ }
2341
+ ),
2342
+ )
2343
+ _required_string(finding["summary"], f"{path}.summary", maximum=2_000)
2344
+ evidence_digest = _required_string(
2345
+ finding["evidence_digest"],
2346
+ f"{path}.evidence_digest",
2347
+ maximum=71,
2348
+ )
2349
+ if not _WIRE_SHA256.fullmatch(evidence_digest):
2350
+ raise RuntimeContractError(f"{path}.evidence_digest is not a canonical wire digest")
2351
+ affected = _object(finding["affected_artifact"], f"{path}.affected_artifact")
2352
+ _exact_fields(
2353
+ affected,
2354
+ {"artifact_id", "kind", "content_digest"},
2355
+ f"{path}.affected_artifact",
2356
+ )
2357
+ artifact_id = _required_string(
2358
+ affected["artifact_id"],
2359
+ f"{path}.affected_artifact.artifact_id",
2360
+ maximum=36,
2361
+ )
2362
+ if not _UUID.fullmatch(artifact_id):
2363
+ raise RuntimeContractError(f"{path}.affected_artifact.artifact_id is not canonical")
2364
+ kind = _required_string(
2365
+ affected["kind"],
2366
+ f"{path}.affected_artifact.kind",
2367
+ maximum=64,
2368
+ )
2369
+ if not _ARTIFACT_KIND.fullmatch(kind):
2370
+ raise RuntimeContractError(f"{path}.affected_artifact.kind is not canonical")
2371
+ content_digest = _required_string(
2372
+ affected["content_digest"],
2373
+ f"{path}.affected_artifact.content_digest",
2374
+ maximum=71,
2375
+ )
2376
+ if not _WIRE_SHA256.fullmatch(content_digest):
2377
+ raise RuntimeContractError(
2378
+ f"{path}.affected_artifact.content_digest is not a canonical wire digest"
2379
+ )
2380
+ conditions = _string_array(
2381
+ finding["acceptance_conditions"],
2382
+ f"{path}.acceptance_conditions",
2383
+ nonempty=True,
2384
+ maximum=32,
2385
+ )
2386
+ if any(len(condition) > 1_000 for condition in conditions):
2387
+ raise RuntimeContractError(f"{path}.acceptance_conditions contains oversized text")
2388
+ return finding_id
2389
+
2390
+
2391
+ def _validate_repair(value: Any, path: str) -> None:
2392
+ repair = _object(value, path)
2393
+ _exact_fields(
2394
+ repair,
2395
+ {
2396
+ "repair_id",
2397
+ "finding_ids",
2398
+ "operations",
2399
+ "acceptance_evidence",
2400
+ "successor_required",
2401
+ },
2402
+ path,
2403
+ )
2404
+ _identifier(repair["repair_id"], f"{path}.repair_id")
2405
+ _string_array(repair["finding_ids"], f"{path}.finding_ids", nonempty=True, maximum=256)
2406
+ operations = _array(
2407
+ repair["operations"],
2408
+ f"{path}.operations",
2409
+ nonempty=True,
2410
+ maximum=256,
2411
+ )
2412
+ operation_ids: list[str] = []
2413
+ for index, value_operation in enumerate(operations):
2414
+ operation_path = f"{path}.operations[{index}]"
2415
+ operation = _object(value_operation, operation_path)
2416
+ _exact_fields(
2417
+ operation,
2418
+ {"operation_id", "kind", "target", "parameters"},
2419
+ operation_path,
2420
+ )
2421
+ operation_ids.append(
2422
+ _identifier(
2423
+ operation["operation_id"],
2424
+ f"{operation_path}.operation_id",
2425
+ )
2426
+ )
2427
+ _required_string(operation["kind"], f"{operation_path}.kind", maximum=128)
2428
+ _required_string(operation["target"], f"{operation_path}.target", maximum=512)
2429
+ _object(operation["parameters"], f"{operation_path}.parameters")
2430
+ _require_unique(operation_ids, f"{path}.operations operation IDs")
2431
+ _string_array(
2432
+ repair["acceptance_evidence"],
2433
+ f"{path}.acceptance_evidence",
2434
+ nonempty=True,
2435
+ maximum=128,
2436
+ )
2437
+ if repair["successor_required"] is not True:
2438
+ raise RuntimeContractError(f"{path}.successor_required must be true")
2439
+
2440
+
2441
+ def _strict_json_value(value: Any, path: str, *, depth: int, state: list[int]) -> Any:
2442
+ state[0] += 1
2443
+ if state[0] > 4_096:
2444
+ raise RuntimeContractError(f"{path} exceeds the structured payload node limit")
2445
+ if depth > 16:
2446
+ raise RuntimeContractError(f"{path} exceeds the structured payload depth limit")
2447
+ if value is None or type(value) is bool:
2448
+ return value
2449
+ if type(value) is int:
2450
+ if not -MAX_ACCOUNTING_INTEGER <= value <= MAX_ACCOUNTING_INTEGER:
2451
+ raise RuntimeContractError(f"{path} integer is outside the supported range")
2452
+ return value
2453
+ if isinstance(value, str):
2454
+ if len(value) > 100_000:
2455
+ raise RuntimeContractError(f"{path} string exceeds 100000 characters")
2456
+ return value
2457
+ if isinstance(value, Mapping):
2458
+ if len(value) > 1_024 or any(not isinstance(key, str) for key in value):
2459
+ raise RuntimeContractError(f"{path} must be a bounded object with string keys")
2460
+ return {
2461
+ key: _strict_json_value(item, f"{path}.{key}", depth=depth + 1, state=state)
2462
+ for key, item in value.items()
2463
+ }
2464
+ if isinstance(value, (list, tuple)):
2465
+ if len(value) > 1_024:
2466
+ raise RuntimeContractError(f"{path} array exceeds 1024 entries")
2467
+ return [
2468
+ _strict_json_value(item, f"{path}[{index}]", depth=depth + 1, state=state)
2469
+ for index, item in enumerate(value)
2470
+ ]
2471
+ raise RuntimeContractError(f"{path} contains unsupported value type {type(value).__name__}")
2472
+
2473
+
2474
+ def _canonical_json_bytes(value: Any) -> bytes:
2475
+ normalized = _strict_json_value(value, "value", depth=0, state=[0])
2476
+ return (
2477
+ json.dumps(
2478
+ normalized,
2479
+ ensure_ascii=False,
2480
+ allow_nan=False,
2481
+ sort_keys=True,
2482
+ separators=(",", ":"),
2483
+ ).encode("utf-8")
2484
+ + b"\n"
2485
+ )
2486
+
2487
+
2488
+ def _safe_response_hash(value: Any) -> str:
2489
+ try:
2490
+ raw = _canonical_json_bytes(value)
2491
+ except RuntimeContractError:
2492
+ raw = f"<unsupported:{type(value).__module__}.{type(value).__qualname__}>".encode()
2493
+ return _sha256(raw)
2494
+
2495
+
2496
+ def _provider_configuration_digest(adapter: ProviderAdapter) -> str:
2497
+ declared = getattr(adapter, "configuration_sha256", None)
2498
+ if declared is not None:
2499
+ return _digest(declared, "adapter.configuration_sha256")
2500
+ return _sha256(
2501
+ _canonical_json_bytes(
2502
+ {
2503
+ "provider": adapter.provider,
2504
+ "model_identifier": adapter.model_identifier,
2505
+ }
2506
+ )
2507
+ )
2508
+
2509
+
2510
+ def _prompt_digest(request: AgentRequest) -> str:
2511
+ return _sha256(
2512
+ _canonical_json_bytes(
2513
+ {
2514
+ "prompt_template_version": request.prompt_template_version,
2515
+ "payload_schema_version": request.payload_schema_version,
2516
+ "payload": request.payload,
2517
+ }
2518
+ )
2519
+ )
2520
+
2521
+
2522
+ def _reject_duplicate_pairs(pairs: Sequence[tuple[str, Any]]) -> dict[str, Any]:
2523
+ result: dict[str, Any] = {}
2524
+ for key, value in pairs:
2525
+ if key in result:
2526
+ raise RuntimeContractError(f"duplicate JSON key: {key!r}")
2527
+ result[key] = value
2528
+ return result
2529
+
2530
+
2531
+ def _reject_json_constant(value: str) -> Any:
2532
+ raise RuntimeContractError(f"unsupported JSON constant: {value}")
2533
+
2534
+
2535
+ def _object(value: Any, path: str) -> Mapping[str, Any]:
2536
+ if not isinstance(value, Mapping) or any(not isinstance(key, str) for key in value):
2537
+ raise RuntimeContractError(f"{path} must be an object with string keys")
2538
+ return value
2539
+
2540
+
2541
+ def _array(
2542
+ value: Any,
2543
+ path: str,
2544
+ *,
2545
+ nonempty: bool = False,
2546
+ maximum: int,
2547
+ ) -> Sequence[Any]:
2548
+ if not isinstance(value, list):
2549
+ raise RuntimeContractError(f"{path} must be an array")
2550
+ if nonempty and not value:
2551
+ raise RuntimeContractError(f"{path} cannot be empty")
2552
+ if len(value) > maximum:
2553
+ raise RuntimeContractError(f"{path} exceeds {maximum} entries")
2554
+ return value
2555
+
2556
+
2557
+ def _string_array(
2558
+ value: Any,
2559
+ path: str,
2560
+ *,
2561
+ nonempty: bool,
2562
+ maximum: int,
2563
+ ) -> tuple[str, ...]:
2564
+ values = _array(value, path, nonempty=nonempty, maximum=maximum)
2565
+ parsed = tuple(
2566
+ _required_string(item, f"{path}[{index}]", maximum=10_000)
2567
+ for index, item in enumerate(values)
2568
+ )
2569
+ _require_unique(parsed, path)
2570
+ return parsed
2571
+
2572
+
2573
+ def _exact_fields(
2574
+ data: Mapping[str, Any],
2575
+ required: set[str],
2576
+ path: str,
2577
+ *,
2578
+ optional: set[str] | None = None,
2579
+ ) -> None:
2580
+ optional = optional or set()
2581
+ missing = required - data.keys()
2582
+ extra = data.keys() - required - optional
2583
+ if missing or extra:
2584
+ raise RuntimeContractError(
2585
+ f"{path} has invalid fields; missing={sorted(missing)}, extra={sorted(extra)}"
2586
+ )
2587
+
2588
+
2589
+ def _required_string(value: Any, path: str, *, maximum: int) -> str:
2590
+ if not isinstance(value, str) or not value or value != value.strip():
2591
+ raise RuntimeContractError(f"{path} must be nonempty trimmed text")
2592
+ if len(value) > maximum:
2593
+ raise RuntimeContractError(f"{path} exceeds {maximum} characters")
2594
+ if any(ord(char) < 0x20 and char not in "\t\n\r" for char in value):
2595
+ raise RuntimeContractError(f"{path} contains a control character")
2596
+ return value
2597
+
2598
+
2599
+ def _identifier(value: Any, path: str) -> str:
2600
+ text = _required_string(value, path, maximum=128)
2601
+ if not _IDENTIFIER.fullmatch(text):
2602
+ raise RuntimeContractError(f"{path} must be a canonical lowercase identifier")
2603
+ return text
2604
+
2605
+
2606
+ def _version(value: Any, path: str) -> str:
2607
+ text = _required_string(value, path, maximum=128)
2608
+ if not _IDENTIFIER.fullmatch(text) or "." not in text:
2609
+ raise RuntimeContractError(f"{path} must be a canonical version identifier")
2610
+ return text
2611
+
2612
+
2613
+ def _digest(value: Any, path: str) -> str:
2614
+ if not isinstance(value, str) or not _SHA256.fullmatch(value):
2615
+ raise RuntimeContractError(f"{path} must be a lowercase SHA-256 digest")
2616
+ return value
2617
+
2618
+
2619
+ def _nonnegative_int(value: Any, path: str) -> int:
2620
+ if type(value) is not int or value < 0:
2621
+ raise RuntimeContractError(f"{path} must be a nonnegative integer")
2622
+ return value
2623
+
2624
+
2625
+ def _positive_int(value: Any, path: str) -> int:
2626
+ if type(value) is not int or value < 1:
2627
+ raise RuntimeContractError(f"{path} must be a positive integer")
2628
+ return value
2629
+
2630
+
2631
+ def _accounting_int(value: Any, path: str) -> int:
2632
+ _nonnegative_int(value, path)
2633
+ if value > MAX_ACCOUNTING_INTEGER:
2634
+ raise RuntimeContractError(f"{path} exceeds the signed 64-bit accounting range")
2635
+ return value
2636
+
2637
+
2638
+ def _positive_accounting_int(value: Any, path: str) -> int:
2639
+ _positive_int(value, path)
2640
+ if value > MAX_ACCOUNTING_INTEGER:
2641
+ raise RuntimeContractError(f"{path} exceeds the signed 64-bit accounting range")
2642
+ return value
2643
+
2644
+
2645
+ def _provider_accounting(value: Any, path: str) -> tuple[int, bool]:
2646
+ _nonnegative_int(value, f"provider.{path}")
2647
+ return min(value, MAX_ACCOUNTING_INTEGER), value > MAX_ACCOUNTING_INTEGER
2648
+
2649
+
2650
+ def _checked_add(left: int, right: int, path: str) -> int:
2651
+ result = left + right
2652
+ if result > MAX_ACCOUNTING_INTEGER:
2653
+ raise RuntimeContractError(f"{path} exceeds the signed 64-bit accounting range")
2654
+ return result
2655
+
2656
+
2657
+ def _require_unique(values: Sequence[str] | Any, path: str) -> None:
2658
+ sequence = tuple(values)
2659
+ if len(sequence) != len(set(sequence)):
2660
+ raise RuntimeContractError(f"{path} must be unique")
2661
+
2662
+
2663
+ def _sha256(value: bytes) -> str:
2664
+ return hashlib.sha256(value).hexdigest()
2665
+
2666
+
2667
+ def _stable_failure_code(error: Exception) -> str:
2668
+ text = str(error)
2669
+ for candidate in (
2670
+ "input_tokens_per_call",
2671
+ "output_tokens_per_call",
2672
+ "total_tokens_per_call",
2673
+ "cost_micros_per_call",
2674
+ "time_micros_per_call",
2675
+ "input_tokens_per_run",
2676
+ "output_tokens_per_run",
2677
+ "total_tokens_per_run",
2678
+ "cost_micros_per_run",
2679
+ "time_micros_per_run",
2680
+ "calls",
2681
+ ):
2682
+ if candidate in text:
2683
+ return f"{candidate}_exhausted"
2684
+ return "budget_reservation_denied"
2685
+
2686
+
2687
+ def _after_budget_code(exceeded: Sequence[str]) -> str:
2688
+ return f"{sorted(exceeded)[0]}_exceeded"
2689
+
2690
+
2691
+ def _monotonic_micros() -> int:
2692
+ import time
2693
+
2694
+ return time.monotonic_ns() // 1_000
2695
+
2696
+
2697
+ __all__ = [
2698
+ "AGENT_EVIDENCE_VERSION",
2699
+ "AGENT_REQUEST_VERSION",
2700
+ "AGENT_RESPONSE_VERSION",
2701
+ "AGENT_ROLES",
2702
+ "AGENT_RUNTIME_VERSION",
2703
+ "MAX_ACCOUNTING_INTEGER",
2704
+ "REPLAY_CATALOG_VERSION",
2705
+ "ROLE_PAYLOAD_SCHEMAS",
2706
+ "SEMANTIC_REVIEW_INPUT_VERSION",
2707
+ "SEMANTIC_REVIEW_OUTPUT_VERSION",
2708
+ "TOOL_POLICY_VERSION",
2709
+ "AgentCallCancelled",
2710
+ "AgentCallEvidence",
2711
+ "AgentCallFailure",
2712
+ "AgentCallResult",
2713
+ "AgentCallTimedOut",
2714
+ "AgentRequest",
2715
+ "AgentResponse",
2716
+ "AgentRuntime",
2717
+ "BudgetExceededAfterCall",
2718
+ "BudgetExceededBeforeCall",
2719
+ "BudgetLedger",
2720
+ "BudgetLimits",
2721
+ "BudgetSnapshot",
2722
+ "MalformedProviderResponse",
2723
+ "ProviderAdapter",
2724
+ "ProviderAgentRuntime",
2725
+ "ProviderCallFailed",
2726
+ "ProviderResult",
2727
+ "ProviderTimeoutError",
2728
+ "ReplayAgentRuntime",
2729
+ "ReplayAmbiguityError",
2730
+ "ReplayError",
2731
+ "ReplayMissError",
2732
+ "ReplayTamperError",
2733
+ "ReplayVersionError",
2734
+ "RuntimeContractError",
2735
+ "SecretReference",
2736
+ "TimingEvidence",
2737
+ "ToolCallEvidence",
2738
+ "ToolInvocation",
2739
+ "ToolPolicy",
2740
+ "ToolPolicyDenied",
2741
+ "ToolPolicyError",
2742
+ "Usage",
2743
+ ]