evalkit-py 0.2.0.dev12__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 (371) hide show
  1. evalkit/__init__.py +0 -0
  2. evalkit/api/__init__.py +0 -0
  3. evalkit/api/access.py +74 -0
  4. evalkit/api/agents.py +159 -0
  5. evalkit/api/app.py +360 -0
  6. evalkit/api/auth.py +225 -0
  7. evalkit/api/cli_dist.py +10 -0
  8. evalkit/api/datasets.py +169 -0
  9. evalkit/api/deps.py +79 -0
  10. evalkit/api/download.py +15 -0
  11. evalkit/api/evaluators.py +121 -0
  12. evalkit/api/experiments.py +616 -0
  13. evalkit/api/fixtures.py +95 -0
  14. evalkit/api/harness_check.py +48 -0
  15. evalkit/api/imports.py +180 -0
  16. evalkit/api/limits.py +84 -0
  17. evalkit/api/llm_providers.py +120 -0
  18. evalkit/api/mcp/__init__.py +0 -0
  19. evalkit/api/mcp/auth.py +100 -0
  20. evalkit/api/mcp/descriptions.py +336 -0
  21. evalkit/api/mcp/models.py +259 -0
  22. evalkit/api/mcp/server.py +101 -0
  23. evalkit/api/mcp/tools.py +844 -0
  24. evalkit/api/meta.py +19 -0
  25. evalkit/api/otel.py +350 -0
  26. evalkit/api/pull.py +197 -0
  27. evalkit/api/review.py +271 -0
  28. evalkit/api/runs.py +555 -0
  29. evalkit/api/server_keys.py +44 -0
  30. evalkit/api/tokens.py +51 -0
  31. evalkit/api/trace_imports.py +173 -0
  32. evalkit/api/trace_sources.py +49 -0
  33. evalkit/api/traces.py +196 -0
  34. evalkit/api/users.py +72 -0
  35. evalkit/api/workspaces.py +131 -0
  36. evalkit/bundled/contracts/atif-accepted.json +5 -0
  37. evalkit/bundled/contracts/custom-evaluator-api-v1.json +243 -0
  38. evalkit/bundled/contracts/harness-api-v1.json +938 -0
  39. evalkit/bundled/contracts/span-mapping-v1.json +314 -0
  40. evalkit/bundled/docs/concepts.md +26 -0
  41. evalkit/bundled/docs/config.md +38 -0
  42. evalkit/bundled/docs/harness-api.md +44 -0
  43. evalkit/bundled/docs/install.md +60 -0
  44. evalkit/bundled/docs/login.md +36 -0
  45. evalkit/bundled/docs/mappings.md +136 -0
  46. evalkit/bundled/docs/mcp.md +44 -0
  47. evalkit/bundled/docs/quickstart.md +142 -0
  48. evalkit/bundled/docs/sdk-runs.md +171 -0
  49. evalkit/bundled/docs/tracing.md +171 -0
  50. evalkit/bundled/docs/try-locally.md +288 -0
  51. evalkit/bundled/docs/workspaces.md +45 -0
  52. evalkit/bundled/skills/authoring-eval-cases/SKILL.md +387 -0
  53. evalkit/bundled/skills/authoring-eval-cases/references/overlays.md +83 -0
  54. evalkit/bundled/skills/authoring-eval-cases/references/tool-checks.md +71 -0
  55. evalkit/bundled/skills/connect-traces/SKILL.md +296 -0
  56. evalkit/bundled/skills/connect-traces/references/gen_ai.json +42 -0
  57. evalkit/bundled/skills/connect-traces/references/openinference.json +35 -0
  58. evalkit/bundled/skills/connect-traces/references/span-mapping-schema.md +88 -0
  59. evalkit/bundled/skills/eval-driven-development/SKILL.md +458 -0
  60. evalkit/bundled/skills/eval-driven-development/references/analyze-run.md +97 -0
  61. evalkit/bundled/skills/eval-driven-development/references/diagnose.md +130 -0
  62. evalkit/bundled/skills/eval-driven-development/references/synthesize-cases.md +87 -0
  63. evalkit/bundled/skills/eval-driven-development/references/synthesize.md +165 -0
  64. evalkit/bundled/skills/evalkit-deployment/SKILL.md +332 -0
  65. evalkit/bundled/skills/evalkit-deployment/config.example.yaml +49 -0
  66. evalkit/bundled/skills/evalkit-deployment/docker-compose.yml +51 -0
  67. evalkit/bundled/skills/evalkit-deployment/references/config-and-deploy.md +333 -0
  68. evalkit/bundled/skills/evalkit-deployment/references/harness-api.md +139 -0
  69. evalkit/bundled/skills/evalkit-deployment/try/.env.example +11 -0
  70. evalkit/bundled/skills/evalkit-deployment/try/agent.py +131 -0
  71. evalkit/bundled/skills/evalkit-deployment/try/cases/acme/balance-sheet/cases.yaml +25 -0
  72. evalkit/bundled/skills/evalkit-deployment/try/cases/acme/income-statement/cases.yaml +43 -0
  73. evalkit/bundled/skills/evalkit-deployment/try/config.yaml +17 -0
  74. evalkit/bundled/skills/evalkit-deployment/try/docker-compose.yml +82 -0
  75. evalkit/bundled/skills/evalkit-deployment/try/evalkit.yaml +4 -0
  76. evalkit/bundled/skills/evalkit-deployment/try/mapping.json +42 -0
  77. evalkit/bundled/skills/evalkit-deployment/try/run.py +33 -0
  78. evalkit/bundled/skills/evalkit-usage/SKILL.md +509 -0
  79. evalkit/bundled/skills/evalkit-usage/references/suite-fixtures.md +42 -0
  80. evalkit/bundled/ui/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  81. evalkit/bundled/ui/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  82. evalkit/bundled/ui/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  83. evalkit/bundled/ui/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  84. evalkit/bundled/ui/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  85. evalkit/bundled/ui/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  86. evalkit/bundled/ui/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  87. evalkit/bundled/ui/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  88. evalkit/bundled/ui/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  89. evalkit/bundled/ui/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  90. evalkit/bundled/ui/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  91. evalkit/bundled/ui/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  92. evalkit/bundled/ui/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  93. evalkit/bundled/ui/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  94. evalkit/bundled/ui/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  95. evalkit/bundled/ui/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  96. evalkit/bundled/ui/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  97. evalkit/bundled/ui/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  98. evalkit/bundled/ui/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  99. evalkit/bundled/ui/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  100. evalkit/bundled/ui/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  101. evalkit/bundled/ui/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  102. evalkit/bundled/ui/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  103. evalkit/bundled/ui/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  104. evalkit/bundled/ui/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  105. evalkit/bundled/ui/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  106. evalkit/bundled/ui/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  107. evalkit/bundled/ui/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  108. evalkit/bundled/ui/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  109. evalkit/bundled/ui/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  110. evalkit/bundled/ui/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  111. evalkit/bundled/ui/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  112. evalkit/bundled/ui/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  113. evalkit/bundled/ui/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  114. evalkit/bundled/ui/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  115. evalkit/bundled/ui/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  116. evalkit/bundled/ui/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  117. evalkit/bundled/ui/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  118. evalkit/bundled/ui/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  119. evalkit/bundled/ui/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  120. evalkit/bundled/ui/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  121. evalkit/bundled/ui/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  122. evalkit/bundled/ui/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  123. evalkit/bundled/ui/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  124. evalkit/bundled/ui/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  125. evalkit/bundled/ui/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  126. evalkit/bundled/ui/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  127. evalkit/bundled/ui/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  128. evalkit/bundled/ui/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  129. evalkit/bundled/ui/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  130. evalkit/bundled/ui/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  131. evalkit/bundled/ui/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  132. evalkit/bundled/ui/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  133. evalkit/bundled/ui/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  134. evalkit/bundled/ui/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  135. evalkit/bundled/ui/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  136. evalkit/bundled/ui/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  137. evalkit/bundled/ui/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  138. evalkit/bundled/ui/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  139. evalkit/bundled/ui/assets/abap-08VXUWAP.js +1 -0
  140. evalkit/bundled/ui/assets/apex-BWPQTe0t.js +1 -0
  141. evalkit/bundled/ui/assets/azcli-Bc_sGQ0U.js +1 -0
  142. evalkit/bundled/ui/assets/bat-i0X4ZdIN.js +1 -0
  143. evalkit/bundled/ui/assets/bicep-B5-_aFwp.js +2 -0
  144. evalkit/bundled/ui/assets/cameligo-DMUM7wLl.js +1 -0
  145. evalkit/bundled/ui/assets/clojure-Cm7r79vr.js +1 -0
  146. evalkit/bundled/ui/assets/codicon-DF1abBS2.ttf +0 -0
  147. evalkit/bundled/ui/assets/coffee-Ba7i2nA0.js +1 -0
  148. evalkit/bundled/ui/assets/cpp-C7h46wYY.js +1 -0
  149. evalkit/bundled/ui/assets/csharp-BKxtCVv1.js +1 -0
  150. evalkit/bundled/ui/assets/csp-bTuwJoIa.js +1 -0
  151. evalkit/bundled/ui/assets/css-DIMkf-bt.js +3 -0
  152. evalkit/bundled/ui/assets/css.worker-DwfUZ3fB.js +96 -0
  153. evalkit/bundled/ui/assets/cssMode-C97l1pbB.js +1 -0
  154. evalkit/bundled/ui/assets/cypher-CVaqCwHa.js +1 -0
  155. evalkit/bundled/ui/assets/dart-onAF5SnQ.js +1 -0
  156. evalkit/bundled/ui/assets/dockerfile-DZFCIeNp.js +1 -0
  157. evalkit/bundled/ui/assets/ecl-94qehhXe.js +1 -0
  158. evalkit/bundled/ui/assets/editor-DjpsE8to.css +1 -0
  159. evalkit/bundled/ui/assets/editor.api-NTesIxw5.js +908 -0
  160. evalkit/bundled/ui/assets/editor.worker-CRz__AKW.js +33 -0
  161. evalkit/bundled/ui/assets/elixir-6RTg0lbw.js +1 -0
  162. evalkit/bundled/ui/assets/flow9-C5_-GSwl.js +1 -0
  163. evalkit/bundled/ui/assets/freemarker2-BWqEFagp.js +3 -0
  164. evalkit/bundled/ui/assets/fsharp-C8Ef5oNN.js +1 -0
  165. evalkit/bundled/ui/assets/go-C-y9NEjX.js +1 -0
  166. evalkit/bundled/ui/assets/graphql-fmXr3nnJ.js +1 -0
  167. evalkit/bundled/ui/assets/handlebars-DkIF-whN.js +1 -0
  168. evalkit/bundled/ui/assets/hcl-CpzslTdj.js +1 -0
  169. evalkit/bundled/ui/assets/html-C_rpeQXJ.js +1 -0
  170. evalkit/bundled/ui/assets/html.worker-D9ugpX4y.js +509 -0
  171. evalkit/bundled/ui/assets/htmlMode-B0YIq3DF.js +1 -0
  172. evalkit/bundled/ui/assets/index-Cw-jBH6M.js +736 -0
  173. evalkit/bundled/ui/assets/index-DKtoQZtm.css +1 -0
  174. evalkit/bundled/ui/assets/ini-sBoK_t0W.js +1 -0
  175. evalkit/bundled/ui/assets/java-BEtHBSE6.js +1 -0
  176. evalkit/bundled/ui/assets/javascript-D4EGhYA6.js +1 -0
  177. evalkit/bundled/ui/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  178. evalkit/bundled/ui/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  179. evalkit/bundled/ui/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  180. evalkit/bundled/ui/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  181. evalkit/bundled/ui/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  182. evalkit/bundled/ui/assets/json.worker-CV9fqkUp.js +65 -0
  183. evalkit/bundled/ui/assets/jsonMode-pQjSfIH6.js +7 -0
  184. evalkit/bundled/ui/assets/julia-Bri6UV-V.js +1 -0
  185. evalkit/bundled/ui/assets/kotlin-BOotOW0E.js +1 -0
  186. evalkit/bundled/ui/assets/less-B9JPFI3C.js +2 -0
  187. evalkit/bundled/ui/assets/lexon-CfSJPG6W.js +1 -0
  188. evalkit/bundled/ui/assets/liquid-DbrvoYjD.js +1 -0
  189. evalkit/bundled/ui/assets/lspLanguageFeatures-D0B7cH6y.js +4 -0
  190. evalkit/bundled/ui/assets/lua-CsQS60Ue.js +1 -0
  191. evalkit/bundled/ui/assets/m3-D-oSqn_W.js +1 -0
  192. evalkit/bundled/ui/assets/markdown-Cimd5fb3.js +1 -0
  193. evalkit/bundled/ui/assets/mdx-B0HrujLR.js +1 -0
  194. evalkit/bundled/ui/assets/mips-CIPQ_RoX.js +1 -0
  195. evalkit/bundled/ui/assets/monacoEditor-DsoPncxW.js +68 -0
  196. evalkit/bundled/ui/assets/monacoEditor-n7clI0si.css +1 -0
  197. evalkit/bundled/ui/assets/msdax-DauUninz.js +1 -0
  198. evalkit/bundled/ui/assets/mysql-SOo6toE5.js +1 -0
  199. evalkit/bundled/ui/assets/objective-c-FvmIjYaQ.js +1 -0
  200. evalkit/bundled/ui/assets/pascal-DrH0SRf2.js +1 -0
  201. evalkit/bundled/ui/assets/pascaligo-D-ptJ9y-.js +1 -0
  202. evalkit/bundled/ui/assets/perl-oz_6vUea.js +1 -0
  203. evalkit/bundled/ui/assets/pgsql-DTj74zXo.js +1 -0
  204. evalkit/bundled/ui/assets/php-nr791fC2.js +1 -0
  205. evalkit/bundled/ui/assets/pla-CopQ2nXW.js +1 -0
  206. evalkit/bundled/ui/assets/postiats-43DmfD33.js +1 -0
  207. evalkit/bundled/ui/assets/powerquery-D3hlyOfw.js +1 -0
  208. evalkit/bundled/ui/assets/powershell-DmHpPYUd.js +1 -0
  209. evalkit/bundled/ui/assets/protobuf-C531GsRP.js +2 -0
  210. evalkit/bundled/ui/assets/pug-Z5eAx3Zn.js +1 -0
  211. evalkit/bundled/ui/assets/python-CieHVM9n.js +1 -0
  212. evalkit/bundled/ui/assets/qsharp-DkqhCAOL.js +1 -0
  213. evalkit/bundled/ui/assets/r-BwWrilGY.js +1 -0
  214. evalkit/bundled/ui/assets/razor-sLewSMyp.js +1 -0
  215. evalkit/bundled/ui/assets/redis-ClamHrr6.js +1 -0
  216. evalkit/bundled/ui/assets/redshift-DT7zqm-g.js +1 -0
  217. evalkit/bundled/ui/assets/restructuredtext-BYgofb2h.js +1 -0
  218. evalkit/bundled/ui/assets/ruby-DezsRK8O.js +1 -0
  219. evalkit/bundled/ui/assets/rust-DdL9SqIa.js +1 -0
  220. evalkit/bundled/ui/assets/sb-CcwsVR0C.js +1 -0
  221. evalkit/bundled/ui/assets/scala-DHpiXF5c.js +1 -0
  222. evalkit/bundled/ui/assets/scheme-BeGwcela.js +1 -0
  223. evalkit/bundled/ui/assets/scss-gp-XZpBa.js +3 -0
  224. evalkit/bundled/ui/assets/shell-CC2rA5mh.js +1 -0
  225. evalkit/bundled/ui/assets/solidity-BEEn4gHE.js +1 -0
  226. evalkit/bundled/ui/assets/sophia-CRfGWb83.js +1 -0
  227. evalkit/bundled/ui/assets/sparql-D_Lu-MrJ.js +1 -0
  228. evalkit/bundled/ui/assets/sql-NEE52Syq.js +1 -0
  229. evalkit/bundled/ui/assets/st-DbInun42.js +1 -0
  230. evalkit/bundled/ui/assets/swift-Bxkupp3x.js +1 -0
  231. evalkit/bundled/ui/assets/systemverilog-Bz4Y3fRF.js +1 -0
  232. evalkit/bundled/ui/assets/tcl-DISqw1ZD.js +1 -0
  233. evalkit/bundled/ui/assets/ts.worker-DIAQkoB3.js +67741 -0
  234. evalkit/bundled/ui/assets/tsMode-BcVQ1nu0.js +11 -0
  235. evalkit/bundled/ui/assets/twig-De2hgUGE.js +1 -0
  236. evalkit/bundled/ui/assets/typescript-CUvNWd9p.js +1 -0
  237. evalkit/bundled/ui/assets/typespec-B8J7ngcE.js +1 -0
  238. evalkit/bundled/ui/assets/vb-DV3o63ZY.js +1 -0
  239. evalkit/bundled/ui/assets/wgsl-DpFanUEy.js +298 -0
  240. evalkit/bundled/ui/assets/workers-CwpYQtzp.js +1 -0
  241. evalkit/bundled/ui/assets/xml-C1HnXMdF.js +1 -0
  242. evalkit/bundled/ui/assets/yaml-D7Ey3HQu.js +1 -0
  243. evalkit/bundled/ui/favicon.svg +6 -0
  244. evalkit/bundled/ui/index.html +14 -0
  245. evalkit/cli/__init__.py +0 -0
  246. evalkit/cli/admin_commands.py +590 -0
  247. evalkit/cli/client.py +67 -0
  248. evalkit/cli/init_server.py +165 -0
  249. evalkit/cli/local_run.py +195 -0
  250. evalkit/cli/main.py +1365 -0
  251. evalkit/cli/runs_commands.py +539 -0
  252. evalkit/cli/server_commands.py +130 -0
  253. evalkit/cli/skill_targets.py +43 -0
  254. evalkit/cli/tokens_commands.py +52 -0
  255. evalkit/cli/traces_commands.py +332 -0
  256. evalkit/cli/update.py +85 -0
  257. evalkit/client/__init__.py +0 -0
  258. evalkit/client/agents.py +25 -0
  259. evalkit/client/api.py +193 -0
  260. evalkit/client/blob.py +104 -0
  261. evalkit/client/connection.py +88 -0
  262. evalkit/client/evaluators.py +11 -0
  263. evalkit/client/sessions.py +17 -0
  264. evalkit/client/store.py +139 -0
  265. evalkit/client/summary.py +46 -0
  266. evalkit/client/sync.py +198 -0
  267. evalkit/config.py +227 -0
  268. evalkit/contracts/__init__.py +0 -0
  269. evalkit/contracts/atif.py +115 -0
  270. evalkit/contracts/cases.py +456 -0
  271. evalkit/contracts/custom_evaluator.py +58 -0
  272. evalkit/contracts/enums.py +86 -0
  273. evalkit/contracts/esf.py +120 -0
  274. evalkit/contracts/harness.py +281 -0
  275. evalkit/contracts/notes.json +4 -0
  276. evalkit/contracts/notes.py +7 -0
  277. evalkit/contracts/settings.py +41 -0
  278. evalkit/core/__init__.py +0 -0
  279. evalkit/core/access.py +75 -0
  280. evalkit/core/agents.py +65 -0
  281. evalkit/core/blob.py +46 -0
  282. evalkit/core/capabilities.py +36 -0
  283. evalkit/core/costmap.py +90 -0
  284. evalkit/core/custom_eval.py +50 -0
  285. evalkit/core/data/cost_map.json +81546 -0
  286. evalkit/core/endpoints.py +31 -0
  287. evalkit/core/evaluators/__init__.py +29 -0
  288. evalkit/core/evaluators/base.py +121 -0
  289. evalkit/core/evaluators/gauges.py +84 -0
  290. evalkit/core/evaluators/presentation.py +166 -0
  291. evalkit/core/evaluators/response_time.py +21 -0
  292. evalkit/core/evaluators/rubrics.py +494 -0
  293. evalkit/core/evaluators/tool_trajectory.py +187 -0
  294. evalkit/core/evaluators/trajectory_rubrics.py +198 -0
  295. evalkit/core/fixtures.py +57 -0
  296. evalkit/core/harness.py +227 -0
  297. evalkit/core/harness_check.py +191 -0
  298. evalkit/core/importer.py +257 -0
  299. evalkit/core/judge.py +78 -0
  300. evalkit/core/lint.py +265 -0
  301. evalkit/core/overlays.py +76 -0
  302. evalkit/core/paths.py +25 -0
  303. evalkit/core/promotion.py +94 -0
  304. evalkit/core/pull.py +198 -0
  305. evalkit/core/python_eval.py +59 -0
  306. evalkit/core/remote_evaluators.py +32 -0
  307. evalkit/core/review.py +220 -0
  308. evalkit/core/run_digest.py +421 -0
  309. evalkit/core/runner.py +995 -0
  310. evalkit/core/scorecard.py +255 -0
  311. evalkit/core/traces/__init__.py +0 -0
  312. evalkit/core/traces/convert.py +336 -0
  313. evalkit/core/traces/importer.py +113 -0
  314. evalkit/core/traces/mapping.py +385 -0
  315. evalkit/core/traces/poll.py +38 -0
  316. evalkit/core/traces/raw.py +21 -0
  317. evalkit/core/traces/sessions.py +172 -0
  318. evalkit/core/traces/settle.py +14 -0
  319. evalkit/core/traces/sources/__init__.py +19 -0
  320. evalkit/core/traces/sources/base.py +25 -0
  321. evalkit/core/traces/sources/phoenix.py +121 -0
  322. evalkit/core/trajectory.py +248 -0
  323. evalkit/core/workspace.py +30 -0
  324. evalkit/credentials.py +119 -0
  325. evalkit/db/__init__.py +0 -0
  326. evalkit/db/agents.py +289 -0
  327. evalkit/db/alembic/env.py +90 -0
  328. evalkit/db/alembic/script.py.mako +28 -0
  329. evalkit/db/alembic/versions/0011_baseline.py +1002 -0
  330. evalkit/db/alembic/versions/0012_ticket_token_kind.py +27 -0
  331. evalkit/db/alembic/versions/0013_keys_and_llm_providers.py +67 -0
  332. evalkit/db/cases.py +608 -0
  333. evalkit/db/crypto.py +12 -0
  334. evalkit/db/evaluators.py +156 -0
  335. evalkit/db/experiments.py +363 -0
  336. evalkit/db/fixture_blobs.py +91 -0
  337. evalkit/db/keys.py +78 -0
  338. evalkit/db/llm_providers.py +176 -0
  339. evalkit/db/locks.py +6 -0
  340. evalkit/db/mappings.py +168 -0
  341. evalkit/db/models.py +567 -0
  342. evalkit/db/promotion.py +271 -0
  343. evalkit/db/rekey.py +46 -0
  344. evalkit/db/review.py +250 -0
  345. evalkit/db/review_state.py +50 -0
  346. evalkit/db/runs.py +533 -0
  347. evalkit/db/scorecard.py +300 -0
  348. evalkit/db/session.py +71 -0
  349. evalkit/db/sessions.py +90 -0
  350. evalkit/db/spans.py +360 -0
  351. evalkit/db/store.py +209 -0
  352. evalkit/db/tokens.py +100 -0
  353. evalkit/db/trace_imports.py +308 -0
  354. evalkit/db/trace_sources.py +131 -0
  355. evalkit/db/users.py +186 -0
  356. evalkit/db/workspaces.py +232 -0
  357. evalkit/paths.py +33 -0
  358. evalkit/sdk/__init__.py +29 -0
  359. evalkit/sdk/binding.py +69 -0
  360. evalkit/sdk/evaluators.py +56 -0
  361. evalkit/sdk/runner.py +957 -0
  362. evalkit/sdk/tracing.py +166 -0
  363. evalkit/store/__init__.py +0 -0
  364. evalkit/store/base.py +354 -0
  365. evalkit/store/blob.py +69 -0
  366. evalkit/version.py +44 -0
  367. evalkit_py-0.2.0.dev12.dist-info/METADATA +205 -0
  368. evalkit_py-0.2.0.dev12.dist-info/RECORD +371 -0
  369. evalkit_py-0.2.0.dev12.dist-info/WHEEL +4 -0
  370. evalkit_py-0.2.0.dev12.dist-info/entry_points.txt +2 -0
  371. evalkit_py-0.2.0.dev12.dist-info/licenses/LICENSE +661 -0
