failproofai 1.0.3 → 1.0.4-beta.2

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 (251) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +5 -5
  3. package/.next/standalone/.next/prerender-manifest.json +5 -5
  4. package/.next/standalone/.next/required-server-files.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +2 -2
  6. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  8. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  11. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
  12. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  13. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +2 -2
  15. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  16. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  17. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  18. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  19. package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
  20. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
  21. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +14 -14
  22. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  23. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/audit/invite/route_client-reference-manifest.js +1 -1
  25. package/.next/standalone/.next/server/app/api/audit/run/route.js +4 -4
  26. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/audit/run/route_client-reference-manifest.js +1 -1
  28. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/audit/status/route_client-reference-manifest.js +1 -1
  30. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/auth/login-request/route_client-reference-manifest.js +1 -1
  32. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/auth/login-verify/route_client-reference-manifest.js +1 -1
  34. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/api/auth/logout/route_client-reference-manifest.js +1 -1
  36. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/api/auth/status/route_client-reference-manifest.js +1 -1
  38. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  39. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route_client-reference-manifest.js +1 -1
  40. package/.next/standalone/.next/server/app/audit/page/build-manifest.json +2 -2
  41. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +37 -4
  42. package/.next/standalone/.next/server/app/audit/page.js +2 -2
  43. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  45. package/.next/standalone/.next/server/app/index.html +1 -1
  46. package/.next/standalone/.next/server/app/index.rsc +15 -15
  47. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
  48. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  49. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  50. package/.next/standalone/.next/server/app/page/build-manifest.json +2 -2
  51. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  52. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  53. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  54. package/.next/standalone/.next/server/app/policies/page/build-manifest.json +2 -2
  55. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
  56. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  57. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  58. package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +2 -2
  59. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  60. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  61. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  62. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +2 -2
  63. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  64. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  65. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  66. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  67. package/.next/standalone/.next/server/app/projects/page/build-manifest.json +2 -2
  68. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  69. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  70. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  71. package/.next/standalone/.next/server/app/settings/page/build-manifest.json +2 -2
  72. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -4
  73. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  74. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  75. package/.next/standalone/.next/server/chunks/{[externals]__0-9ti7z._.js → [externals]__1lh7m5d._.js} +1 -1
  76. package/.next/standalone/.next/server/chunks/{[externals]__0u3wrvq._.js → [externals]__1rqkg_y._.js} +1 -1
  77. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
  78. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1adacul._.js +1 -1
  79. package/.next/standalone/.next/server/chunks/{_1-akg6w._.js → _08w6xzm._.js} +2 -2
  80. package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
  81. package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
  82. package/.next/standalone/.next/server/chunks/_1ek68ln._.js +4 -4
  83. package/.next/standalone/.next/server/chunks/lib_telemetry_ts_0dctyyw._.js +1 -1
  84. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_01r25oi._.js +2 -2
  85. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_09z9-p7._.js +2 -2
  86. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  87. package/.next/standalone/.next/server/chunks/src_hooks_fp-home_ts_09kv0bn._.js +1 -1
  88. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013jr2b._.js +2 -2
  89. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +2 -2
  90. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +2 -2
  91. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +2 -2
  92. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +2 -2
  93. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0oa1lav._.js +1 -1
  94. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p-5p8u._.js +2 -2
  95. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0rgu2r3._.js +3 -0
  96. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s740oi._.js +2 -2
  97. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +2 -2
  98. package/.next/standalone/.next/server/chunks/ssr/_0-oij9d._.js +23 -0
  99. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
  100. package/.next/standalone/.next/server/chunks/ssr/_0l2hi_d._.js +3 -0
  101. package/.next/standalone/.next/server/chunks/ssr/_0oq1dh6._.js +3 -0
  102. package/.next/standalone/.next/server/chunks/ssr/_1es2j7i._.js +47 -13
  103. package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +1 -1
  104. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +2 -2
  105. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -69
  106. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_rerun-button_tsx_0blihgw._.js +3 -0
  107. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  108. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
  109. package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +1 -1
  110. package/.next/standalone/.next/server/chunks/ssr/node_modules_13n822a._.js +1 -1
  111. package/.next/standalone/.next/server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_11bnuzn._.js +2 -2
  112. package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +1 -1
  113. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589g._.js +1 -1
  114. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +1 -1
  115. package/.next/standalone/.next/server/middleware-build-manifest.js +5 -5
  116. package/.next/standalone/.next/server/pages/404.html +1 -1
  117. package/.next/standalone/.next/server/pages/500.html +1 -1
  118. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  119. package/.next/standalone/.next/server/server-reference-manifest.json +54 -21
  120. package/.next/standalone/.next/static/chunks/010bv1w6j171t.js +1 -0
  121. package/.next/standalone/.next/static/chunks/04r6ch8uf_n8m.js +1 -0
  122. package/.next/standalone/.next/static/chunks/{29-iqnp9v_axt.css → 0o-hh5_turzlz.css} +1 -1
  123. package/.next/standalone/.next/static/chunks/{40nr6ehj4p3cw.css → 0qmsunv04x4na.css} +1 -1
  124. package/.next/standalone/.next/static/chunks/{0ollg-nnu3t3e.js → 0wz8yftk18ts2.js} +1 -1
  125. package/.next/standalone/.next/static/chunks/0zebh1n9jkfbt.js +1 -0
  126. package/.next/standalone/.next/static/chunks/1eyl7fzkmnkf_.js +1 -0
  127. package/.next/standalone/.next/static/chunks/1pb1oztsbwcss.js +1 -0
  128. package/.next/standalone/.next/static/chunks/{1-_zkmnm-jwei.js → 2aquitk72k2op.js} +1 -1
  129. package/.next/standalone/.next/static/chunks/2bi_1y0a_smt7.js +6 -0
  130. package/.next/standalone/.next/static/chunks/{42yfkhekx440s.js → 2ej3b8gk5ittu.js} +1 -1
  131. package/.next/standalone/.next/static/chunks/{354-n7p7labpn.js → 2zafgs90r_leq.js} +1 -1
  132. package/.next/standalone/.next/static/chunks/{2xm5mj1d21b4n.js → 32spub4wqjem-.js} +1 -1
  133. package/.next/standalone/.next/static/chunks/{1a6u7l3giejch.js → 3m4upvybtrexd.js} +1 -1
  134. package/.next/standalone/.next/static/chunks/{2k9f4tyv04809.css → 3uyhmm01g64k4.css} +1 -0
  135. package/.next/standalone/.next/static/chunks/{turbopack-2x-o099q64i8h.js → turbopack-3s3c-u_u_l0u9.js} +1 -1
  136. package/.next/standalone/.opencode/plugins/failproofai.mjs +51 -27
  137. package/.next/standalone/app/actions/get-leaks.ts +100 -0
  138. package/.next/standalone/app/audit/_components/audit-dashboard.tsx +112 -33
  139. package/.next/standalone/app/audit/_components/audit-poster.tsx +29 -10
  140. package/.next/standalone/app/audit/_components/come-back-better-section.tsx +6 -1
  141. package/.next/standalone/app/audit/_components/empty-state.tsx +4 -1
  142. package/.next/standalone/app/audit/_components/how-to-improve-section.tsx +19 -5
  143. package/.next/standalone/app/audit/_components/leak-section.tsx +179 -0
  144. package/.next/standalone/app/audit/_components/share-templates.ts +110 -44
  145. package/.next/standalone/app/audit/audit-styles.css +114 -1
  146. package/.next/standalone/fp-cloud-cli/CHANGELOG.md +26 -3
  147. package/.next/standalone/fp-cloud-cli/fp_cli/_click_compat.py +31 -8
  148. package/.next/standalone/fp-cloud-cli/fp_cli/permissions.py +1 -0
  149. package/.next/standalone/fp-cloud-cli/pyproject.toml +9 -2
  150. package/.next/standalone/fp-cloud-cli/tests/test_click_compat.py +54 -0
  151. package/.next/standalone/fp-cloud-cli/uv.lock +9 -12
  152. package/.next/standalone/lib/auth/api-server-client.ts +28 -0
  153. package/.next/standalone/lib/claude-sessions.ts +101 -27
  154. package/.next/standalone/node_modules/@next/env/package.json +1 -1
  155. package/.next/standalone/node_modules/next/dist/build/swc/index.js +1 -1
  156. package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo-experimental.runtime.prod.js +1 -1
  157. package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo.runtime.prod.js +1 -1
  158. package/.next/standalone/node_modules/next/dist/compiled/next-server/app-route-turbo.runtime.prod.js +1 -1
  159. package/.next/standalone/node_modules/next/dist/compiled/next-server/pages-turbo.runtime.prod.js +5 -5
  160. package/.next/standalone/node_modules/next/dist/experimental/testmode/fetch.js +10 -2
  161. package/.next/standalone/node_modules/next/dist/lib/patch-incorrect-lockfile.js +3 -3
  162. package/.next/standalone/node_modules/next/dist/lib/typescript/runTypeScriptCli.js +2 -2
  163. package/.next/standalone/node_modules/next/dist/lib/verify-typescript-setup.js +3 -1
  164. package/.next/standalone/node_modules/next/dist/server/config.js +1 -1
  165. package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-turbopack.js +2 -2
  166. package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-webpack.js +1 -1
  167. package/.next/standalone/node_modules/next/dist/server/lib/app-info-log.js +1 -1
  168. package/.next/standalone/node_modules/next/dist/server/lib/incremental-cache/file-system-cache.js +17 -4
  169. package/.next/standalone/node_modules/next/dist/server/lib/start-server.js +1 -1
  170. package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/escape-path-delimiters.js +1 -1
  171. package/.next/standalone/node_modules/next/dist/telemetry/anonymous-meta.js +1 -1
  172. package/.next/standalone/node_modules/next/dist/telemetry/events/swc-load-failure.js +1 -1
  173. package/.next/standalone/node_modules/next/dist/telemetry/events/version.js +2 -2
  174. package/.next/standalone/node_modules/next/package.json +11 -11
  175. package/.next/standalone/package.json +14 -13
  176. package/.next/standalone/sdk/python/CHANGELOG.md +155 -0
  177. package/.next/standalone/sdk/python/README.md +8 -0
  178. package/.next/standalone/sdk/python/examples/evaluator_worker.py +121 -0
  179. package/.next/standalone/sdk/python/failproofai_sdk/_version.py +1 -1
  180. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/__init__.py +101 -0
  181. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/__main__.py +49 -0
  182. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/_sandbox_runner.py +63 -0
  183. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/authoring.py +404 -0
  184. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/client.py +299 -0
  185. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/protocol.py +754 -0
  186. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/runtime.py +936 -0
  187. package/.next/standalone/sdk/python/failproofai_sdk/evaluator/source.py +684 -0
  188. package/.next/standalone/sdk/python/tests/fixtures/evaluator_v2/README.md +28 -0
  189. package/.next/standalone/sdk/python/tests/fixtures/evaluator_v2/contract.json +252 -0
  190. package/.next/standalone/sdk/python/tests/test_evaluator_authoring.py +124 -0
  191. package/.next/standalone/sdk/python/tests/test_evaluator_client.py +253 -0
  192. package/.next/standalone/sdk/python/tests/test_evaluator_example.py +35 -0
  193. package/.next/standalone/sdk/python/tests/test_evaluator_http_e2e.py +636 -0
  194. package/.next/standalone/sdk/python/tests/test_evaluator_main.py +47 -0
  195. package/.next/standalone/sdk/python/tests/test_evaluator_protocol.py +246 -0
  196. package/.next/standalone/sdk/python/tests/test_evaluator_review_fixes.py +164 -0
  197. package/.next/standalone/sdk/python/tests/test_evaluator_runtime.py +1103 -0
  198. package/.next/standalone/sdk/python/tests/test_evaluator_source.py +430 -0
  199. package/.next/standalone/sdk/python/tests/test_zero_dependencies.py +17 -0
  200. package/.next/standalone/sdk/python/uv.lock +29 -29
  201. package/.next/standalone/server.js +1 -1
  202. package/README.md +13 -58
  203. package/dist/cli.mjs +5368 -8756
  204. package/dist/worker.mjs +539 -312
  205. package/lib/auth/api-server-client.ts +28 -0
  206. package/lib/claude-sessions.ts +101 -27
  207. package/package.json +14 -13
  208. package/pi-extension/index.ts +42 -4
  209. package/scripts/changelog-open.py +115 -0
  210. package/src/audit/cli.ts +134 -0
  211. package/src/audit/desktop-notify.ts +420 -0
  212. package/src/audit/harm-report.ts +101 -0
  213. package/src/audit/index.ts +141 -13
  214. package/src/audit/leak-fingerprint.ts +200 -0
  215. package/src/audit/leak-notice.ts +161 -0
  216. package/src/audit/leak-record.ts +232 -0
  217. package/src/audit/leak-scan.ts +292 -0
  218. package/src/audit/leak-store.ts +217 -0
  219. package/src/audit/macos-notifier.ts +310 -0
  220. package/src/audit/redact-example.ts +266 -14
  221. package/src/audit/report-harm.ts +12 -1
  222. package/src/audit/report.ts +6 -3
  223. package/src/audit/schedule-cli.ts +37 -0
  224. package/src/audit/scoring.ts +49 -0
  225. package/src/audit/types.ts +41 -0
  226. package/src/hooks/builtin-policies.ts +67 -2
  227. package/src/hooks/configure-wizard.ts +20 -0
  228. package/src/hooks/fp-config.ts +71 -11
  229. package/src/hooks/fp-home.ts +27 -0
  230. package/src/hooks/handler.ts +59 -1
  231. package/src/hooks/integrations.ts +47 -13
  232. package/src/hooks/notice.ts +155 -0
  233. package/src/hooks/uninstall-cli.ts +15 -0
  234. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1b5jzxr._.js +0 -3
  235. package/.next/standalone/.next/server/chunks/ssr/_0j6gd_k._.js +0 -23
  236. package/.next/standalone/.next/server/chunks/ssr/_0v8eebd._.js +0 -3
  237. package/.next/standalone/.next/server/chunks/ssr/_1ylae7o._.js +0 -3
  238. package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +0 -3
  239. package/.next/standalone/.next/server/chunks/ssr/node_modules_html-to-image_es_index_1ao30b1.js +0 -3
  240. package/.next/standalone/.next/static/chunks/03slu0c8_71y7.js +0 -1
  241. package/.next/standalone/.next/static/chunks/0nm11ysn0153h.js +0 -1
  242. package/.next/standalone/.next/static/chunks/0u64yey446k4f.css +0 -1
  243. package/.next/standalone/.next/static/chunks/1r9676mkvihto.js +0 -1
  244. package/.next/standalone/.next/static/chunks/30pct5s734jua.js +0 -6
  245. package/.next/standalone/.next/static/chunks/3pibxnbhjza7n.js +0 -69
  246. package/.next/standalone/.next/static/chunks/3qfoeqhy5l10x.js +0 -1
  247. package/.next/standalone/.next/static/chunks/3zkg2s2vzxc3d.js +0 -1
  248. package/.next/standalone/.next/static/chunks/41b5ptaocvi_l.js +0 -1
  249. /package/.next/standalone/.next/static/{JBYgkXkA1PS48m9P_hKNw → aKNv4Kmv98Xpwns0Xa-C-}/_buildManifest.js +0 -0
  250. /package/.next/standalone/.next/static/{JBYgkXkA1PS48m9P_hKNw → aKNv4Kmv98Xpwns0Xa-C-}/_clientMiddlewareManifest.js +0 -0
  251. /package/.next/standalone/.next/static/{JBYgkXkA1PS48m9P_hKNw → aKNv4Kmv98Xpwns0Xa-C-}/_ssgManifest.js +0 -0
