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
@@ -0,0 +1,844 @@
1
+ import asyncio
2
+ import functools
3
+ import logging
4
+ from collections.abc import Awaitable, Callable
5
+ from datetime import date, datetime, timedelta, tzinfo
6
+ from typing import Any, get_args
7
+ from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
8
+
9
+ import httpx
10
+ from fastapi import HTTPException
11
+ from fastmcp import FastMCP
12
+ from fastmcp.exceptions import ToolError
13
+ from fastmcp.server.auth import require_scopes
14
+ from mcp.types import ToolAnnotations
15
+ from starlette.datastructures import State
16
+
17
+ from evalkit.api import experiments as experiments_api
18
+ from evalkit.api import runs as runs_api
19
+ from evalkit.api.agents import agent_view
20
+ from evalkit.api.mcp import descriptions as d
21
+ from evalkit.api.mcp.auth import SCOPE_READ
22
+ from evalkit.api.mcp.models import (
23
+ AgentRow,
24
+ BatchMeta,
25
+ DownloadRunFileResponse,
26
+ Evaluation,
27
+ ExperimentRow,
28
+ ExperimentRunRow,
29
+ ExperimentRunsMeta,
30
+ FileRow,
31
+ FileText,
32
+ FindAgentsResponse,
33
+ FindExperimentsResponse,
34
+ FindRunsResponse,
35
+ GetExperimentRunsResponse,
36
+ GetExperimentsResponse,
37
+ GetRunFilesResponse,
38
+ GetRunsResponse,
39
+ GetRunStatsResponse,
40
+ GetTrajectoryResponse,
41
+ ModelCostRow,
42
+ Money,
43
+ Note,
44
+ PagedMeta,
45
+ PlainMeta,
46
+ RunMetrics,
47
+ RunRow,
48
+ StatsMeta,
49
+ StatsRow,
50
+ SubagentRef,
51
+ ToolCallRow,
52
+ TrajectoryMeta,
53
+ TurnRow,
54
+ usd,
55
+ )
56
+ from evalkit.contracts.atif import Step, Trajectory
57
+ from evalkit.contracts.enums import ExperimentStatus
58
+ from evalkit.core.agents import AgentDefinition, UnknownAgent
59
+ from evalkit.core.costmap import load_cost_map
60
+ from evalkit.core.paths import is_safe_run_id
61
+ from evalkit.core.run_digest import (
62
+ GROUP_BY_VALUES,
63
+ GroupStats,
64
+ RunDigest,
65
+ StatsInput,
66
+ Turn,
67
+ clip,
68
+ content_text,
69
+ digest,
70
+ group_stats,
71
+ priced_steps,
72
+ turns,
73
+ )
74
+ from evalkit.core.traces.sessions import SessionTooLarge
75
+ from evalkit.core.trajectory import backfill_step_costs
76
+ from evalkit.core.workspace import parse_slug
77
+ from evalkit.db import experiments as db_experiments
78
+ from evalkit.db import scorecard as db_scorecard
79
+ from evalkit.db.runs import RunSource, trajectory_keys
80
+ from evalkit.db.workspaces import UnknownWorkspace
81
+ from evalkit.store.base import UnknownExperiment
82
+
83
+
84
+ def _registration(open_world: bool) -> dict[str, Any]:
85
+ return {
86
+ "annotations": ToolAnnotations(
87
+ read_only_hint=True, destructive_hint=False, open_world_hint=open_world
88
+ ),
89
+ "meta": {"anthropic/alwaysLoad": True},
90
+ "auth": require_scopes(SCOPE_READ),
91
+ }
92
+
93
+
94
+ # Open world: the tool reads the agent's harness live, not only evalkit's store.
95
+ READ_TOOL = _registration(open_world=False)
96
+ HARNESS_READ_TOOL = _registration(open_world=True)
97
+
98
+ FIND_RUNS_MAX = 200
99
+ FIND_RUNS_LIST_MAX = 5000
100
+ GET_RUNS_MAX = 200
101
+ STATS_MAX_RUNS = 5000
102
+ STEP_PAGE_MAX = 100
103
+ FILE_CHARS = 20000
104
+ FILE_CHARS_MAX = 200000
105
+ EXPERIMENTS_MAX = 100
106
+ GET_EXPERIMENTS_MAX = 20
107
+ # Per call, so one heavy call cannot queue ahead of a light one; the server-wide
108
+ # cap bounds what every call together puts on harnesses and the event loop.
109
+ FETCH_CONCURRENCY = 32
110
+ FETCH_CONCURRENCY_TOTAL = 64
111
+ # Whole call, listing included: under the 60 s load-balancer timeout.
112
+ CALL_BUDGET_SECONDS = 45
113
+ OVER_BUDGET = "not read within this call's time budget; ask for it again with get_runs"
114
+
115
+ LOGGER = logging.getLogger(__name__)
116
+ RUN_INCLUDES = frozenset({"metrics", "turns", "tool_calls"})
117
+ RUN_SOURCES: tuple[str, ...] = get_args(RunSource)
118
+ EXPERIMENT_INCLUDES = frozenset({"scorecard"})
119
+
120
+
121
+ STATUS_CODES = {
122
+ 400: "invalid_request",
123
+ 401: "forbidden",
124
+ 403: "forbidden",
125
+ 404: "not_found",
126
+ 409: "conflict",
127
+ 413: "invalid_request",
128
+ }
129
+
130
+
131
+ def fail(code: str, detail: str, log_level: int = logging.WARNING) -> ToolError:
132
+ return ToolError(f"{code}: {detail}", log_level=log_level)
133
+
134
+
135
+ def translate(exc: Exception) -> ToolError | None:
136
+ if isinstance(exc, HTTPException):
137
+ code = STATUS_CODES.get(exc.status_code, "upstream_failed")
138
+ level = logging.ERROR if exc.status_code == 500 else logging.WARNING
139
+ return fail(code, str(exc.detail), level)
140
+ if isinstance(exc, (UnknownAgent, UnknownExperiment, UnknownWorkspace)):
141
+ return fail("not_found", exc.args[0])
142
+ return None
143
+
144
+
145
+ # FastMCP logs any exception other than ToolError at ERROR before middleware
146
+ # sees it, so expected errors become ToolError inside the tool.
147
+ def expected_errors[**P, R](tool: Callable[P, Awaitable[R]]) -> Callable[P, Awaitable[R]]:
148
+ @functools.wraps(tool)
149
+ async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
150
+ try:
151
+ return await tool(*args, **kwargs)
152
+ except Exception as exc:
153
+ translated = translate(exc)
154
+ if translated is None:
155
+ raise
156
+ raise translated from exc
157
+
158
+ return wrapper
159
+
160
+
161
+ def deadline() -> float:
162
+ return asyncio.get_running_loop().time() + CALL_BUDGET_SECONDS
163
+
164
+
165
+ def check_run_ids(run_ids: list[str]) -> None:
166
+ unsafe = [r for r in run_ids if not is_safe_run_id(r)]
167
+ if unsafe:
168
+ raise fail("invalid_run_id", f"run ids must be one plain path segment: {unsafe[:5]}")
169
+
170
+
171
+ def check_agent_id(ref: str) -> str:
172
+ try:
173
+ parse_slug(ref)
174
+ except ValueError as exc:
175
+ raise fail(
176
+ "invalid_agent_id", f"{ref!r}: use the name@workspace id from find_agents"
177
+ ) from exc
178
+ return ref
179
+
180
+
181
+ def check_choice(code: str, value: str, allowed: tuple[str, ...]) -> None:
182
+ if value not in allowed:
183
+ raise fail(code, f"{value!r}; valid: {list(allowed)}")
184
+
185
+
186
+ def check_page(limit: int, offset: int, maximum: int) -> None:
187
+ if not 1 <= limit <= maximum:
188
+ raise fail("invalid_limit", f"limit must be between 1 and {maximum}; got {limit}")
189
+ if offset < 0:
190
+ raise fail("invalid_offset", f"offset must be 0 or greater; got {offset}")
191
+
192
+
193
+ def check_ids(ids: list[Any], maximum: int) -> None:
194
+ if not ids:
195
+ raise fail("no_ids_provided", "pass at least one id")
196
+ if len(ids) > maximum:
197
+ raise fail("too_many_ids", f"at most {maximum} ids per call; got {len(ids)}")
198
+
199
+
200
+ def check_include(include: list[str], allowed: frozenset[str]) -> set[str]:
201
+ unknown = sorted(set(include) - allowed)
202
+ if unknown:
203
+ raise fail("invalid_include", f"{unknown}; valid: {sorted(allowed)}")
204
+ return set(include)
205
+
206
+
207
+ def zone_of(name: str) -> ZoneInfo:
208
+ try:
209
+ return ZoneInfo(name)
210
+ except (ZoneInfoNotFoundError, ValueError) as exc:
211
+ raise fail("invalid_timezone", f"{name} is not an IANA timezone") from exc
212
+
213
+
214
+ def _bound(value: str, zone: tzinfo, *, end: bool) -> datetime:
215
+ try:
216
+ if len(value) == 10:
217
+ day = date.fromisoformat(value)
218
+ moment = datetime(day.year, day.month, day.day, tzinfo=zone)
219
+ return moment + timedelta(days=1) if end else moment
220
+ parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
221
+ except ValueError as exc:
222
+ raise fail(
223
+ "invalid_date_format", f"{value!r}: use YYYY-MM-DD or an ISO 8601 timestamp"
224
+ ) from exc
225
+ return parsed if parsed.tzinfo else parsed.replace(tzinfo=zone)
226
+
227
+
228
+ def window(
229
+ start_date: str | None, end_date: str | None, zone: tzinfo
230
+ ) -> tuple[datetime | None, datetime | None]:
231
+ since = _bound(start_date, zone, end=False) if start_date else None
232
+ until = _bound(end_date, zone, end=True) if end_date else None
233
+ if since and until and since >= until:
234
+ raise fail("invalid_range", "start_date must be before end_date")
235
+ return since, until
236
+
237
+
238
+ def paged_meta(
239
+ count: int, total: int | None, offset: int, has_more: bool | None = None
240
+ ) -> dict[str, Any]:
241
+ if has_more is None:
242
+ has_more = total is not None and offset + count < total
243
+ return {
244
+ "count": count,
245
+ "total": total,
246
+ "has_more": has_more,
247
+ "next_offset": offset + count if has_more else None,
248
+ }
249
+
250
+
251
+ def run_metrics(result: RunDigest) -> RunMetrics:
252
+ fields = result.model_dump(exclude={"run_id", "cost_usd", "tool_call_groups", "cost_by_model"})
253
+ by_model = [
254
+ ModelCostRow(**m.model_dump(exclude={"cost_usd"}), cost=Money(value=m.cost_usd))
255
+ for m in result.cost_by_model
256
+ ]
257
+ return RunMetrics(**fields, cost=usd(result.cost_usd), cost_by_model=by_model)
258
+
259
+
260
+ def turn_row(turn: Turn) -> TurnRow:
261
+ return TurnRow(**turn.model_dump(exclude={"cost_usd"}), cost=usd(turn.cost_usd))
262
+
263
+
264
+ def run_row(run: runs_api.BrowserRun) -> RunRow:
265
+ return RunRow(
266
+ id=run.run_id,
267
+ title=run.title,
268
+ created_at=run.created_at,
269
+ user_id=run.user_id,
270
+ client_id=run.client_id,
271
+ source=run.source,
272
+ status=run.status,
273
+ experiment_id=run.experiment_id,
274
+ experiment_number=run.experiment_number,
275
+ case_id=run.case_id,
276
+ )
277
+
278
+
279
+ def stats_row(stats: GroupStats) -> StatsRow:
280
+ return StatsRow(**stats.model_dump(exclude={"cost_usd"}), cost=Money(value=stats.cost_usd))
281
+
282
+
283
+ def clip_value(value: Any, max_chars: int) -> Any:
284
+ return value if len(repr(value)) <= max_chars else clip(str(value), max_chars)
285
+
286
+
287
+ def clipped_step(step: Step, max_chars: int) -> dict:
288
+ out = step.model_dump(exclude_none=True)
289
+ if step.message is not None:
290
+ out["message"] = clip(content_text(step.message), max_chars)
291
+ if step.reasoning_content is not None:
292
+ out["reasoning_content"] = clip(step.reasoning_content, max_chars)
293
+ for call in out.get("tool_calls") or []:
294
+ call["arguments"] = clip_value(call["arguments"], max_chars)
295
+ if step.observation is not None:
296
+ results = out["observation"]["results"]
297
+ for result, raw in zip(results, step.observation.results, strict=True):
298
+ if raw.content is not None:
299
+ result["content"] = clip(content_text(raw.content), max_chars)
300
+ return out
301
+
302
+
303
+ Digested = tuple[RunDigest, list[Turn]]
304
+
305
+
306
+ class Runs:
307
+ def __init__(self, state: State) -> None:
308
+ self.state = state
309
+ self.gate = asyncio.Semaphore(FETCH_CONCURRENCY_TOTAL)
310
+
311
+ async def agent(self, ref: str) -> AgentDefinition:
312
+ return await runs_api.agent_or_404(self.state.agents, check_agent_id(ref))
313
+
314
+ async def cost_map(self) -> dict:
315
+ return await asyncio.to_thread(load_cost_map, self.state.cfg.cost_map)
316
+
317
+ async def trajectory(self, record: AgentDefinition, run_id: str) -> tuple[Trajectory, set]:
318
+ async with self.state.session_factory() as session:
319
+ trajectory = await runs_api.fetch_trajectory(
320
+ record, run_id, self.state.runner_deps, self.state.sessions, session
321
+ )
322
+ reported = priced_steps(trajectory)
323
+ backfill_step_costs(trajectory, await self.cost_map())
324
+ return trajectory, reported
325
+
326
+ async def digests(
327
+ self, record: AgentDefinition, run_ids: list[str], budget: float
328
+ ) -> list[Digested | HTTPException]:
329
+ # One stored-run lookup and one cost map per call, and no DB connection
330
+ # held across harness calls; whatever is not read within the budget
331
+ # comes back as a per-run failure rather than outliving the request.
332
+ async with self.state.session_factory() as session:
333
+ keys = await trajectory_keys(session, record.id, run_ids)
334
+ cost_map = await self.cost_map()
335
+ deps = self.state.runner_deps
336
+ client = (
337
+ await runs_api.harness_client(deps, record)
338
+ if runs_api.server_may_drive(record)
339
+ else None
340
+ )
341
+
342
+ call_gate = asyncio.Semaphore(FETCH_CONCURRENCY)
343
+
344
+ async def one(run_id: str) -> Digested:
345
+ async with call_gate, self.gate:
346
+ trajectory = await runs_api.read_trajectory(
347
+ record, run_id, deps, self.state.sessions, keys.get(run_id), client
348
+ )
349
+ reported = priced_steps(trajectory)
350
+ backfill_step_costs(trajectory, cost_map)
351
+ return digest(run_id, trajectory, reported), turns(trajectory)
352
+
353
+ tasks = [asyncio.create_task(one(run_id)) for run_id in run_ids]
354
+ try:
355
+ remaining = budget - asyncio.get_running_loop().time()
356
+ if tasks and remaining > 0:
357
+ await asyncio.wait(tasks, timeout=remaining)
358
+ finally:
359
+ for task in tasks:
360
+ task.cancel()
361
+ await asyncio.gather(*tasks, return_exceptions=True)
362
+ if client is not None:
363
+ await client.aclose()
364
+ return [outcome(task) for task in tasks]
365
+
366
+ async def page(
367
+ self, record: AgentDefinition, *, limit: int, offset: int, budget: float, **filters: Any
368
+ ) -> runs_api.RunPage:
369
+ try:
370
+ async with asyncio.timeout_at(budget), self.state.session_factory() as session:
371
+ return await runs_api.resolve_runs(
372
+ record,
373
+ self.state.runner_deps,
374
+ self.state.spans,
375
+ session,
376
+ limit=limit,
377
+ offset=offset,
378
+ **filters,
379
+ )
380
+ except TimeoutError as exc:
381
+ raise fail(
382
+ "harness_timeout",
383
+ f"listing the agent's runs took longer than {CALL_BUDGET_SECONDS}s; "
384
+ "narrow the window or the filters",
385
+ ) from exc
386
+
387
+
388
+ def outcome(task: asyncio.Task[Digested]) -> Digested | HTTPException:
389
+ if task.cancelled():
390
+ return HTTPException(status_code=504, detail=OVER_BUDGET)
391
+ exc = task.exception()
392
+ if exc is None:
393
+ return task.result()
394
+ if isinstance(exc, HTTPException):
395
+ return exc
396
+ if isinstance(exc, httpx.HTTPError | TimeoutError):
397
+ return HTTPException(status_code=502, detail=f"harness unreachable ({type(exc).__name__})")
398
+ if isinstance(exc, ValueError):
399
+ return HTTPException(status_code=502, detail="malformed trajectory")
400
+ if isinstance(exc, SessionTooLarge):
401
+ return HTTPException(status_code=413, detail="session too large to convert")
402
+ if not isinstance(exc, Exception):
403
+ raise exc
404
+ LOGGER.error("mcp.trajectory_read_failed", exc_info=exc)
405
+ return HTTPException(
406
+ status_code=502, detail=f"trajectory could not be read ({type(exc).__name__})"
407
+ )
408
+
409
+
410
+ # A harness answers an id it does not know with 404, or 400/422 when the id is not
411
+ # even of its shape: either way the run does not exist there.
412
+ NOT_FOUND_STATUSES = frozenset({400, 404, 422})
413
+ ROW_ERROR_CODES = {504: "harness_timeout", 413: "session_too_large"}
414
+
415
+
416
+ def unread(result: Digested | HTTPException) -> bool:
417
+ return isinstance(result, HTTPException) and result.status_code not in NOT_FOUND_STATUSES
418
+
419
+
420
+ def unread_note(results: list[Digested | HTTPException], truncated: bool) -> Note | None:
421
+ unread_count = sum(1 for r in results if unread(r))
422
+ parts = []
423
+ if truncated:
424
+ parts.append("the window holds more runs than were read")
425
+ if unread_count:
426
+ parts.append(f"{unread_count} runs could not be read (time budget or harness errors)")
427
+ return Note(code="partial_coverage", message="; ".join(parts)) if parts else None
428
+
429
+
430
+ def with_digest(row: RunRow, result: Digested | HTTPException, include: set[str]) -> RunRow:
431
+ if isinstance(result, HTTPException):
432
+ if result.status_code in NOT_FOUND_STATUSES:
433
+ row.note = Note(code="not_found")
434
+ else:
435
+ row.error = ROW_ERROR_CODES.get(result.status_code, "harness_failed")
436
+ row.detail = f"trajectory: {result.detail}"
437
+ return row
438
+ found, run_turns = result
439
+ if "metrics" in include:
440
+ row.metrics = run_metrics(found)
441
+ if "turns" in include:
442
+ row.turns = [turn_row(t) for t in run_turns]
443
+ if "tool_calls" in include:
444
+ row.tool_calls = [ToolCallRow(**g.model_dump()) for g in found.tool_call_groups]
445
+ return row
446
+
447
+
448
+ def harness_note(page: runs_api.RunPage) -> Note | None:
449
+ if not page.harness_note:
450
+ return None
451
+ return Note(code="partial_coverage", message=page.harness_note)
452
+
453
+
454
+ def register(mcp: FastMCP, state: State) -> None:
455
+ runs = Runs(state)
456
+
457
+ @mcp.tool(description=d.FIND_AGENTS, **READ_TOOL)
458
+ @expected_errors
459
+ async def find_agents(
460
+ workspace: str | None = None, include_archived: bool = False
461
+ ) -> FindAgentsResponse:
462
+ found = await state.agents.list_agents(
463
+ workspace=workspace, include_archived=include_archived
464
+ )
465
+ rows = []
466
+ for agent in found:
467
+ view = agent_view(agent)
468
+ rows.append(
469
+ AgentRow(
470
+ id=view["ref"],
471
+ name=view["name"],
472
+ workspace=view["workspace"],
473
+ workspace_type=view["workspace_type"],
474
+ trajectory_source=view["trajectory_source"],
475
+ driver=view["driver"],
476
+ archived=view["archived"],
477
+ )
478
+ )
479
+ return FindAgentsResponse(data=rows, meta=PlainMeta(count=len(rows)))
480
+
481
+ @mcp.tool(description=d.FIND_RUNS, **HARNESS_READ_TOOL)
482
+ @expected_errors
483
+ async def find_runs(
484
+ agent: str,
485
+ start_date: str | None = None,
486
+ end_date: str | None = None,
487
+ source: str = "all",
488
+ query: str | None = None,
489
+ user_id: str | None = None,
490
+ client_id: str | None = None,
491
+ timezone: str = "UTC",
492
+ tool_error: str | None = None,
493
+ failed_only: bool = False,
494
+ include: list[str] | None = None,
495
+ limit: int = 50,
496
+ offset: int = 0,
497
+ ) -> FindRunsResponse:
498
+ wanted = check_include(include or [], RUN_INCLUDES)
499
+ check_choice("invalid_source", source, RUN_SOURCES)
500
+ by_digest = tool_error is not None or failed_only
501
+ if by_digest:
502
+ wanted = wanted | {"metrics"}
503
+ if wanted and limit > FIND_RUNS_MAX:
504
+ raise fail(
505
+ "invalid_limit",
506
+ f"at most {FIND_RUNS_MAX} with include, tool_error or failed_only; got {limit}. "
507
+ "List the window without them (up to 5000), then get_runs for the ids in batches",
508
+ )
509
+ check_page(limit, offset, FIND_RUNS_MAX if wanted else FIND_RUNS_LIST_MAX)
510
+ since, until = window(start_date, end_date, zone_of(timezone))
511
+ budget = deadline()
512
+ record = await runs.agent(agent)
513
+ filters = {
514
+ "source": source,
515
+ "since": since,
516
+ "until": until,
517
+ "query": query,
518
+ "user_id": user_id,
519
+ "client_id": client_id,
520
+ }
521
+ if by_digest:
522
+ # Which runs failed, or errored in a tool, is known only from their
523
+ # trajectories: digest the window (as get_run_stats does), keep the
524
+ # matching runs, then page those.
525
+ def matches(found: RunDigest) -> bool:
526
+ if failed_only and not found.failures:
527
+ return False
528
+ return tool_error is None or any(
529
+ e.tool == tool_error for e in found.tool_errors_by_tool
530
+ )
531
+
532
+ listing = await runs.page(
533
+ record, limit=STATS_MAX_RUNS, offset=0, budget=budget, **filters
534
+ )
535
+ results = await runs.digests(record, [run.run_id for run in listing.runs], budget)
536
+ # A run that could not be read might match: it stays, carrying its
537
+ # note, rather than vanishing from the count.
538
+ hits = [
539
+ (run, result)
540
+ for run, result in zip(listing.runs, results, strict=True)
541
+ if (
542
+ result.status_code not in NOT_FOUND_STATUSES
543
+ if isinstance(result, HTTPException)
544
+ else matches(result[0])
545
+ )
546
+ ]
547
+ rows = [
548
+ with_digest(run_row(run), result, wanted)
549
+ for run, result in hits[offset : offset + limit]
550
+ ]
551
+ note = harness_note(listing) or unread_note(
552
+ results, truncated=listing.total > len(listing.runs)
553
+ )
554
+ meta = PagedMeta(**paged_meta(len(rows), len(hits), offset), note=note)
555
+ return FindRunsResponse(data=rows, meta=meta)
556
+ page = await runs.page(record, limit=limit, offset=offset, budget=budget, **filters)
557
+ rows = [run_row(run) for run in page.runs]
558
+ note = harness_note(page)
559
+ if wanted:
560
+ results = await runs.digests(record, [run.run_id for run in page.runs], budget)
561
+ rows = [with_digest(r, res, wanted) for r, res in zip(rows, results, strict=True)]
562
+ note = note or unread_note(results, truncated=False)
563
+ meta = PagedMeta(**paged_meta(len(rows), page.total, offset), note=note)
564
+ return FindRunsResponse(data=rows, meta=meta)
565
+
566
+ @mcp.tool(description=d.GET_RUNS, **HARNESS_READ_TOOL)
567
+ @expected_errors
568
+ async def get_runs(
569
+ agent: str, run_ids: list[str], include: list[str] | None = None
570
+ ) -> GetRunsResponse:
571
+ check_ids(run_ids, GET_RUNS_MAX)
572
+ check_run_ids(run_ids)
573
+ wanted = check_include(["metrics"] if include is None else include, RUN_INCLUDES)
574
+ budget = deadline()
575
+ record = await runs.agent(agent)
576
+ page = await runs.page(record, limit=len(run_ids), offset=0, budget=budget, run_ids=run_ids)
577
+ listed = {run.run_id: run for run in page.runs}
578
+ results = await runs.digests(record, run_ids, budget)
579
+ rows = []
580
+ for run_id, result in zip(run_ids, results, strict=True):
581
+ run = listed.get(run_id)
582
+ row = run_row(run) if run else RunRow(id=run_id)
583
+ rows.append(with_digest(row, result, wanted))
584
+ unmatched = sum(1 for row in rows if row.note and row.note.code == "not_found")
585
+ failed = sum(1 for row in rows if row.error)
586
+ meta = BatchMeta(
587
+ count=len(rows),
588
+ matched=len(rows) - unmatched - failed,
589
+ unmatched=unmatched,
590
+ failed=failed or None,
591
+ )
592
+ return GetRunsResponse(data=rows, meta=meta)
593
+
594
+ @mcp.tool(description=d.GET_RUN_STATS, **HARNESS_READ_TOOL)
595
+ @expected_errors
596
+ async def get_run_stats(
597
+ agent: str,
598
+ start_date: str | None = None,
599
+ end_date: str | None = None,
600
+ group_by: str | list[str] = "none",
601
+ timezone: str = "UTC",
602
+ source: str = "all",
603
+ query: str | None = None,
604
+ user_id: str | None = None,
605
+ client_id: str | None = None,
606
+ max_runs: int = 500,
607
+ ) -> GetRunStatsResponse:
608
+ groupings = [group_by] if isinstance(group_by, str) else list(dict.fromkeys(group_by))
609
+ bad = [g for g in groupings if g not in GROUP_BY_VALUES]
610
+ if bad or not groupings:
611
+ raise fail("invalid_group_by", f"{bad or group_by!r}; valid: {list(GROUP_BY_VALUES)}")
612
+ check_choice("invalid_source", source, RUN_SOURCES)
613
+ if not 1 <= max_runs <= STATS_MAX_RUNS:
614
+ raise fail(
615
+ "invalid_limit", f"max_runs must be between 1 and {STATS_MAX_RUNS}; got {max_runs}"
616
+ )
617
+ zone = zone_of(timezone)
618
+ since, until = window(start_date, end_date, zone)
619
+ budget = deadline()
620
+ record = await runs.agent(agent)
621
+ filters = {
622
+ "source": source,
623
+ "since": since,
624
+ "until": until,
625
+ "query": query,
626
+ "user_id": user_id,
627
+ "client_id": client_id,
628
+ }
629
+ page = await runs.page(record, limit=max_runs, offset=0, budget=budget, **filters)
630
+ listed, in_window = page.runs, page.total
631
+ results = await runs.digests(record, [run.run_id for run in listed], budget)
632
+ note = harness_note(page) or unread_note(results, truncated=False)
633
+ items = [
634
+ StatsInput(
635
+ digest=result[0],
636
+ turn_durations=[
637
+ t.duration_seconds for t in result[1] if t.duration_seconds is not None
638
+ ],
639
+ source=run.source,
640
+ user_id=run.user_id,
641
+ created_at=run.created_at,
642
+ )
643
+ for run, result in zip(listed, results, strict=True)
644
+ if not isinstance(result, HTTPException)
645
+ ]
646
+ rows = [
647
+ stats_row(s)
648
+ for g in groupings
649
+ for s in (group_stats(items, zone, g) if items else []) # type: ignore[arg-type]
650
+ ]
651
+ meta = StatsMeta(
652
+ count=len(rows),
653
+ group_by=groupings,
654
+ timezone=timezone,
655
+ runs_considered=len(listed),
656
+ runs_in_window=in_window,
657
+ capped=in_window > len(listed),
658
+ unavailable=len(listed) - len(items),
659
+ note=note,
660
+ )
661
+ return GetRunStatsResponse(data=rows, meta=meta)
662
+
663
+ @mcp.tool(description=d.GET_TRAJECTORY, **HARNESS_READ_TOOL)
664
+ @expected_errors
665
+ async def get_trajectory(
666
+ agent: str,
667
+ run_id: str,
668
+ subagent: int | None = None,
669
+ max_chars: int = 2000,
670
+ limit: int = 20,
671
+ offset: int = 0,
672
+ ) -> GetTrajectoryResponse:
673
+ check_page(limit, offset, STEP_PAGE_MAX)
674
+ if not 100 <= max_chars <= 50000:
675
+ raise fail("invalid_max_chars", "max_chars must be between 100 and 50000")
676
+ check_run_ids([run_id])
677
+ record = await runs.agent(agent)
678
+ root, _ = await runs.trajectory(record, run_id)
679
+ subs = root.subagent_trajectories or []
680
+ trajectory = root
681
+ if subagent is not None:
682
+ if not 0 <= subagent < len(subs):
683
+ raise fail("not_found", f"run has {len(subs)} subagent trajectories")
684
+ trajectory = subs[subagent]
685
+ steps = [clipped_step(s, max_chars) for s in trajectory.steps[offset : offset + limit]]
686
+ meta = TrajectoryMeta(
687
+ **paged_meta(len(steps), len(trajectory.steps), offset),
688
+ schema_version=root.schema_version,
689
+ session_id=trajectory.session_id,
690
+ trajectory_id=trajectory.trajectory_id,
691
+ agent=trajectory.agent.model_dump(exclude_none=True, exclude={"tool_definitions"})
692
+ if trajectory.agent
693
+ else None,
694
+ final_metrics=trajectory.final_metrics.model_dump(exclude_none=True)
695
+ if trajectory.final_metrics
696
+ else None,
697
+ subagents=[
698
+ SubagentRef(
699
+ index=i,
700
+ trajectory_id=sub.trajectory_id,
701
+ agent=sub.agent.name if sub.agent else None,
702
+ steps=len(sub.steps),
703
+ )
704
+ for i, sub in enumerate(subs)
705
+ ],
706
+ )
707
+ return GetTrajectoryResponse(data=steps, meta=meta)
708
+
709
+ @mcp.tool(description=d.GET_RUN_FILES, **HARNESS_READ_TOOL)
710
+ @expected_errors
711
+ async def get_run_files(agent: str, run_id: str) -> GetRunFilesResponse:
712
+ check_run_ids([run_id])
713
+ listing = await runs_api.list_files(
714
+ check_agent_id(agent), run_id, state.runner_deps, state.agents
715
+ )
716
+ rows = [FileRow(**f) for f in listing["files"]]
717
+ return GetRunFilesResponse(data=rows, meta=PlainMeta(count=len(rows)))
718
+
719
+ @mcp.tool(description=d.DOWNLOAD_RUN_FILE, **HARNESS_READ_TOOL)
720
+ @expected_errors
721
+ async def download_run_file(
722
+ agent: str, run_id: str, path: str, max_chars: int = FILE_CHARS, offset: int = 0
723
+ ) -> DownloadRunFileResponse:
724
+ if not 1 <= max_chars <= FILE_CHARS_MAX:
725
+ raise fail("invalid_max_chars", f"max_chars must be between 1 and {FILE_CHARS_MAX}")
726
+ if offset < 0:
727
+ raise fail("invalid_offset", f"offset must be 0 or greater; got {offset}")
728
+ check_run_ids([run_id])
729
+ response = await runs_api.get_file(
730
+ check_agent_id(agent), run_id, path, state.runner_deps, state.agents
731
+ )
732
+ body = b"".join(
733
+ [c.encode() if isinstance(c, str) else bytes(c) async for c in response.body_iterator]
734
+ )
735
+ try:
736
+ text = body.decode("utf-8")
737
+ except UnicodeDecodeError:
738
+ row = FileText(
739
+ path=path,
740
+ size=len(body),
741
+ note=Note(code="not_applicable", message="binary file; not returned as text"),
742
+ )
743
+ meta = PagedMeta(count=1, total=None, has_more=False)
744
+ return DownloadRunFileResponse(data=[row], meta=meta)
745
+ chunk = text[offset : offset + max_chars]
746
+ window_meta = paged_meta(len(chunk), len(text), offset)
747
+ meta = PagedMeta(**{**window_meta, "count": 1})
748
+ return DownloadRunFileResponse(
749
+ data=[FileText(path=path, size=len(body), text=chunk)], meta=meta
750
+ )
751
+
752
+ @mcp.tool(description=d.FIND_EXPERIMENTS, **READ_TOOL)
753
+ @expected_errors
754
+ async def find_experiments(
755
+ workspace: str | None = None,
756
+ status: str | None = None,
757
+ limit: int = 20,
758
+ offset: int = 0,
759
+ ) -> FindExperimentsResponse:
760
+ check_page(limit, offset, EXPERIMENTS_MAX)
761
+ if status is not None:
762
+ check_choice("invalid_status", status, tuple(ExperimentStatus))
763
+ async with state.session_factory() as session:
764
+ found = await experiments_api.list_experiments(
765
+ session,
766
+ workspace=workspace,
767
+ status=ExperimentStatus(status) if status else None,
768
+ limit=limit + 1,
769
+ offset=offset,
770
+ )
771
+ rows = [ExperimentRow(**{k: v for k, v in e.items() if k != "config"}) for e in found]
772
+ meta = PagedMeta(**paged_meta(min(len(rows), limit), None, offset, len(rows) > limit))
773
+ rows = rows[:limit]
774
+ return FindExperimentsResponse(data=rows, meta=meta)
775
+
776
+ @mcp.tool(description=d.GET_EXPERIMENTS, **READ_TOOL)
777
+ @expected_errors
778
+ async def get_experiments(
779
+ experiment_ids: list[int], include: list[str] | None = None
780
+ ) -> GetExperimentsResponse:
781
+ check_ids(experiment_ids, GET_EXPERIMENTS_MAX)
782
+ wanted = check_include(include or [], EXPERIMENT_INCLUDES)
783
+ rows = []
784
+ async with state.session_factory() as session:
785
+ for experiment_id in experiment_ids:
786
+ try:
787
+ exp = await db_experiments.get_experiment(session, experiment_id)
788
+ except UnknownExperiment:
789
+ rows.append(ExperimentRow(id=experiment_id, note=Note(code="not_found")))
790
+ continue
791
+ extra: dict[str, Any] = {
792
+ "progress": (await db_scorecard.progress(session, experiment_id)).model_dump()
793
+ }
794
+ if "scorecard" in wanted:
795
+ extra["scorecard"] = await experiments_api.scorecard(
796
+ experiment_id, state.store, None
797
+ )
798
+ rows.append(ExperimentRow(**exp.model_dump(exclude={"config"}), **extra))
799
+ unmatched = sum(1 for r in rows if r.note)
800
+ meta = BatchMeta(count=len(rows), matched=len(rows) - unmatched, unmatched=unmatched)
801
+ return GetExperimentsResponse(data=rows, meta=meta)
802
+
803
+ @mcp.tool(description=d.GET_EXPERIMENT_RUNS, **READ_TOOL)
804
+ @expected_errors
805
+ async def get_experiment_runs(
806
+ experiment_id: int, failed_only: bool = False, limit: int = 50, offset: int = 0
807
+ ) -> GetExperimentRunsResponse:
808
+ check_page(limit, offset, FIND_RUNS_MAX)
809
+ exp = await state.store.get_experiment(experiment_id)
810
+ rows = []
811
+ for run in await state.store.run_details(experiment_id):
812
+ evaluations = [
813
+ Evaluation(
814
+ name=e.name,
815
+ kind=e.kind,
816
+ value=e.value,
817
+ label=e.label,
818
+ explanation=clip(e.explanation, 1000),
819
+ error=e.error,
820
+ )
821
+ for e in run.evaluations
822
+ ]
823
+ failed = run.agent_run.error is not None or any(
824
+ e.kind == "score" and e.value is not None and e.value < 1 for e in evaluations
825
+ )
826
+ if failed_only and not failed:
827
+ continue
828
+ rows.append(
829
+ ExperimentRunRow(
830
+ id=run.agent_run.harness_run_id,
831
+ case_id=run.case_id,
832
+ repetition=run.repetition,
833
+ status=run.agent_run.status,
834
+ error=run.agent_run.error,
835
+ answer=clip(run.agent_run.answer, 2000),
836
+ started_at=run.agent_run.started_at,
837
+ ended_at=run.agent_run.ended_at,
838
+ metrics=run.agent_run.metrics,
839
+ evaluations=evaluations,
840
+ )
841
+ )
842
+ page = rows[offset : offset + limit]
843
+ meta = ExperimentRunsMeta(**paged_meta(len(page), len(rows), offset), agent=exp.agent)
844
+ return GetExperimentRunsResponse(data=page, meta=meta)