evalkit/core/runner.py ADDED
@@ -0,0 +1,995 @@
1
+ """The experiment runner: `prepare_experiment` pins the selection and creates
2
+ the record (server only; it needs the review state), then `drive_experiment`
3
+ (phase 1: task runs) and `evaluate_experiment` (phase 2: scoring) run
4
+ anywhere a `Store` and a `BlobStore` are available. Both phases are idempotent so
5
+ a failed experiment resumes instead of restarting.
6
+
7
+ Phase 1 fans agent runs out to the harness under a concurrency cap, persists
8
+ each trajectory and the run's artifacts to the blob store at completion, and
9
+ records the run. Phase 2 walks recorded runs and executes configured
10
+ built-ins, configured custom evaluators, and every gauge, publishing evaluation
11
+ rows to the store. The runner holds a lease on the experiment throughout and
12
+ renews it while it works.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import asyncio
18
+ import json
19
+ import logging
20
+ import tempfile
21
+ import time
22
+ import uuid
23
+ from collections.abc import Awaitable, Callable, Iterable
24
+ from contextlib import ExitStack
25
+ from dataclasses import dataclass, field
26
+ from datetime import UTC, datetime
27
+ from typing import IO, Literal, Protocol
28
+
29
+ import httpx
30
+ from pydantic import BaseModel, ValidationError
31
+
32
+ from evalkit.config import RunnerConfig
33
+ from evalkit.contracts.atif import Trajectory, TrajectoryValidationError, validate_trajectory
34
+ from evalkit.contracts.custom_evaluator import EvalCase, ScoreItem
35
+ from evalkit.contracts.enums import (
36
+ EvaluationSource,
37
+ EvaluatorKind,
38
+ ExperimentStatus,
39
+ RunStatus,
40
+ TrajectoryStatus,
41
+ )
42
+ from evalkit.contracts.harness import (
43
+ Capabilities,
44
+ FileRef,
45
+ RunMetadata,
46
+ answer_text,
47
+ input_errors,
48
+ )
49
+ from evalkit.contracts.settings import JudgeSettings
50
+ from evalkit.core.agents import AgentDefinition, AgentLookup, explicit
51
+ from evalkit.core.blob import (
52
+ BlobStore,
53
+ artifact_key,
54
+ fixture_blob_key,
55
+ trajectory_key,
56
+ workspace_key,
57
+ )
58
+ from evalkit.core.capabilities import traced_capabilities
59
+ from evalkit.core.costmap import load_cost_map
60
+ from evalkit.core.custom_eval import EvalError, score_with_custom_evaluator
61
+ from evalkit.core.evaluators.base import (
62
+ BUILTIN_EVALUATORS,
63
+ BUILTIN_GAUGES,
64
+ EvalContext,
65
+ execute,
66
+ neutral_skip,
67
+ )
68
+ from evalkit.core.harness import HarnessClient, RunTimeout
69
+ from evalkit.core.paths import is_safe_path
70
+ from evalkit.core.python_eval import PythonEvaluator, score_with_python_evaluator
71
+ from evalkit.core.remote_evaluators import EvaluatorLookup, RemoteEvaluator
72
+ from evalkit.core.traces.poll import poll_trajectory
73
+ from evalkit.core.traces.sessions import SessionReader
74
+ from evalkit.core.trajectory import (
75
+ RunMetrics,
76
+ TrajectoryView,
77
+ backfill_step_costs,
78
+ derive_metrics,
79
+ )
80
+ from evalkit.core.workspace import parse_slug
81
+ from evalkit.credentials import KeySource
82
+ from evalkit.store.base import (
83
+ AgentRunRecord,
84
+ CaseView,
85
+ DatasetRow,
86
+ EvaluationRow,
87
+ Experiment,
88
+ ExperimentConfig,
89
+ ExperimentSnapshot,
90
+ GitRevision,
91
+ NewExperiment,
92
+ PinnedVersion,
93
+ RunForScoring,
94
+ ScoreRow,
95
+ Selection,
96
+ Store,
97
+ )
98
+
99
+ LOGGER = logging.getLogger(__name__)
100
+
101
+ LEASE_SECONDS = 60
102
+ RENEW_EVERY_SECONDS = 15
103
+ ACTIVE = frozenset({ExperimentStatus.RUNNING, ExperimentStatus.EVALUATING})
104
+ ALLOWED: frozenset[tuple[ExperimentStatus | None, ExperimentStatus]] = frozenset(
105
+ {
106
+ (None, ExperimentStatus.RUNNING),
107
+ (ExperimentStatus.RUNNING, ExperimentStatus.EVALUATING),
108
+ (ExperimentStatus.EVALUATING, ExperimentStatus.COMPLETED),
109
+ (ExperimentStatus.RUNNING, ExperimentStatus.FAILED),
110
+ (ExperimentStatus.EVALUATING, ExperimentStatus.FAILED),
111
+ (ExperimentStatus.FAILED, ExperimentStatus.RUNNING),
112
+ (ExperimentStatus.COMPLETED, ExperimentStatus.RUNNING),
113
+ (ExperimentStatus.COMPLETED, ExperimentStatus.EVALUATING),
114
+ (ExperimentStatus.FAILED, ExperimentStatus.EVALUATING),
115
+ }
116
+ )
117
+
118
+
119
+ class ExperimentRequest(BaseModel):
120
+ dataset: str
121
+ agent: str
122
+ selection: Selection = Selection(all=True)
123
+ repetitions: int = 1
124
+ model: str | None = None
125
+ capabilities: Capabilities | None = None
126
+ cases: GitRevision | None = None
127
+ evalkit_version: str | None = None
128
+ code_evaluators: list[str] | None = None
129
+ judge: JudgeSettings | None = None
130
+ holder: str | None = None
131
+ trajectory_wait_seconds: int | None = None
132
+
133
+
134
+ def _no_progress(message: str) -> None:
135
+ return None
136
+
137
+
138
+ class AgentKeySource(Protocol):
139
+ async def agent_key(self, agent: AgentDefinition) -> str | None: ...
140
+
141
+
142
+ @dataclass
143
+ class RunnerDeps:
144
+ cfg: RunnerConfig
145
+ store: Store
146
+ blob_store: BlobStore
147
+ agents: AgentLookup
148
+ evaluators: EvaluatorLookup
149
+ credentials: KeySource
150
+ harness_transport: httpx.AsyncBaseTransport | None = None
151
+ custom_eval_transport: httpx.AsyncBaseTransport | None = None
152
+ progress: Callable[[str], None] = _no_progress
153
+ sessions: SessionReader | None = None
154
+ python_evaluators: dict[str, PythonEvaluator] = field(default_factory=dict)
155
+ agent_keys: AgentKeySource | None = None
156
+
157
+
158
+ class ExperimentFactory(Protocol):
159
+ async def dataset(self, ref: str) -> DatasetRow: ...
160
+
161
+ async def cases_for(
162
+ self, dataset: str, selection: Selection, *, agent: str | None
163
+ ) -> list[CaseView]: ...
164
+
165
+ async def create_experiment(self, req: NewExperiment) -> Experiment: ...
166
+
167
+
168
+ async def _spool_fixtures(
169
+ deps: RunnerDeps, case: CaseView, stack: ExitStack
170
+ ) -> list[tuple[str, IO[bytes]]]:
171
+ files: list[tuple[str, IO[bytes]]] = []
172
+ for file in case.files:
173
+ spool = stack.enter_context(tempfile.TemporaryFile())
174
+ blob = await deps.blob_store.get_stream(fixture_blob_key(file.sha256))
175
+ async for chunk in blob.chunks:
176
+ await asyncio.to_thread(spool.write, chunk)
177
+ spool.seek(0)
178
+ files.append((file.key, spool))
179
+ return files
180
+
181
+
182
+ async def _fixture_refs(deps: RunnerDeps, case: CaseView, *, with_urls: bool) -> list[FileRef]:
183
+ return [
184
+ FileRef(
185
+ name=file.key,
186
+ url=await deps.blob_store.object_url(fixture_blob_key(file.sha256))
187
+ if with_urls
188
+ else None,
189
+ context=file.context,
190
+ )
191
+ for file in case.files
192
+ ]
193
+
194
+
195
+ class AgentArchived(ValueError):
196
+ def __init__(self, ref: str):
197
+ super().__init__(f"agent {ref} is archived")
198
+
199
+
200
+ class NothingRunnable(ValueError):
201
+ def __init__(self, dataset: str, dropped: list[str]):
202
+ reason = (
203
+ "every selected case needs fixture files and the harness accepts none: "
204
+ + ", ".join(dropped)
205
+ if dropped
206
+ else "no cases match the selection"
207
+ )
208
+ super().__init__(f"nothing to run in {dataset}: {reason}")
209
+
210
+
211
+ class InvalidCaseInputs(Exception):
212
+ def __init__(self, agent: str, bad: dict[str, str]):
213
+ self.agent = agent
214
+ self.bad = bad
215
+ lines = "\n".join(f" {case_id}: {error}" for case_id, error in sorted(bad.items()))
216
+ super().__init__(f"{len(bad)} case input(s) fail agent {agent}'s input_schema:\n{lines}")
217
+
218
+
219
+ class LeaseLost(ValueError):
220
+ def __init__(self, exp_id: int):
221
+ self.exp_id = exp_id
222
+ super().__init__(
223
+ f"experiment {exp_id} was taken over by another runner or marked failed; "
224
+ f"check it with evalkit status {exp_id}, then resume it"
225
+ )
226
+
227
+
228
+ def error_text(exc: BaseException) -> str:
229
+ message = str(exc)
230
+ return f"{type(exc).__name__}: {message}" if message else type(exc).__name__
231
+
232
+
233
+ class ScoringFailed(Exception):
234
+ def __init__(self, exp_id: int, failed: int, scored: int, first: BaseException):
235
+ self.exp_id = exp_id
236
+ super().__init__(
237
+ f"scoring failed for {failed} of {failed + scored} runs of experiment {exp_id}; "
238
+ f"first error: {error_text(first)}"
239
+ )
240
+
241
+
242
+ def case_title(case_input: object) -> str | None:
243
+ question = case_input.get("question") if isinstance(case_input, dict) else None
244
+ if not isinstance(question, str) or not question.strip():
245
+ return None
246
+ return question.strip().splitlines()[0][:200]
247
+
248
+
249
+ async def _persist_artifacts(
250
+ deps: RunnerDeps,
251
+ client: HarnessClient,
252
+ harness_run_id: str,
253
+ refs: list[dict],
254
+ ) -> None:
255
+ for ref in refs:
256
+ path = ref.get("path")
257
+ if not path:
258
+ continue
259
+ if not is_safe_path(path):
260
+ LOGGER.warning(
261
+ "artifact %s for run %s has an unsafe path; skipped", path, harness_run_id
262
+ )
263
+ continue
264
+ try:
265
+ data = await client.get_file(harness_run_id, path)
266
+ except httpx.HTTPError as exc:
267
+ LOGGER.warning("artifact %s for run %s unavailable: %s", path, harness_run_id, exc)
268
+ continue
269
+ await deps.blob_store.put_object(artifact_key(client.agent.id, harness_run_id, path), data)
270
+
271
+
272
+ async def _persist_workspace_tree(
273
+ deps: RunnerDeps, client: HarnessClient, harness_run_id: str
274
+ ) -> None:
275
+ try:
276
+ tree = await client.list_files(harness_run_id)
277
+ except httpx.HTTPError as exc:
278
+ LOGGER.warning("workspace tree for run %s unavailable: %s", harness_run_id, exc)
279
+ return
280
+ await deps.blob_store.put_object(
281
+ workspace_key(client.agent.id, harness_run_id), tree.model_dump_json().encode()
282
+ )
283
+
284
+
285
+ @dataclass
286
+ class DrivenRun:
287
+ case: CaseView
288
+ repetition: int
289
+ started: datetime
290
+ harness_run_id: str
291
+ answer: str | None = None
292
+ output: dict | None = None
293
+ error: str | None = None
294
+ run_status: RunStatus = RunStatus.FAILED
295
+ metrics: dict = field(default_factory=dict)
296
+ reported_model: str | None = None
297
+ trajectory: Trajectory | None = None
298
+ trajectory_status: TrajectoryStatus | None = None
299
+ wall_ms: int = 0
300
+
301
+
302
+ async def _drive_one(
303
+ deps: RunnerDeps,
304
+ exp: Experiment,
305
+ client: HarnessClient,
306
+ caps: Capabilities,
307
+ case: CaseView,
308
+ repetition: int,
309
+ model: str | None,
310
+ ) -> DrivenRun:
311
+ started = datetime.now(UTC)
312
+ clock = time.monotonic()
313
+ file_refs: list[FileRef] | None = None
314
+ if case.files:
315
+ file_refs = await _fixture_refs(deps, case, with_urls=caps.accepts.file_urls)
316
+ with ExitStack() as stack:
317
+ files = (
318
+ await _spool_fixtures(deps, case, stack)
319
+ if case.files and not caps.accepts.file_urls
320
+ else None
321
+ )
322
+ harness_run_id = await client.start_run(
323
+ case.input,
324
+ files=files,
325
+ file_refs=file_refs,
326
+ model=model,
327
+ metadata=RunMetadata(
328
+ experiment_id=str(exp.id), case_id=case.case_id, repetition=repetition
329
+ ),
330
+ )
331
+ driven = DrivenRun(
332
+ case=case, repetition=repetition, started=started, harness_run_id=harness_run_id
333
+ )
334
+ try:
335
+ status = await client.poll_until_terminal(harness_run_id)
336
+ driven.answer = status.answer
337
+ driven.output = status.output
338
+ driven.error = status.error
339
+ driven.run_status = RunStatus(status.status)
340
+ driven.reported_model = status.model
341
+ if status.status != "completed" and driven.error is None:
342
+ driven.error = f"run ended with status {status.status}"
343
+ if status.artifacts:
344
+ driven.metrics["artifacts"] = [
345
+ a.model_dump(exclude_none=True) for a in status.artifacts
346
+ ]
347
+ except RunTimeout:
348
+ driven.run_status = RunStatus.TIMEOUT
349
+ driven.error = f"run timed out after {client.agent.settings.run_timeout_seconds}s"
350
+ except ValidationError as exc:
351
+ driven.error = f"harness protocol violation: {exc.errors()[0]['msg']}"
352
+ driven.wall_ms = int((time.monotonic() - clock) * 1000)
353
+ fetch = not client.agent.is_otel and caps.provides.trajectory
354
+ if fetch and driven.error != "cancelled":
355
+ try:
356
+ driven.trajectory = await client.trajectory(harness_run_id)
357
+ driven.trajectory_status = TrajectoryStatus.VALID
358
+ except TrajectoryValidationError as exc:
359
+ driven.trajectory_status = TrajectoryStatus.INVALID
360
+ LOGGER.warning("trajectory for run %s failed ATIF validation: %s", harness_run_id, exc)
361
+ except httpx.HTTPError as exc:
362
+ driven.trajectory_status = TrajectoryStatus.UNAVAILABLE
363
+ LOGGER.warning("trajectory fetch for run %s failed: %s", harness_run_id, exc)
364
+ return driven
365
+
366
+
367
+ def reused_run_id(harness_run_id: str) -> str:
368
+ return f"{harness_run_id}#reused-{uuid.uuid4().hex[:12]}"
369
+
370
+
371
+ async def store_trajectory(
372
+ deps: RunnerDeps,
373
+ agent_id: int,
374
+ harness_run_id: str,
375
+ trajectory: Trajectory,
376
+ status: TrajectoryStatus | None,
377
+ model: str | None,
378
+ ) -> str:
379
+ backfill_step_costs(trajectory, load_cost_map(deps.cfg.cost_map), model)
380
+ if status == TrajectoryStatus.PARTIAL:
381
+ trajectory.extra = {**(trajectory.extra or {}), "trajectory_status": "partial"}
382
+ key = trajectory_key(agent_id, harness_run_id)
383
+ await deps.blob_store.put_object(
384
+ key, json.dumps(trajectory.model_dump(exclude_none=True)).encode()
385
+ )
386
+ return key
387
+
388
+
389
+ async def _record_one(
390
+ deps: RunnerDeps,
391
+ exp: Experiment,
392
+ client: HarnessClient,
393
+ caps: Capabilities,
394
+ driven: DrivenRun,
395
+ holder: str | None,
396
+ ) -> str | None:
397
+ agent = client.agent
398
+ used = await deps.store.harness_run_id_used(exp.id, driven.harness_run_id)
399
+ if used is not None:
400
+ driven.harness_run_id = reused_run_id(driven.harness_run_id)
401
+ driven.run_status = RunStatus.FAILED
402
+ driven.error = used
403
+ driven.trajectory = None
404
+ driven.trajectory_status = TrajectoryStatus.UNAVAILABLE
405
+ driven.metrics.pop("artifacts", None)
406
+ elif agent.is_otel and driven.error != "cancelled":
407
+ if deps.sessions is None:
408
+ raise ValueError(
409
+ f"agent {agent.name} takes its trajectory from traces but no session reader "
410
+ "is configured"
411
+ )
412
+ doc, status = await poll_trajectory(
413
+ deps.sessions,
414
+ exp.agent,
415
+ driven.harness_run_id,
416
+ agent.settings.trajectory_wait_seconds,
417
+ )
418
+ driven.trajectory_status = TrajectoryStatus(status)
419
+ if doc is not None:
420
+ driven.trajectory = validate_trajectory(doc)
421
+ key: str | None = None
422
+ if driven.trajectory is not None:
423
+ key = await store_trajectory(
424
+ deps,
425
+ agent.id,
426
+ driven.harness_run_id,
427
+ driven.trajectory,
428
+ driven.trajectory_status,
429
+ exp.config.model or driven.reported_model,
430
+ )
431
+ view = TrajectoryView(driven.trajectory)
432
+ driven.metrics.update(derive_metrics(view, driven.wall_ms).model_dump(exclude_none=True))
433
+ driven.metrics.setdefault("duration_ms", driven.wall_ms)
434
+ await deps.store.record_run(
435
+ exp.id,
436
+ driven.case.case_id,
437
+ driven.repetition,
438
+ AgentRunRecord(
439
+ harness_run_id=driven.harness_run_id,
440
+ status=driven.run_status,
441
+ answer=driven.answer,
442
+ output=driven.output,
443
+ error=driven.error,
444
+ started_at=driven.started,
445
+ ended_at=datetime.now(UTC),
446
+ trajectory_key=key,
447
+ trajectory_status=driven.trajectory_status,
448
+ metrics=driven.metrics,
449
+ title=case_title(driven.case.input),
450
+ ),
451
+ holder=holder,
452
+ )
453
+ deps.progress(f"run {driven.case.case_id} #{driven.repetition} {driven.run_status}")
454
+ if caps.provides.workspace and driven.metrics.get("artifacts"):
455
+ await _persist_artifacts(deps, client, driven.harness_run_id, driven.metrics["artifacts"])
456
+ if caps.provides.workspace and used is None:
457
+ await _persist_workspace_tree(deps, client, driven.harness_run_id)
458
+ return driven.reported_model
459
+
460
+
461
+ def dropped_cases_text(dropped: list[str]) -> str:
462
+ return (
463
+ f"skipped {len(dropped)} case(s) with fixture files; the agent accepts neither "
464
+ f"files nor file urls: {', '.join(dropped)}"
465
+ )
466
+
467
+
468
+ def runnable(cases: list[CaseView], caps: Capabilities) -> tuple[list[CaseView], list[str]]:
469
+ if caps.accepts.file_input:
470
+ return cases, []
471
+ gated = sorted(case.case_id for case in cases if case.files)
472
+ return [case for case in cases if not case.files], gated
473
+
474
+
475
+ def _check_inputs(agent: str, caps: Capabilities, cases: list[CaseView]) -> None:
476
+ bad = {}
477
+ for case in cases:
478
+ errors = input_errors(caps.effective_input_schema, case.input)
479
+ if errors:
480
+ bad[case.case_id] = errors[0]
481
+ if bad:
482
+ raise InvalidCaseInputs(agent, bad)
483
+
484
+
485
+ async def harness_capabilities(
486
+ agent: AgentDefinition, api_key: str | None, transport: httpx.AsyncBaseTransport | None
487
+ ) -> Capabilities:
488
+ client = HarnessClient(agent, api_key, transport=transport)
489
+ try:
490
+ return traced_capabilities(await client.capabilities(), agent)
491
+ finally:
492
+ await client.aclose()
493
+
494
+
495
+ async def prepare_experiment(
496
+ factory: ExperimentFactory,
497
+ req: ExperimentRequest,
498
+ *,
499
+ agent: AgentDefinition,
500
+ caps: Capabilities,
501
+ remote_evaluators: dict[str, RemoteEvaluator],
502
+ judge: JudgeSettings,
503
+ runner: str,
504
+ holder: str,
505
+ revision: GitRevision | None = None,
506
+ ) -> tuple[Experiment, list[str]]:
507
+ model = req.model
508
+ if model is not None and not caps.accepts.model_override:
509
+ LOGGER.warning(
510
+ "harness %s does not accept model override — running with its default model",
511
+ req.agent,
512
+ )
513
+ model = None
514
+ dataset = await factory.dataset(req.dataset)
515
+ cases = await factory.cases_for(dataset.ref, req.selection, agent=agent.ref)
516
+ cases, dropped = runnable(cases, caps)
517
+ if not cases:
518
+ raise NothingRunnable(dataset.ref, dropped)
519
+ _check_inputs(req.agent, caps, cases)
520
+ config = ExperimentConfig(
521
+ model=model,
522
+ judge=judge,
523
+ selection=req.selection,
524
+ revision=req.cases or revision,
525
+ snapshot=ExperimentSnapshot(
526
+ capabilities=caps.model_dump(),
527
+ trajectory_source=agent.trajectory_source,
528
+ trajectory_wait_seconds=agent.settings.trajectory_wait_seconds
529
+ if req.trajectory_wait_seconds is None
530
+ else req.trajectory_wait_seconds,
531
+ remote_evaluators={
532
+ name: evaluator.settings for name, evaluator in remote_evaluators.items()
533
+ },
534
+ code_evaluators=sorted(req.code_evaluators or []),
535
+ evalkit_version=req.evalkit_version,
536
+ ),
537
+ )
538
+ exp = await factory.create_experiment(
539
+ NewExperiment(
540
+ dataset=dataset.ref,
541
+ agent=agent.ref,
542
+ runner=runner,
543
+ repetitions=req.repetitions,
544
+ config=config,
545
+ cases=[
546
+ PinnedVersion(case_ref=case.case_ref, content_hash=case.content_hash)
547
+ for case in cases
548
+ ],
549
+ holder=holder,
550
+ )
551
+ )
552
+ return exp, dropped
553
+
554
+
555
+ async def _agent_for(deps: RunnerDeps, exp: Experiment) -> AgentDefinition:
556
+ agent = await deps.agents.get(explicit(parse_slug(exp.agent)))
557
+ if agent.archived:
558
+ raise AgentArchived(agent.ref)
559
+ return agent
560
+
561
+
562
+ async def missing_local_keys(
563
+ deps: RunnerDeps, agent: AgentDefinition, cases: Iterable[CaseView]
564
+ ) -> list[str]:
565
+ used = {name for case in cases for name in case.effective_evaluators.get("custom") or {}}
566
+ lines = []
567
+ if agent.harness_url is not None and deps.credentials.key_for("agent", agent.ref) is None:
568
+ lines.append(
569
+ f"no credentials.agents entry for {agent.ref} in evalkit.yaml; "
570
+ "calling the harness without x-api-key"
571
+ )
572
+ for name, evaluator in (await deps.evaluators.in_workspace(agent.workspace)).items():
573
+ if name in used and deps.credentials.key_for("evaluator", evaluator.ref) is None:
574
+ lines.append(
575
+ f"no credentials.evaluators entry for {evaluator.ref} in evalkit.yaml; "
576
+ "calling it without x-api-key"
577
+ )
578
+ return lines
579
+
580
+
581
+ async def drive_experiment(deps: RunnerDeps, exp_id: int, holder: str | None = None) -> None:
582
+ exp = await deps.store.get_experiment(exp_id)
583
+ agent = await _agent_for(deps, exp)
584
+ client = HarnessClient(
585
+ agent,
586
+ deps.credentials.key_for("agent", agent.ref, agent.harness_url),
587
+ transport=deps.harness_transport,
588
+ )
589
+ # Runs execute under the experiment's snapshotted contract (capabilities,
590
+ # model, pinned cases), never a fresh harness call, so resumed runs are
591
+ # gated and modelled like the rest of the experiment.
592
+ caps = Capabilities.model_validate(exp.config.snapshot.capabilities)
593
+ model = exp.config.model
594
+ try:
595
+ cases = {case.case_id: case for case in await deps.store.pinned_cases(exp_id)}
596
+ judge_key(deps, exp.config.judge, cases.values())
597
+ limit = agent.settings.max_concurrent_runs
598
+ if caps.limits.max_concurrent_runs is not None:
599
+ limit = min(limit, caps.limits.max_concurrent_runs)
600
+ semaphore = asyncio.Semaphore(max(limit, 1))
601
+ reported_models: list[str | None] = []
602
+
603
+ async def bounded(case: CaseView, repetition: int) -> None:
604
+ async with semaphore:
605
+ driven = await _drive_one(deps, exp, client, caps, case, repetition, model)
606
+ reported_models.append(await _record_one(deps, exp, client, caps, driven, holder))
607
+
608
+ await asyncio.gather(
609
+ *(
610
+ bounded(cases[pending.case_id], pending.repetition)
611
+ for pending in await deps.store.pending_runs(exp_id)
612
+ )
613
+ )
614
+ if model is None:
615
+ observed = next((m for m in reported_models if m), None)
616
+ if observed is not None:
617
+ await deps.store.set_model(exp_id, observed)
618
+ finally:
619
+ await client.aclose()
620
+
621
+
622
+ async def _renew_forever(store: Store, exp_id: int, holder: str) -> None:
623
+ renewed_at = time.monotonic()
624
+ while True:
625
+ await asyncio.sleep(RENEW_EVERY_SECONDS)
626
+ try:
627
+ renewed = await store.renew_lease(exp_id, holder)
628
+ except Exception as exc:
629
+ if time.monotonic() - renewed_at >= LEASE_SECONDS:
630
+ raise LeaseLost(exp_id) from exc
631
+ LOGGER.warning("renewing the lease on experiment %s failed: %s", exp_id, exc)
632
+ continue
633
+ if not renewed:
634
+ LOGGER.warning("lost lease on experiment %s", exp_id)
635
+ raise LeaseLost(exp_id)
636
+ renewed_at = time.monotonic()
637
+
638
+
639
+ async def with_lease(store: Store, exp_id: int, holder: str, work: Awaitable[None]) -> None:
640
+ renew = asyncio.create_task(_renew_forever(store, exp_id, holder))
641
+ main = asyncio.ensure_future(work)
642
+ try:
643
+ await asyncio.wait({main, renew}, return_when=asyncio.FIRST_COMPLETED)
644
+ if not main.done():
645
+ main.cancel()
646
+ await asyncio.gather(main, return_exceptions=True)
647
+ renew.result()
648
+ main.result()
649
+ finally:
650
+ renew.cancel()
651
+ main.cancel()
652
+
653
+
654
+ async def run_leased(deps: RunnerDeps, exp_id: int, holder: str, work: Awaitable[None]) -> None:
655
+ try:
656
+ await with_lease(deps.store, exp_id, holder, work)
657
+ except LeaseLost:
658
+ raise
659
+ except Exception as exc:
660
+ await deps.store.transition(exp_id, ExperimentStatus.FAILED, holder, error=error_text(exc))
661
+ raise
662
+ await deps.store.transition(exp_id, ExperimentStatus.COMPLETED, holder)
663
+
664
+
665
+ async def run_experiment(deps: RunnerDeps, exp_id: int, holder: str) -> None:
666
+ async def work() -> None:
667
+ await drive_experiment(deps, exp_id, holder)
668
+ await deps.store.transition(exp_id, ExperimentStatus.EVALUATING, holder)
669
+ await evaluate_experiment(deps, exp_id, holder)
670
+
671
+ await run_leased(deps, exp_id, holder, work())
672
+
673
+
674
+ async def rescore_experiment(
675
+ deps: RunnerDeps, exp_id: int, holder: str, *, refetch_trajectories: bool = False
676
+ ) -> None:
677
+ await run_leased(
678
+ deps,
679
+ exp_id,
680
+ holder,
681
+ evaluate_experiment(deps, exp_id, holder, refetch_trajectories=refetch_trajectories),
682
+ )
683
+
684
+
685
+ def judge_key_for(
686
+ credentials: KeySource, judge: JudgeSettings, cases: Iterable[CaseView]
687
+ ) -> str | None:
688
+ needed = any(
689
+ BUILTIN_EVALUATORS[name].uses_judge
690
+ for case in cases
691
+ for name in _configured_builtins(case.effective_evaluators)
692
+ )
693
+ return credentials.require_judge_key(judge.base_url) if needed else None
694
+
695
+
696
+ def judge_key(deps: RunnerDeps, judge: JudgeSettings, cases: Iterable[CaseView]) -> str | None:
697
+ return judge_key_for(deps.credentials, judge, cases)
698
+
699
+
700
+ def _configured_builtins(evaluators: dict) -> dict[str, dict]:
701
+ configured: dict[str, dict] = {}
702
+ for family in ("output", "trajectory"):
703
+ for name, config in (evaluators.get(family) or {}).items():
704
+ if name in BUILTIN_EVALUATORS:
705
+ configured[name] = config
706
+ return configured
707
+
708
+
709
+ REFETCHABLE = (TrajectoryStatus.PARTIAL, TrajectoryStatus.UNAVAILABLE)
710
+ REFETCH_CONCURRENCY = 8
711
+ SCORABLE = (TrajectoryStatus.VALID, TrajectoryStatus.PARTIAL)
712
+
713
+
714
+ async def stored_trajectory_doc(
715
+ blob_store: BlobStore, agent_id: int, run: AgentRunRecord
716
+ ) -> tuple[dict | None, TrajectoryStatus | None]:
717
+ status = run.trajectory_status
718
+ if status in REFETCHABLE:
719
+ candidate = trajectory_key(agent_id, run.harness_run_id)
720
+ if await blob_store.object_exists(candidate):
721
+ doc = json.loads(await blob_store.get_object(candidate))
722
+ return doc, TrajectoryStatus((doc.get("extra") or {}).get("trajectory_status", "valid"))
723
+ if run.trajectory_key and status in SCORABLE:
724
+ return json.loads(await blob_store.get_object(run.trajectory_key)), status
725
+ return None, status
726
+
727
+
728
+ async def _load_view(deps: RunnerDeps, agent_id: int, run: AgentRunRecord) -> TrajectoryView | None:
729
+ doc, status = await stored_trajectory_doc(deps.blob_store, agent_id, run)
730
+ if doc is None or status not in SCORABLE:
731
+ return None
732
+ return TrajectoryView(validate_trajectory(doc))
733
+
734
+
735
+ def _rows(
736
+ producer: str, source: EvaluationSource, scores: Iterable[ScoreRow]
737
+ ) -> list[EvaluationRow]:
738
+ return [
739
+ EvaluationRow(
740
+ producer=producer,
741
+ name=score.name,
742
+ kind=EvaluatorKind.GAUGE if score.kind == "gauge" else EvaluatorKind.SCORE,
743
+ source=source,
744
+ value=score.score,
745
+ label=score.label,
746
+ explanation=score.explanation,
747
+ error=score.error,
748
+ )
749
+ for score in scores
750
+ ]
751
+
752
+
753
+ def _custom_rows(
754
+ name: str,
755
+ kind: Literal["evaluator", "gauge"],
756
+ source: EvaluationSource,
757
+ outcome: list[ScoreItem] | EvalError,
758
+ ) -> list[EvaluationRow]:
759
+ if isinstance(outcome, EvalError):
760
+ return _rows(
761
+ name,
762
+ source,
763
+ [ScoreRow(name=name, kind=kind, score=None, label="error", error=outcome.message)],
764
+ )
765
+ return _rows(
766
+ name,
767
+ source,
768
+ [
769
+ ScoreRow(
770
+ name=item.name,
771
+ kind=kind,
772
+ score=item.score,
773
+ label=item.label,
774
+ explanation=item.explanation,
775
+ )
776
+ for item in outcome
777
+ ],
778
+ )
779
+
780
+
781
+ def custom_case_input(case: CaseView) -> dict:
782
+ if not case.files:
783
+ return dict(case.input)
784
+ return {**case.input, "files": [file.model_dump() for file in case.files]}
785
+
786
+
787
+ async def _evaluate_run(
788
+ deps: RunnerDeps,
789
+ exp: Experiment,
790
+ agent: AgentDefinition,
791
+ caps: Capabilities,
792
+ cost_map: dict,
793
+ run: RunForScoring,
794
+ judge_semaphore: asyncio.Semaphore,
795
+ judge_api_key: str | None,
796
+ configured_custom: dict[str, RemoteEvaluator],
797
+ holder: str | None,
798
+ *,
799
+ force: bool = False,
800
+ ) -> None:
801
+ record = run.agent_run
802
+ structured = record.output
803
+ answer = (
804
+ answer_text(record.answer, structured) if structured is not None else record.answer or ""
805
+ )
806
+ metrics_dict = dict(record.metrics)
807
+ view = await _load_view(deps, agent.id, record)
808
+ if view is not None:
809
+ wall = metrics_dict.get("duration_ms")
810
+ metrics_dict = {**metrics_dict, **derive_metrics(view, wall).model_dump(exclude_none=True)}
811
+ ctx = EvalContext(
812
+ case=run.case,
813
+ answer=answer,
814
+ output=structured,
815
+ trajectory_view=view,
816
+ wall_ms=metrics_dict.get("duration_ms") or 0,
817
+ judge_cfg=exp.config.judge,
818
+ judge_api_key=judge_api_key,
819
+ metrics=RunMetrics.model_validate(metrics_dict) if metrics_dict else None,
820
+ session_id=record.harness_run_id,
821
+ cost_map=cost_map,
822
+ )
823
+ already = set() if force else run.done
824
+ rows: list[EvaluationRow] = []
825
+ evaluators = run.case.effective_evaluators
826
+
827
+ for name, config in _configured_builtins(evaluators).items():
828
+ evaluator = BUILTIN_EVALUATORS[name]
829
+ if set(evaluator.emits) <= already:
830
+ continue
831
+ async with judge_semaphore:
832
+ try:
833
+ scores = await execute(evaluator, ctx, config, caps)
834
+ except Exception as exc:
835
+ LOGGER.warning(
836
+ "evaluator %s errored on run %s: %s", name, run.experiment_run_id, exc
837
+ )
838
+ scores = [ScoreRow(name=name, score=None, label="error", error=str(exc))]
839
+ rows.extend(_rows(name, EvaluationSource.BUILTIN, scores))
840
+
841
+ custom_config = evaluators.get("custom") or {}
842
+ trajectory_doc: dict = {}
843
+ if view is not None:
844
+ trajectory_doc = view.trajectory.model_dump(exclude_none=True)
845
+ eval_case = EvalCase(
846
+ id=run.case.case_id,
847
+ input=custom_case_input(run.case),
848
+ expected=run.case.expected,
849
+ config={},
850
+ )
851
+ for name, config in custom_config.items():
852
+ if name in already:
853
+ continue
854
+ python_evaluator = deps.python_evaluators.get(name)
855
+ if python_evaluator is not None:
856
+ if python_evaluator.requires_trajectory and view is None:
857
+ rows.extend(
858
+ _rows(
859
+ name, EvaluationSource.CODE, [neutral_skip(name, "run has no trajectory")]
860
+ )
861
+ )
862
+ continue
863
+ outcome = await score_with_python_evaluator(
864
+ python_evaluator,
865
+ case=eval_case.model_copy(update={"config": config}),
866
+ answer=answer,
867
+ output=structured,
868
+ trajectory=trajectory_doc,
869
+ )
870
+ rows.extend(_custom_rows(name, python_evaluator.kind, EvaluationSource.CODE, outcome))
871
+ continue
872
+ evaluator_cfg = configured_custom.get(name)
873
+ if evaluator_cfg is None:
874
+ rows.extend(
875
+ _rows(
876
+ name,
877
+ EvaluationSource.REMOTE,
878
+ [
879
+ ScoreRow(
880
+ name=name,
881
+ score=None,
882
+ label="error",
883
+ error=f"custom evaluator {name} is not configured",
884
+ )
885
+ ],
886
+ )
887
+ )
888
+ continue
889
+ result = await score_with_custom_evaluator(
890
+ evaluator_cfg,
891
+ deps.credentials.key_for(
892
+ "evaluator", evaluator_cfg.ref, evaluator_cfg.settings.endpoint
893
+ ),
894
+ case=eval_case.model_copy(update={"config": config}),
895
+ answer=answer,
896
+ output=structured,
897
+ trajectory=trajectory_doc,
898
+ transport=deps.custom_eval_transport,
899
+ )
900
+ rows.extend(_custom_rows(name, evaluator_cfg.wire_kind, EvaluationSource.REMOTE, result))
901
+
902
+ for name, gauge in BUILTIN_GAUGES.items():
903
+ if name in already:
904
+ continue
905
+ rows.extend(_rows(name, EvaluationSource.BUILTIN, await execute(gauge, ctx, {}, caps)))
906
+
907
+ if rows:
908
+ await deps.store.publish_evaluations(
909
+ run.experiment_run_id, rows, force=force, holder=holder
910
+ )
911
+ deps.progress(f"scored {run.case.case_id} #{run.repetition} ({len(rows)} rows)")
912
+
913
+
914
+ async def _refetch(
915
+ deps: RunnerDeps, exp: Experiment, agent: AgentDefinition, runs: list[RunForScoring]
916
+ ) -> set[int]:
917
+ if deps.sessions is None:
918
+ raise ValueError("refetching trajectories needs a session reader")
919
+ wait = exp.config.snapshot.trajectory_wait_seconds
920
+ refetched: set[int] = set()
921
+ slots = asyncio.Semaphore(REFETCH_CONCURRENCY)
922
+ sessions = deps.sessions
923
+
924
+ async def one(run: RunForScoring) -> None:
925
+ async with slots:
926
+ doc, status = await poll_trajectory(
927
+ sessions, exp.agent, run.agent_run.harness_run_id, wait
928
+ )
929
+ if doc is None:
930
+ return
931
+ trajectory = validate_trajectory(doc)
932
+ trajectory.extra = {**(trajectory.extra or {}), "trajectory_status": status}
933
+ await store_trajectory(
934
+ deps,
935
+ agent.id,
936
+ run.agent_run.harness_run_id,
937
+ trajectory,
938
+ TrajectoryStatus(status),
939
+ exp.config.model,
940
+ )
941
+ refetched.add(run.experiment_run_id)
942
+ deps.progress(f"refetched trajectory for {run.case.case_id} #{run.repetition}: {status}")
943
+
944
+ await asyncio.gather(
945
+ *(one(run) for run in runs if run.agent_run.trajectory_status in REFETCHABLE)
946
+ )
947
+ return refetched
948
+
949
+
950
+ async def evaluate_experiment(
951
+ deps: RunnerDeps,
952
+ exp_id: int,
953
+ holder: str | None = None,
954
+ *,
955
+ refetch_trajectories: bool = False,
956
+ ) -> None:
957
+ exp = await deps.store.get_experiment(exp_id)
958
+ agent = await deps.agents.get(explicit(parse_slug(exp.agent)))
959
+ caps = Capabilities.model_validate(exp.config.snapshot.capabilities)
960
+ cost_map = load_cost_map(deps.cfg.cost_map)
961
+ runs = await deps.store.runs_to_score(exp_id, force=False)
962
+ refetched = await _refetch(deps, exp, agent, runs) if refetch_trajectories else set()
963
+ judge_semaphore = asyncio.Semaphore(exp.config.judge.concurrency)
964
+ api_key = judge_key(deps, exp.config.judge, [run.case for run in runs])
965
+ configured_custom = (
966
+ await deps.evaluators.in_workspace(agent.workspace)
967
+ if any(run.case.effective_evaluators.get("custom") for run in runs)
968
+ else {}
969
+ )
970
+ results = await asyncio.gather(
971
+ *(
972
+ _evaluate_run(
973
+ deps,
974
+ exp,
975
+ agent,
976
+ caps,
977
+ cost_map,
978
+ run,
979
+ judge_semaphore,
980
+ api_key,
981
+ configured_custom,
982
+ holder,
983
+ force=run.experiment_run_id in refetched,
984
+ )
985
+ for run in runs
986
+ ),
987
+ return_exceptions=True,
988
+ )
989
+ errors = [r for r in results if isinstance(r, BaseException)]
990
+ for error in errors:
991
+ if not isinstance(error, Exception):
992
+ raise error
993
+ if errors:
994
+ first = errors[0]
995
+ raise ScoringFailed(exp_id, len(errors), len(results) - len(errors), first) from first