@@ -0,0 +1,754 @@
1
+ """Dependency-free wire models for the outbound Evaluator v2 protocol."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from collections.abc import Mapping
7
+ from dataclasses import asdict, dataclass
8
+ from enum import Enum
9
+ from typing import Any, TypeVar
10
+
11
+ PROTOCOL_VERSION = "2"
12
+ TRANSCRIPT_SCHEMA_VERSION = "2"
13
+ RESULT_SCHEMA_VERSION = "2"
14
+
15
+ REGISTER_PATH = "/v1/evaluator/workers/register"
16
+ CLAIM_PATH = "/v1/evaluator/assignments/claim"
17
+ TRANSCRIPT_PATH = "/v1/evaluator/assignments/{assignment_id}/transcript"
18
+ DEFINITIONS_PATH = "/v1/evaluator/assignments/{assignment_id}/definitions"
19
+ PLAN_PATH = "/v1/evaluator/assignments/{assignment_id}/plan"
20
+ HEARTBEAT_PATH = "/v1/evaluator/runs/heartbeat"
21
+ RESULT_PATH = "/v1/evaluator/runs/{evaluation_run_id}/result"
22
+ WORKER_ID_HEADER = "X-FailproofAI-Worker-Id"
23
+ LEASE_GENERATION_HEADER = "X-FailproofAI-Lease-Generation"
24
+
25
+ HEARTBEAT_INTERVAL_SECONDS = 30
26
+ LEASE_DURATION_SECONDS = 120
27
+ # Fallback poll cadence if the register response omits poll_interval_seconds. The
28
+ # worker prefers the server-advertised value; claims are normal short polls, never
29
+ # long-polls, so this only bounds idle latency, not connection lifetime.
30
+ DEFAULT_POLL_INTERVAL_SECONDS = 10
31
+ MAX_ATTEMPTS = 5
32
+
33
+ MAX_CATALOG_DEFINITIONS = 100
34
+ MAX_CLAIM_CAPACITY = 32
35
+ MAX_TRANSCRIPT_BYTES = 25 * 1024 * 1024
36
+ MAX_RESULTS_PER_RUN = 25
37
+ MAX_EVAL_KEY_BYTES = 128
38
+ MAX_DISPLAY_NAME_BYTES = 128
39
+ MAX_VERSION_BYTES = 128
40
+ MAX_WORKER_ID_BYTES = 128
41
+ MAX_LABEL_BYTES = 64
42
+ MAX_LABELS_PER_RESULT = 20
43
+ MAX_SUMMARY_BYTES = 4 * 1024
44
+ MAX_REASONING_BYTES = 16 * 1024
45
+ MAX_UNIT_BYTES = 64
46
+ MAX_DISPLAY_VALUE_BYTES = 256
47
+ MAX_DESCRIPTION_BYTES = 1_000
48
+ MAX_ERROR_CODE_BYTES = 64
49
+ MAX_ERROR_MESSAGE_BYTES = 4 * 1024
50
+
51
+ ERROR_SPECS = {
52
+ "invalid_credentials": {"http_status": 401, "retryable": False},
53
+ "instance_disabled": {"http_status": 403, "retryable": False},
54
+ "insufficient_permissions": {"http_status": 403, "retryable": False},
55
+ "assignment_not_found": {"http_status": 404, "retryable": False},
56
+ "run_not_found": {"http_status": 404, "retryable": False},
57
+ "catalog_mismatch": {"http_status": 409, "retryable": False},
58
+ "lease_lost": {"http_status": 409, "retryable": False},
59
+ "plan_conflict": {"http_status": 409, "retryable": False},
60
+ "submission_conflict": {"http_status": 409, "retryable": False},
61
+ "retry_budget_exhausted": {"http_status": 409, "retryable": False},
62
+ "transcript_too_large": {"http_status": 413, "retryable": False},
63
+ "invalid_request": {"http_status": 422, "retryable": False},
64
+ "invalid_catalog": {"http_status": 422, "retryable": False},
65
+ "incomplete_plan": {"http_status": 422, "retryable": False},
66
+ "unsupported_protocol_version": {"http_status": 426, "retryable": False},
67
+ "internal_error": {"http_status": 500, "retryable": True},
68
+ }
69
+
70
+
71
+ class ProtocolError(ValueError):
72
+ """A local or remote evaluator protocol contract violation."""
73
+
74
+
75
+ class UnsupportedProtocolVersion(ProtocolError):
76
+ def __init__(self, received: str) -> None:
77
+ super().__init__(
78
+ f"unsupported evaluator protocol version {received!r}; "
79
+ f"supported major version is {PROTOCOL_VERSION}"
80
+ )
81
+ self.received = received
82
+
83
+
84
+ def validate_protocol_version(version: str) -> None:
85
+ if version != PROTOCOL_VERSION:
86
+ raise UnsupportedProtocolVersion(version)
87
+
88
+
89
+ class EvaluatorKind(str, Enum):
90
+ MANAGED = "managed"
91
+ CUSTOMER = "customer"
92
+
93
+
94
+ class ResultKind(str, Enum):
95
+ SCORE = "score"
96
+ METRIC = "metric"
97
+ ASSERTION = "assertion"
98
+
99
+
100
+ class ExecutionMode(str, Enum):
101
+ LOCAL = "local"
102
+ PYTHON = "python"
103
+
104
+
105
+ class TerminalRunStatus(str, Enum):
106
+ SUCCEEDED = "succeeded"
107
+ FAILED = "failed"
108
+ TIMED_OUT = "timed_out"
109
+ CANCELLED = "cancelled"
110
+
111
+
112
+ _EnumT = TypeVar("_EnumT", bound=Enum)
113
+
114
+
115
+ def _wire(value: Any) -> Any:
116
+ if isinstance(value, Enum):
117
+ return value.value
118
+ if hasattr(value, "__dataclass_fields__"):
119
+ return {key: _wire(item) for key, item in asdict(value).items()}
120
+ if isinstance(value, (list, tuple)):
121
+ return [_wire(item) for item in value]
122
+ if isinstance(value, dict):
123
+ return {key: _wire(item) for key, item in value.items()}
124
+ return value
125
+
126
+
127
+ class WireModel:
128
+ def to_wire(self) -> dict[str, Any]:
129
+ return _wire(self)
130
+
131
+
132
+ def _string(data: Mapping[str, Any], key: str) -> str:
133
+ value = data.get(key)
134
+ if not isinstance(value, str):
135
+ raise ProtocolError(f"{key} must be a string")
136
+ return value
137
+
138
+
139
+ def _integer(data: Mapping[str, Any], key: str) -> int:
140
+ value = data.get(key)
141
+ if isinstance(value, bool) or not isinstance(value, int):
142
+ raise ProtocolError(f"{key} must be an integer")
143
+ return value
144
+
145
+
146
+ def _list(data: Mapping[str, Any], key: str) -> list[Any]:
147
+ value = data.get(key)
148
+ if not isinstance(value, list):
149
+ raise ProtocolError(f"{key} must be an array")
150
+ return value
151
+
152
+
153
+ def _object(value: Any, field_name: str) -> Mapping[str, Any]:
154
+ if not isinstance(value, Mapping):
155
+ raise ProtocolError(f"{field_name} must be an object")
156
+ return value
157
+
158
+
159
+ def _object_list(data: Mapping[str, Any], key: str) -> tuple[Mapping[str, Any], ...]:
160
+ return tuple(
161
+ _object(value, f"{key}[{index}]")
162
+ for index, value in enumerate(_list(data, key))
163
+ )
164
+
165
+
166
+ def _string_list(data: Mapping[str, Any], key: str) -> tuple[str, ...]:
167
+ values = _list(data, key)
168
+ for index, value in enumerate(values):
169
+ if not isinstance(value, str):
170
+ raise ProtocolError(f"{key}[{index}] must be a string")
171
+ return tuple(values)
172
+
173
+
174
+ def _enum(enum_type: type[_EnumT], data: Mapping[str, Any], key: str) -> _EnumT:
175
+ value = _string(data, key)
176
+ try:
177
+ return enum_type(value)
178
+ except ValueError as error:
179
+ allowed = ", ".join(repr(item.value) for item in enum_type)
180
+ raise ProtocolError(f"{key} must be one of {allowed}") from error
181
+
182
+
183
+ def _positive_integer(data: Mapping[str, Any], key: str) -> int:
184
+ value = _integer(data, key)
185
+ if value <= 0:
186
+ raise ProtocolError(f"{key} must be greater than zero")
187
+ return value
188
+
189
+
190
+ def _nonnegative_integer(data: Mapping[str, Any], key: str) -> int:
191
+ value = _integer(data, key)
192
+ if value < 0:
193
+ raise ProtocolError(f"{key} must not be negative")
194
+ return value
195
+
196
+
197
+ def _optional_string(data: Mapping[str, Any], key: str) -> str | None:
198
+ value = data.get(key)
199
+ if value is not None and not isinstance(value, str):
200
+ raise ProtocolError(f"{key} must be a string or null")
201
+ return value
202
+
203
+
204
+ @dataclass(frozen=True)
205
+ class CatalogDefinition(WireModel):
206
+ eval_key: str
207
+ display_name: str
208
+ eval_version: str
209
+ result_kind: ResultKind
210
+ labels: tuple[str, ...] = ()
211
+
212
+ @classmethod
213
+ def from_wire(cls, data: Mapping[str, Any]) -> CatalogDefinition:
214
+ return cls(
215
+ eval_key=_string(data, "eval_key"),
216
+ display_name=_string(data, "display_name"),
217
+ eval_version=_string(data, "eval_version"),
218
+ result_kind=_enum(ResultKind, data, "result_kind"),
219
+ labels=_string_list(data, "labels"),
220
+ )
221
+
222
+
223
+ @dataclass(frozen=True)
224
+ class RegisterRequest(WireModel):
225
+ worker_id: str
226
+ sdk_version: str
227
+ catalog_revision: str
228
+ max_concurrency: int
229
+ definitions: tuple[CatalogDefinition, ...]
230
+ protocol_version: str = PROTOCOL_VERSION
231
+
232
+ @classmethod
233
+ def from_wire(cls, data: Mapping[str, Any]) -> RegisterRequest:
234
+ validate_protocol_version(_string(data, "protocol_version"))
235
+ return cls(
236
+ worker_id=_string(data, "worker_id"),
237
+ sdk_version=_string(data, "sdk_version"),
238
+ catalog_revision=_string(data, "catalog_revision"),
239
+ max_concurrency=_integer(data, "max_concurrency"),
240
+ definitions=tuple(
241
+ CatalogDefinition.from_wire(item)
242
+ for item in _object_list(data, "definitions")
243
+ ),
244
+ )
245
+
246
+
247
+ @dataclass(frozen=True)
248
+ class RegisterResponse(WireModel):
249
+ evaluator_instance_id: str
250
+ evaluator_kind: EvaluatorKind
251
+ heartbeat_interval_seconds: int
252
+ lease_duration_seconds: int
253
+ poll_interval_seconds: int
254
+ claim_limit: int
255
+ disabled_definitions: tuple[str, ...] = ()
256
+ protocol_version: str = PROTOCOL_VERSION
257
+
258
+ @classmethod
259
+ def from_wire(cls, data: Mapping[str, Any]) -> RegisterResponse:
260
+ validate_protocol_version(_string(data, "protocol_version"))
261
+ return cls(
262
+ evaluator_instance_id=_string(data, "evaluator_instance_id"),
263
+ evaluator_kind=_enum(EvaluatorKind, data, "evaluator_kind"),
264
+ heartbeat_interval_seconds=_integer(data, "heartbeat_interval_seconds"),
265
+ lease_duration_seconds=_integer(data, "lease_duration_seconds"),
266
+ poll_interval_seconds=_integer(data, "poll_interval_seconds"),
267
+ claim_limit=_integer(data, "claim_limit"),
268
+ disabled_definitions=_string_list(data, "disabled_definitions"),
269
+ )
270
+
271
+
272
+ @dataclass(frozen=True)
273
+ class ClaimRequest(WireModel):
274
+ worker_id: str
275
+ catalog_revision: str
276
+ capacity: int
277
+ protocol_version: str = PROTOCOL_VERSION
278
+
279
+ @classmethod
280
+ def from_wire(cls, data: Mapping[str, Any]) -> ClaimRequest:
281
+ validate_protocol_version(_string(data, "protocol_version"))
282
+ return cls(
283
+ worker_id=_string(data, "worker_id"),
284
+ catalog_revision=_string(data, "catalog_revision"),
285
+ capacity=_integer(data, "capacity"),
286
+ )
287
+
288
+
289
+ @dataclass(frozen=True)
290
+ class Assignment(WireModel):
291
+ assignment_id: str
292
+ lease_generation: int
293
+ lease_expires_at: str
294
+ session_id: str
295
+ session_revision_id: str
296
+ agent_id: str
297
+ environment: str
298
+ trigger_reason: str
299
+ event_count: int
300
+ transcript_url: str
301
+ definitions_url: str = ""
302
+
303
+ @classmethod
304
+ def from_wire(cls, data: Mapping[str, Any]) -> Assignment:
305
+ return cls(
306
+ assignment_id=_string(data, "assignment_id"),
307
+ lease_generation=_positive_integer(data, "lease_generation"),
308
+ lease_expires_at=_string(data, "lease_expires_at"),
309
+ session_id=_string(data, "session_id"),
310
+ session_revision_id=_string(data, "session_revision_id"),
311
+ agent_id=_string(data, "agent_id"),
312
+ environment=_string(data, "environment"),
313
+ trigger_reason=_string(data, "trigger_reason"),
314
+ event_count=_nonnegative_integer(data, "event_count"),
315
+ transcript_url=_string(data, "transcript_url"),
316
+ definitions_url=str(data.get("definitions_url") or ""),
317
+ )
318
+
319
+
320
+ @dataclass(frozen=True)
321
+ class AssignmentDefinition(WireModel):
322
+ eval_key: str
323
+ display_name: str
324
+ eval_version: str
325
+ result_kind: ResultKind
326
+ labels: tuple[str, ...] = ()
327
+ execution_mode: ExecutionMode = ExecutionMode.LOCAL
328
+ condition_source: str | None = None
329
+ source_checksum: str | None = None
330
+ timeout_seconds: float | None = None
331
+
332
+ @classmethod
333
+ def from_wire(cls, data: Mapping[str, Any]) -> AssignmentDefinition:
334
+ timeout = data.get("timeout_seconds")
335
+ if timeout is not None:
336
+ if isinstance(timeout, bool) or not isinstance(timeout, (int, float)):
337
+ raise ProtocolError("timeout_seconds must be a number or null")
338
+ timeout = float(timeout)
339
+ if not math.isfinite(timeout) or timeout <= 0:
340
+ raise ProtocolError("timeout_seconds must be finite and greater than zero")
341
+ return cls(
342
+ eval_key=_string(data, "eval_key"),
343
+ display_name=_string(data, "display_name"),
344
+ eval_version=_string(data, "eval_version"),
345
+ result_kind=_enum(ResultKind, data, "result_kind"),
346
+ labels=_string_list(data, "labels"),
347
+ # Require execution_mode explicitly. Coercing a falsy/missing value to
348
+ # 'local' silently ran a server-authored ('python') definition down the
349
+ # customer-local path (or vice-versa); a malformed wire value is a
350
+ # protocol error, not a default (F2).
351
+ execution_mode=_enum(ExecutionMode, data, "execution_mode"),
352
+ condition_source=_optional_string(data, "condition_source"),
353
+ source_checksum=_optional_string(data, "source_checksum"),
354
+ timeout_seconds=timeout,
355
+ )
356
+
357
+
358
+ @dataclass(frozen=True)
359
+ class DefinitionsResponse(WireModel):
360
+ assignment_id: str
361
+ catalog_revision: str
362
+ definitions: tuple[AssignmentDefinition, ...]
363
+ protocol_version: str = PROTOCOL_VERSION
364
+
365
+ @classmethod
366
+ def from_wire(cls, data: Mapping[str, Any]) -> DefinitionsResponse:
367
+ validate_protocol_version(_string(data, "protocol_version"))
368
+ return cls(
369
+ assignment_id=_string(data, "assignment_id"),
370
+ catalog_revision=_string(data, "catalog_revision"),
371
+ definitions=tuple(
372
+ AssignmentDefinition.from_wire(item)
373
+ for item in _object_list(data, "definitions")
374
+ ),
375
+ )
376
+
377
+
378
+ @dataclass(frozen=True)
379
+ class ClaimResponse(WireModel):
380
+ assignments: tuple[Assignment, ...]
381
+ protocol_version: str = PROTOCOL_VERSION
382
+
383
+ @classmethod
384
+ def from_wire(cls, data: Mapping[str, Any]) -> ClaimResponse:
385
+ validate_protocol_version(_string(data, "protocol_version"))
386
+ return cls(
387
+ tuple(
388
+ Assignment.from_wire(item) for item in _object_list(data, "assignments")
389
+ )
390
+ )
391
+
392
+
393
+ @dataclass(frozen=True)
394
+ class TranscriptEvent(WireModel):
395
+ id: str
396
+ ts: str
397
+ event_type: str
398
+ payload: Mapping[str, Any]
399
+
400
+ @classmethod
401
+ def from_wire(cls, data: Mapping[str, Any]) -> TranscriptEvent:
402
+ payload = data.get("payload")
403
+ if not isinstance(payload, Mapping):
404
+ raise ProtocolError("payload must be an object")
405
+ return cls(
406
+ id=_string(data, "id"),
407
+ ts=_string(data, "ts"),
408
+ event_type=_string(data, "event_type"),
409
+ payload=dict(payload),
410
+ )
411
+
412
+
413
+ @dataclass(frozen=True)
414
+ class SessionTranscript(WireModel):
415
+ assignment_id: str
416
+ session_id: str
417
+ session_revision_id: str
418
+ agent_id: str
419
+ environment: str
420
+ started_at: str
421
+ ended_at: str
422
+ event_count: int
423
+ events: tuple[TranscriptEvent, ...]
424
+ schema_version: str = TRANSCRIPT_SCHEMA_VERSION
425
+
426
+ @classmethod
427
+ def from_wire(cls, data: Mapping[str, Any]) -> SessionTranscript:
428
+ version = _string(data, "schema_version")
429
+ if version != TRANSCRIPT_SCHEMA_VERSION:
430
+ raise ProtocolError(f"unsupported transcript schema version {version!r}")
431
+ events = tuple(
432
+ TranscriptEvent.from_wire(item) for item in _object_list(data, "events")
433
+ )
434
+ event_count = _nonnegative_integer(data, "event_count")
435
+ if event_count != len(events):
436
+ raise ProtocolError(
437
+ f"event_count is {event_count}, but transcript contains {len(events)} events"
438
+ )
439
+ return cls(
440
+ assignment_id=_string(data, "assignment_id"),
441
+ session_id=_string(data, "session_id"),
442
+ session_revision_id=_string(data, "session_revision_id"),
443
+ agent_id=_string(data, "agent_id"),
444
+ environment=_string(data, "environment"),
445
+ started_at=_string(data, "started_at"),
446
+ ended_at=_string(data, "ended_at"),
447
+ event_count=event_count,
448
+ events=events,
449
+ )
450
+
451
+ def events_of_type(self, event_type: str) -> tuple[TranscriptEvent, ...]:
452
+ return tuple(event for event in self.events if event.event_type == event_type)
453
+
454
+ def count(self, event_type: str) -> int:
455
+ return sum(event.event_type == event_type for event in self.events)
456
+
457
+
458
+ @dataclass(frozen=True)
459
+ class EvalSelection(WireModel):
460
+ eval_key: str
461
+ eval_version: str
462
+
463
+ @classmethod
464
+ def from_wire(cls, data: Mapping[str, Any]) -> EvalSelection:
465
+ return cls(_string(data, "eval_key"), _string(data, "eval_version"))
466
+
467
+
468
+ @dataclass(frozen=True)
469
+ class SkippedEval(WireModel):
470
+ eval_key: str
471
+ eval_version: str
472
+ reason_code: str
473
+
474
+ @classmethod
475
+ def from_wire(cls, data: Mapping[str, Any]) -> SkippedEval:
476
+ return cls(
477
+ _string(data, "eval_key"),
478
+ _string(data, "eval_version"),
479
+ _string(data, "reason_code"),
480
+ )
481
+
482
+
483
+ @dataclass(frozen=True)
484
+ class PlanRequest(WireModel):
485
+ worker_id: str
486
+ lease_generation: int
487
+ selected: tuple[EvalSelection, ...] = ()
488
+ skipped: tuple[SkippedEval, ...] = ()
489
+ protocol_version: str = PROTOCOL_VERSION
490
+
491
+ @classmethod
492
+ def from_wire(cls, data: Mapping[str, Any]) -> PlanRequest:
493
+ validate_protocol_version(_string(data, "protocol_version"))
494
+ return cls(
495
+ worker_id=_string(data, "worker_id"),
496
+ lease_generation=_positive_integer(data, "lease_generation"),
497
+ selected=tuple(
498
+ EvalSelection.from_wire(item) for item in _object_list(data, "selected")
499
+ ),
500
+ skipped=tuple(
501
+ SkippedEval.from_wire(item) for item in _object_list(data, "skipped")
502
+ ),
503
+ )
504
+
505
+
506
+ @dataclass(frozen=True)
507
+ class PlannedRun(WireModel):
508
+ evaluation_run_id: str
509
+ eval_key: str
510
+ eval_version: str
511
+ execution_mode: ExecutionMode = ExecutionMode.LOCAL
512
+ evaluator_source: str | None = None
513
+ source_checksum: str | None = None
514
+ timeout_seconds: float | None = None
515
+
516
+ @classmethod
517
+ def from_wire(cls, data: Mapping[str, Any]) -> PlannedRun:
518
+ timeout = data.get("timeout_seconds")
519
+ if timeout is not None:
520
+ if isinstance(timeout, bool) or not isinstance(timeout, (int, float)):
521
+ raise ProtocolError("timeout_seconds must be a number or null")
522
+ timeout = float(timeout)
523
+ if not math.isfinite(timeout) or timeout <= 0:
524
+ raise ProtocolError("timeout_seconds must be finite and greater than zero")
525
+ return cls(
526
+ evaluation_run_id=_string(data, "evaluation_run_id"),
527
+ eval_key=_string(data, "eval_key"),
528
+ eval_version=_string(data, "eval_version"),
529
+ # Require execution_mode explicitly. Coercing a falsy/missing value to
530
+ # 'local' silently ran a server-authored ('python') definition down the
531
+ # customer-local path (or vice-versa); a malformed wire value is a
532
+ # protocol error, not a default (F2).
533
+ execution_mode=_enum(ExecutionMode, data, "execution_mode"),
534
+ evaluator_source=_optional_string(data, "evaluator_source"),
535
+ source_checksum=_optional_string(data, "source_checksum"),
536
+ timeout_seconds=timeout,
537
+ )
538
+
539
+
540
+ @dataclass(frozen=True)
541
+ class PlanResponse(WireModel):
542
+ assignment_id: str
543
+ assignment_status: str
544
+ runs: tuple[PlannedRun, ...]
545
+ idempotent_replay: bool = False
546
+ protocol_version: str = PROTOCOL_VERSION
547
+
548
+ @classmethod
549
+ def from_wire(cls, data: Mapping[str, Any]) -> PlanResponse:
550
+ validate_protocol_version(_string(data, "protocol_version"))
551
+ replay = data.get("idempotent_replay", False)
552
+ if not isinstance(replay, bool):
553
+ raise ProtocolError("idempotent_replay must be a boolean")
554
+ return cls(
555
+ assignment_id=_string(data, "assignment_id"),
556
+ assignment_status=_string(data, "assignment_status"),
557
+ runs=tuple(
558
+ PlannedRun.from_wire(item) for item in _object_list(data, "runs")
559
+ ),
560
+ idempotent_replay=replay,
561
+ )
562
+
563
+
564
+ @dataclass(frozen=True)
565
+ class HeartbeatRun(WireModel):
566
+ evaluation_run_id: str
567
+ state: str
568
+ progress: float | None = None
569
+
570
+ @classmethod
571
+ def from_wire(cls, data: Mapping[str, Any]) -> HeartbeatRun:
572
+ progress = data.get("progress")
573
+ if progress is not None:
574
+ if isinstance(progress, bool) or not isinstance(progress, (int, float)):
575
+ raise ProtocolError("progress must be a number or null")
576
+ progress = float(progress)
577
+ if not math.isfinite(progress) or not 0 <= progress <= 1:
578
+ raise ProtocolError("progress must be finite and between 0 and 1")
579
+ return cls(
580
+ evaluation_run_id=_string(data, "evaluation_run_id"),
581
+ state=_string(data, "state"),
582
+ progress=progress,
583
+ )
584
+
585
+
586
+ @dataclass(frozen=True)
587
+ class HeartbeatRequest(WireModel):
588
+ worker_id: str
589
+ lease_generation: int
590
+ runs: tuple[HeartbeatRun, ...]
591
+ protocol_version: str = PROTOCOL_VERSION
592
+
593
+ @classmethod
594
+ def from_wire(cls, data: Mapping[str, Any]) -> HeartbeatRequest:
595
+ validate_protocol_version(_string(data, "protocol_version"))
596
+ return cls(
597
+ worker_id=_string(data, "worker_id"),
598
+ lease_generation=_positive_integer(data, "lease_generation"),
599
+ runs=tuple(
600
+ HeartbeatRun.from_wire(item) for item in _object_list(data, "runs")
601
+ ),
602
+ )
603
+
604
+
605
+ @dataclass(frozen=True)
606
+ class HeartbeatResponse(WireModel):
607
+ lease_expires_at: str
608
+ accepted_run_ids: tuple[str, ...]
609
+ protocol_version: str = PROTOCOL_VERSION
610
+
611
+ @classmethod
612
+ def from_wire(cls, data: Mapping[str, Any]) -> HeartbeatResponse:
613
+ validate_protocol_version(_string(data, "protocol_version"))
614
+ return cls(
615
+ lease_expires_at=_string(data, "lease_expires_at"),
616
+ accepted_run_ids=_string_list(data, "accepted_run_ids"),
617
+ )
618
+
619
+
620
+ @dataclass(frozen=True)
621
+ class ResultItem(WireModel):
622
+ result_key: str
623
+ result_kind: ResultKind
624
+ numeric_value: float | None = None
625
+ bool_value: bool | None = None
626
+ text_value: str | None = None
627
+ unit: str = ""
628
+ display_value: str | None = None
629
+ description: str | None = None
630
+ reasoning: str | None = None
631
+ labels: tuple[str, ...] = ()
632
+
633
+ @classmethod
634
+ def from_wire(cls, data: Mapping[str, Any]) -> ResultItem:
635
+ numeric = data.get("numeric_value")
636
+ if numeric is not None:
637
+ if isinstance(numeric, bool) or not isinstance(numeric, (int, float)):
638
+ raise ProtocolError("numeric_value must be a number or null")
639
+ numeric = float(numeric)
640
+ if not math.isfinite(numeric):
641
+ raise ProtocolError("numeric_value must be finite")
642
+ boolean = data.get("bool_value")
643
+ if boolean is not None and not isinstance(boolean, bool):
644
+ raise ProtocolError("bool_value must be a boolean or null")
645
+ return cls(
646
+ result_key=_string(data, "result_key"),
647
+ result_kind=_enum(ResultKind, data, "result_kind"),
648
+ numeric_value=numeric,
649
+ bool_value=boolean,
650
+ text_value=_optional_string(data, "text_value"),
651
+ unit=_string(data, "unit"),
652
+ display_value=_optional_string(data, "display_value"),
653
+ description=_optional_string(data, "description"),
654
+ reasoning=_optional_string(data, "reasoning"),
655
+ labels=_string_list(data, "labels"),
656
+ )
657
+
658
+
659
+ @dataclass(frozen=True)
660
+ class ResultRequest(WireModel):
661
+ submission_id: str
662
+ worker_id: str
663
+ lease_generation: int
664
+ status: TerminalRunStatus
665
+ started_at: str
666
+ finished_at: str
667
+ duration_ms: int
668
+ summary: str | None
669
+ results: tuple[ResultItem, ...]
670
+ error_code: str | None
671
+ error_message: str | None
672
+ protocol_version: str = PROTOCOL_VERSION
673
+ result_schema_version: str = RESULT_SCHEMA_VERSION
674
+
675
+ @classmethod
676
+ def from_wire(cls, data: Mapping[str, Any]) -> ResultRequest:
677
+ validate_protocol_version(_string(data, "protocol_version"))
678
+ result_schema_version = _string(data, "result_schema_version")
679
+ if result_schema_version != RESULT_SCHEMA_VERSION:
680
+ raise ProtocolError(
681
+ f"unsupported result schema version {result_schema_version!r}"
682
+ )
683
+ return cls(
684
+ submission_id=_string(data, "submission_id"),
685
+ worker_id=_string(data, "worker_id"),
686
+ lease_generation=_positive_integer(data, "lease_generation"),
687
+ status=_enum(TerminalRunStatus, data, "status"),
688
+ started_at=_string(data, "started_at"),
689
+ finished_at=_string(data, "finished_at"),
690
+ duration_ms=_nonnegative_integer(data, "duration_ms"),
691
+ summary=_optional_string(data, "summary"),
692
+ results=tuple(
693
+ ResultItem.from_wire(item) for item in _object_list(data, "results")
694
+ ),
695
+ error_code=_optional_string(data, "error_code"),
696
+ error_message=_optional_string(data, "error_message"),
697
+ )
698
+
699
+
700
+ @dataclass(frozen=True)
701
+ class ResultResponse(WireModel):
702
+ evaluation_run_id: str
703
+ submission_id: str
704
+ status: str
705
+ idempotent_replay: bool
706
+ result_count: int
707
+ result_checksum: str
708
+ protocol_version: str = PROTOCOL_VERSION
709
+
710
+ @classmethod
711
+ def from_wire(cls, data: Mapping[str, Any]) -> ResultResponse:
712
+ validate_protocol_version(_string(data, "protocol_version"))
713
+ replay = data.get("idempotent_replay")
714
+ if not isinstance(replay, bool):
715
+ raise ProtocolError("idempotent_replay must be a boolean")
716
+ return cls(
717
+ evaluation_run_id=_string(data, "evaluation_run_id"),
718
+ submission_id=_string(data, "submission_id"),
719
+ status=_string(data, "status"),
720
+ idempotent_replay=replay,
721
+ result_count=_nonnegative_integer(data, "result_count"),
722
+ result_checksum=_string(data, "result_checksum"),
723
+ )
724
+
725
+
726
+ @dataclass(frozen=True)
727
+ class RemoteError(WireModel):
728
+ code: str
729
+ message: str
730
+ retryable: bool
731
+ request_id: str
732
+
733
+ @classmethod
734
+ def from_wire(cls, data: Mapping[str, Any]) -> RemoteError:
735
+ retryable = data.get("retryable")
736
+ if not isinstance(retryable, bool):
737
+ raise ProtocolError("retryable must be a boolean")
738
+ return cls(
739
+ code=_string(data, "code"),
740
+ message=_string(data, "message"),
741
+ retryable=retryable,
742
+ request_id=_string(data, "request_id"),
743
+ )
744
+
745
+
746
+ @dataclass(frozen=True)
747
+ class ErrorResponse(WireModel):
748
+ error: RemoteError
749
+ protocol_version: str = PROTOCOL_VERSION
750
+
751
+ @classmethod
752
+ def from_wire(cls, data: Mapping[str, Any]) -> ErrorResponse:
753
+ validate_protocol_version(_string(data, "protocol_version"))
754
+ return cls(RemoteError.from_wire(_object(data.get("error"), "error")))