reeval 0.9.0 → 0.10.0
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.
- package/README.md +42 -30
- package/THIRD_PARTY_NOTICES.md +218 -11
- package/assets/package.json +1 -1
- package/assets/pnpm-lock.yaml +30 -30
- package/dist/adapter-DWlpOtRn.js +279 -0
- package/dist/adapter-DWlpOtRn.js.map +1 -0
- package/dist/{agent-6FefIhX7.js → agent-CURAwx_q.js} +6 -6
- package/dist/{agent-6FefIhX7.js.map → agent-CURAwx_q.js.map} +1 -1
- package/dist/{agent-WSmTaesf.js → agent-CaS_2Sce.js} +46 -15
- package/dist/agent-CaS_2Sce.js.map +1 -0
- package/dist/{analysis-DlGBDjTj.js → analysis-Bxub9LMT.js} +4 -4
- package/dist/{analysis-DlGBDjTj.js.map → analysis-Bxub9LMT.js.map} +1 -1
- package/dist/{assemble-BCZzTyR7.js → assemble-B9Alds-z.js} +69 -41
- package/dist/assemble-B9Alds-z.js.map +1 -0
- package/dist/assemble-J5MukZFY.js +1 -0
- package/dist/{client-Bbo9SPlK.js → assert-EUIoRdIt.js} +10 -113
- package/dist/assert-EUIoRdIt.js.map +1 -0
- package/dist/bridge/index.mjs +248 -55
- package/dist/{capture-Bp74Z6Fr.js → capture-CNb9SuU5.js} +145 -31
- package/dist/capture-CNb9SuU5.js.map +1 -0
- package/dist/capture-jquXZBKK.js +1 -0
- package/dist/{cli-CVrMh_qz.js → cli-D303Vj5N.js} +133 -115
- package/dist/cli-D303Vj5N.js.map +1 -0
- package/dist/{cli-spec-CKEYa8V2.js → cli-spec-BPZbQG2p.js} +36 -17
- package/dist/cli-spec-BPZbQG2p.js.map +1 -0
- package/dist/cli.js +4 -2
- package/dist/cli.js.map +1 -1
- package/dist/client-fcwgwOAD.js +106 -0
- package/dist/client-fcwgwOAD.js.map +1 -0
- package/dist/{command-CzIFp1JQ.js → command-BLfT7vnQ.js} +18 -17
- package/dist/command-BLfT7vnQ.js.map +1 -0
- package/dist/{command-C5xFPKHM.js → command-BT9a37ZR.js} +374 -72
- package/dist/command-BT9a37ZR.js.map +1 -0
- package/dist/{command-DUT5l13I.js → command-Bge8F7Ko.js} +316 -83
- package/dist/command-Bge8F7Ko.js.map +1 -0
- package/dist/command-BkFjijN2.js +1 -0
- package/dist/{command-BFP0hKEb.js → command-CMJtHKRa.js} +14 -14
- package/dist/{command-BFP0hKEb.js.map → command-CMJtHKRa.js.map} +1 -1
- package/dist/commands-1p-bIWhN.js +1 -0
- package/dist/{commands-CLmPBrTS.js → commands-BFx16WB8.js} +7 -7
- package/dist/commands-BFx16WB8.js.map +1 -0
- package/dist/config-DEgnUiU-.js +756 -0
- package/dist/config-DEgnUiU-.js.map +1 -0
- package/dist/{context-DbSMEVKs.js → context-C1mxXh80.js} +11 -3
- package/dist/context-C1mxXh80.js.map +1 -0
- package/dist/{db-CfQFwJtq.js → db-CdHizc4_.js} +455 -36
- package/dist/db-CdHizc4_.js.map +1 -0
- package/dist/db-Dm1cRJJP.js +2 -0
- package/dist/debug-D7bEv_UV.js +1 -0
- package/dist/{dist-CTzThfu_.js → dist-B8hJImYt.js} +17 -4
- package/dist/dist-B8hJImYt.js.map +1 -0
- package/dist/{docker-CRQGQbIt.js → docker-7b_2SaH7.js} +22 -21
- package/dist/docker-7b_2SaH7.js.map +1 -0
- package/dist/{docker-DV2GNgle.js → docker-sZrNrr82.js} +3 -3
- package/dist/docker-sZrNrr82.js.map +1 -0
- package/dist/docker-yauK-9X8.js +1 -0
- package/dist/{doctor-DlKycx5H.js → doctor-BYNok4UO.js} +262 -83
- package/dist/doctor-BYNok4UO.js.map +1 -0
- package/dist/{duration-JsJrIWXq.js → duration-CEOQww97.js} +2 -2
- package/dist/{duration-JsJrIWXq.js.map → duration-CEOQww97.js.map} +1 -1
- package/dist/{egress-BhzNh7Yi.js → egress-C4PZTSSe.js} +146 -14
- package/dist/egress-C4PZTSSe.js.map +1 -0
- package/dist/environment-DNjRk0nU.js +1133 -0
- package/dist/environment-DNjRk0nU.js.map +1 -0
- package/dist/{errors-yDAEwmZo.js → errors-B6BSMs03.js} +2 -2
- package/dist/errors-B6BSMs03.js.map +1 -0
- package/dist/{errors-C9w_ud7g.js → errors-r025B0Zb.js} +4 -4
- package/dist/errors-r025B0Zb.js.map +1 -0
- package/dist/evaluate-C1UVL21l.d.ts +92 -0
- package/dist/evaluate-D4zeRGBF.js +2351 -0
- package/dist/evaluate-D4zeRGBF.js.map +1 -0
- package/dist/files-BAt0i00X.js +1 -0
- package/dist/{files-CSzVADHn.js → files-BXUdY8tr.js} +9 -8
- package/dist/{files-CSzVADHn.js.map → files-BXUdY8tr.js.map} +1 -1
- package/dist/files-BhyltlDB.js +1 -0
- package/dist/files-Bk1mC3AN.js +43 -0
- package/dist/files-Bk1mC3AN.js.map +1 -0
- package/dist/{files-DJUFg-4e.js → files-DQmHE7pb.js} +5 -5
- package/dist/{files-DJUFg-4e.js.map → files-DQmHE7pb.js.map} +1 -1
- package/dist/files-ZZPqfAo6.js +1 -0
- package/dist/harness-B8QC9u0O.js +323 -0
- package/dist/harness-B8QC9u0O.js.map +1 -0
- package/dist/harness.d.ts +16 -30
- package/dist/harness.js +1 -2
- package/dist/{header-CALhmafp.js → header-hK-Z_Po9.js} +2 -2
- package/dist/{header-CALhmafp.js.map → header-hK-Z_Po9.js.map} +1 -1
- package/dist/{images-BMgdpMAX.js → images-0On0cztu.js} +114 -45
- package/dist/images-0On0cztu.js.map +1 -0
- package/dist/index.d.ts +9 -37
- package/dist/index.js +6 -2138
- package/dist/{init-BIdLF0F9.js → init-B77Rpcqc.js} +26 -8
- package/dist/init-B77Rpcqc.js.map +1 -0
- package/dist/init-BrOWin3W.js +1 -0
- package/dist/{invocations-CLaL8AuW.js → invocations-DbGfBAA_.js} +4 -4
- package/dist/{invocations-CLaL8AuW.js.map → invocations-DbGfBAA_.js.map} +1 -1
- package/dist/{judge-CeYPN8P_.js → judge-DW_GDOmB.js} +11 -19
- package/dist/{judge-CeYPN8P_.js.map → judge-DW_GDOmB.js.map} +1 -1
- package/dist/judge.d.ts +2 -2
- package/dist/judge.js +1 -1
- package/dist/{live-S0qODb6o.js → live-jlfszAbb.js} +9 -20
- package/dist/live-jlfszAbb.js.map +1 -0
- package/dist/matchers-Ddmc2z7H.js +118 -0
- package/dist/matchers-Ddmc2z7H.js.map +1 -0
- package/dist/{model-DEQo3FkZ.js → model-DaVCY3rb.js} +2 -2
- package/dist/{model-DEQo3FkZ.js.map → model-DaVCY3rb.js.map} +1 -1
- package/dist/model-NM3ID0uR.js +1 -0
- package/dist/{model-B1BZoZ9b.js → model-h_cDtcRr.js} +18 -17
- package/dist/model-h_cDtcRr.js.map +1 -0
- package/dist/model-nwWJvaUR.js +1 -0
- package/dist/{model-BOnlAPzs.js → model-oqcDplJG.js} +111 -19
- package/dist/model-oqcDplJG.js.map +1 -0
- package/dist/{node-Co7EXge0.js → node-B3ydHo6t.js} +10 -47
- package/dist/node-B3ydHo6t.js.map +1 -0
- package/dist/object-CLf2-tNq.js +9 -0
- package/dist/object-CLf2-tNq.js.map +1 -0
- package/dist/{opencode-providers-ByD85Q0l.js → opencode-providers-J9colP0-.js} +96 -21
- package/dist/opencode-providers-J9colP0-.js.map +1 -0
- package/dist/options-Bh2W63dK.js +1119 -0
- package/dist/options-Bh2W63dK.js.map +1 -0
- package/dist/{pairs-DDsjgm5F.js → pairs-CnInRu-G.js} +76 -2
- package/dist/{pairs-DDsjgm5F.js.map → pairs-CnInRu-G.js.map} +1 -1
- package/dist/{patch-bDV8YFW0.js → patch-Zac5aIRM.js} +6 -6
- package/dist/{patch-bDV8YFW0.js.map → patch-Zac5aIRM.js.map} +1 -1
- package/dist/{plan-DUydFc_1.js → plan-C9Cy27Ay.js} +24 -9
- package/dist/plan-C9Cy27Ay.js.map +1 -0
- package/dist/preflight-CPWpQeBI.js +1 -0
- package/dist/{preflight-BkoSxf4C.js → preflight-D7iFPKeK.js} +8 -7
- package/dist/preflight-D7iFPKeK.js.map +1 -0
- package/dist/prepare-BGjePFQl.js +2 -0
- package/dist/{prepare-CUAWHebF.js → prepare-doGxaAGR.js} +38 -30
- package/dist/prepare-doGxaAGR.js.map +1 -0
- package/dist/{provenance-D7HK3vzt.js → provenance-DmE9uPVt.js} +3 -3
- package/dist/{provenance-D7HK3vzt.js.map → provenance-DmE9uPVt.js.map} +1 -1
- package/dist/provider-DehR4CMN.js +1 -0
- package/dist/providers-gDMko8fl.js +1 -0
- package/dist/providers-tAOD1NQN.js +165 -0
- package/dist/providers-tAOD1NQN.js.map +1 -0
- package/dist/{query-BjBMLa-C.js → query-Bes9Kh6Y.js} +3 -3
- package/dist/{query-BjBMLa-C.js.map → query-Bes9Kh6Y.js.map} +1 -1
- package/dist/query-C_G_svLU.js +1 -0
- package/dist/questions-DjAwMxZ3.js +524 -0
- package/dist/questions-DjAwMxZ3.js.map +1 -0
- package/dist/{record-BRP9vZrj.js → record-BKmMvZ6O.js} +9 -1
- package/dist/{record-BRP9vZrj.js.map → record-BKmMvZ6O.js.map} +1 -1
- package/dist/remote-HDRt16ew.js +378 -0
- package/dist/remote-HDRt16ew.js.map +1 -0
- package/dist/{results-Di7m8dQ3.js → results-DuauQI3Z.js} +4 -4
- package/dist/{results-Di7m8dQ3.js.map → results-DuauQI3Z.js.map} +1 -1
- package/dist/{resume-SUz1GskV.js → resume-BRfa8E_s.js} +10 -10
- package/dist/{resume-SUz1GskV.js.map → resume-BRfa8E_s.js.map} +1 -1
- package/dist/{root-CvdRlRZX.js → root-DICDHCtp.js} +4 -4
- package/dist/{root-CvdRlRZX.js.map → root-DICDHCtp.js.map} +1 -1
- package/dist/{runtime-DdsfnRz_.js → runtime-PX_oXiCS.js} +23 -71
- package/dist/runtime-PX_oXiCS.js.map +1 -0
- package/dist/sandbox-D3A8oUb7.js +70 -0
- package/dist/sandbox-D3A8oUb7.js.map +1 -0
- package/dist/sandbox.d.ts +19 -6
- package/dist/sandbox.js +1 -2
- package/dist/server-C7sg1ndo.js +1 -0
- package/dist/{server-BNnXtmS8.js → server-XDleKaF_.js} +11 -11
- package/dist/server-XDleKaF_.js.map +1 -0
- package/dist/shared-DYpIb1BI.js +166 -0
- package/dist/shared-DYpIb1BI.js.map +1 -0
- package/dist/{source-Dof0NQBm.js → source-C8xumIWj.js} +2 -2
- package/dist/{source-Dof0NQBm.js.map → source-C8xumIWj.js.map} +1 -1
- package/dist/source-fGd0SaKP.js +1 -0
- package/dist/{src-8rRXy4u9.js → src-B7treWa8.js} +1538 -180
- package/dist/src-B7treWa8.js.map +1 -0
- package/dist/{src-C1E9HDWO.js → src-C7_1SqDP.js} +3 -3
- package/dist/{src-C1E9HDWO.js.map → src-C7_1SqDP.js.map} +1 -1
- package/dist/{src-DDNXsD9H.js → src-CF17Ug08.js} +5 -4
- package/dist/src-CF17Ug08.js.map +1 -0
- package/dist/{src-C_4Umgh2.js → src-CFp86GtV.js} +52 -51
- package/dist/src-CFp86GtV.js.map +1 -0
- package/dist/src-C_lPtowT.js +2 -0
- package/dist/src-DIL0jST0.js +368 -0
- package/dist/src-DIL0jST0.js.map +1 -0
- package/dist/src-DNnG4e4R.js +2 -0
- package/dist/src-DTbrY1hi.js +2 -0
- package/dist/src-DdyJEh2v.js +2 -0
- package/dist/src-Dj9PAroc.js +2 -0
- package/dist/src-Du8H85Qq.js +2 -0
- package/dist/src-DvCg0NBs.js +324 -0
- package/dist/src-DvCg0NBs.js.map +1 -0
- package/dist/{src-COgE7991.js → src-DvK0EIZx.js} +5 -5
- package/dist/{src-COgE7991.js.map → src-DvK0EIZx.js.map} +1 -1
- package/dist/{src-DVy58-zZ.js → src-FGXhkEkH.js} +5 -5
- package/dist/{src-DVy58-zZ.js.map → src-FGXhkEkH.js.map} +1 -1
- package/dist/src-jzagbuqH.js +2 -0
- package/dist/src-ofUaYbuY.js +3 -0
- package/dist/src-pJZOrYR9.js +1 -0
- package/dist/{src-D95O-Smv.js → src-u8_ixn_F.js} +15 -11
- package/dist/src-u8_ixn_F.js.map +1 -0
- package/dist/{state-CiXyLqg0.js → state-C__9swoG.js} +35 -23
- package/dist/state-C__9swoG.js.map +1 -0
- package/dist/state-CeR6oz0k.js +66 -0
- package/dist/state-CeR6oz0k.js.map +1 -0
- package/dist/state-CgjQo29K.js +1 -0
- package/dist/studio-app/assets/index-CwQN3f_h.css +2 -0
- package/dist/studio-app/assets/{index-CROH2NB3.js → index-DrQc2Yxd.js} +11 -11
- package/dist/studio-app/index.html +2 -2
- package/dist/{summary-BTksrIxn.js → summary-BLAVLySU.js} +150 -19
- package/dist/summary-BLAVLySU.js.map +1 -0
- package/dist/{summary-2OAWF2N7.js → summary-DgyjUh_l.js} +7 -7
- package/dist/{summary-2OAWF2N7.js.map → summary-DgyjUh_l.js.map} +1 -1
- package/dist/testing.d.ts +3 -2
- package/dist/testing.js +3 -2
- package/dist/{tool-target-BU1JnSE9.js → tool-target-D2mcYlSQ.js} +14 -7
- package/dist/tool-target-D2mcYlSQ.js.map +1 -0
- package/dist/{tools-B10Uj7OI.js → tools-GCyNYSc1.js} +6 -6
- package/dist/tools-GCyNYSc1.js.map +1 -0
- package/dist/transcript-BNNANU8A.js +64 -0
- package/dist/transcript-BNNANU8A.js.map +1 -0
- package/dist/{types-D8RLfkFP.d.ts → types-g17bRAzt.d.ts} +389 -42
- package/dist/{usage-DDF_6tkV.js → usage-CleNKkly.js} +4 -2
- package/dist/{usage-DDF_6tkV.js.map → usage-CleNKkly.js.map} +1 -1
- package/dist/view/index.d.ts +42 -8
- package/dist/view/index.js +6 -6
- package/dist/views-BW6oCDUQ.js +2 -0
- package/dist/{views-Dqa5ojfZ.js → views-BmQF4UCI.js} +13 -8
- package/dist/views-BmQF4UCI.js.map +1 -0
- package/dist/{worker-entry-BAikHl9F.js → worker-entry-yOchGYYv.js} +16 -229
- package/dist/worker-entry-yOchGYYv.js.map +1 -0
- package/dist/{workspace-C-TrNIUc.js → workspace-CgyHEM0l.js} +6 -6
- package/dist/{workspace-C-TrNIUc.js.map → workspace-CgyHEM0l.js.map} +1 -1
- package/dist/workspace-D8sSgpiy.js +1 -0
- package/package.json +12 -12
- package/dist/agent-WSmTaesf.js.map +0 -1
- package/dist/assemble-BCZzTyR7.js.map +0 -1
- package/dist/assemble-C5qq5yT2.js +0 -2
- package/dist/capture-Bp74Z6Fr.js.map +0 -1
- package/dist/capture-DS-cPyuE.js +0 -2
- package/dist/cli-CVrMh_qz.js.map +0 -1
- package/dist/cli-spec-CKEYa8V2.js.map +0 -1
- package/dist/client-Bbo9SPlK.js.map +0 -1
- package/dist/command-BrmXUC-W.js +0 -2
- package/dist/command-C5xFPKHM.js.map +0 -1
- package/dist/command-CzIFp1JQ.js.map +0 -1
- package/dist/command-DUT5l13I.js.map +0 -1
- package/dist/commands-CLmPBrTS.js.map +0 -1
- package/dist/commands-fI3G2Wnh.js +0 -2
- package/dist/config-CGvBeewH.js +0 -457
- package/dist/config-CGvBeewH.js.map +0 -1
- package/dist/context-DbSMEVKs.js.map +0 -1
- package/dist/db-CSSFtGcN.js +0 -3
- package/dist/db-CfQFwJtq.js.map +0 -1
- package/dist/debug-SEmW4w8f.js +0 -2
- package/dist/dist-CTzThfu_.js.map +0 -1
- package/dist/docker-CRQGQbIt.js.map +0 -1
- package/dist/docker-DV2GNgle.js.map +0 -1
- package/dist/docker-Df_YOxxB.js +0 -2
- package/dist/doctor-DlKycx5H.js.map +0 -1
- package/dist/egress-BhzNh7Yi.js.map +0 -1
- package/dist/environment-1jE-HhO-.js +0 -657
- package/dist/environment-1jE-HhO-.js.map +0 -1
- package/dist/errors-C9w_ud7g.js.map +0 -1
- package/dist/errors-yDAEwmZo.js.map +0 -1
- package/dist/fake-instructions-CUI33I_F.js +0 -32
- package/dist/fake-instructions-CUI33I_F.js.map +0 -1
- package/dist/files-AiEKuEZe.js +0 -2
- package/dist/files-BM78_nnF.js +0 -2
- package/dist/files-BMCvfB4X.js +0 -2
- package/dist/harness-CeCp2GSW.js +0 -171
- package/dist/harness-CeCp2GSW.js.map +0 -1
- package/dist/images-BMgdpMAX.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/init-B6vegaP_.js +0 -2
- package/dist/init-BIdLF0F9.js.map +0 -1
- package/dist/jiti-BcJHCtrb.js +0 -46
- package/dist/jiti-BcJHCtrb.js.map +0 -1
- package/dist/live-S0qODb6o.js.map +0 -1
- package/dist/model-B1BZoZ9b.js.map +0 -1
- package/dist/model-BOnlAPzs.js.map +0 -1
- package/dist/model-DWeg5J7z.js +0 -2
- package/dist/model-FXko2kl7.js +0 -2
- package/dist/node-Co7EXge0.js.map +0 -1
- package/dist/opencode-providers-ByD85Q0l.js.map +0 -1
- package/dist/options-CKqyPGxX.js +0 -739
- package/dist/options-CKqyPGxX.js.map +0 -1
- package/dist/plan-DUydFc_1.js.map +0 -1
- package/dist/preflight-BkoSxf4C.js.map +0 -1
- package/dist/preflight-D5LW7Oqi.js +0 -2
- package/dist/prepare-CUAWHebF.js.map +0 -1
- package/dist/prepare-oji2oO8W.js +0 -3
- package/dist/provider-BDbNOQhQ.js +0 -2
- package/dist/providers-B1DzMvmH.js +0 -107
- package/dist/providers-B1DzMvmH.js.map +0 -1
- package/dist/providers-CpEfNU1F.js +0 -2
- package/dist/query-oPO_XrTy.js +0 -2
- package/dist/questions-B8by4Bbt.js +0 -234
- package/dist/questions-B8by4Bbt.js.map +0 -1
- package/dist/runtime-DdsfnRz_.js.map +0 -1
- package/dist/sandbox-D5v-of6d.js +0 -139
- package/dist/sandbox-D5v-of6d.js.map +0 -1
- package/dist/server-BNnXtmS8.js.map +0 -1
- package/dist/server-BNwoKzlm.js +0 -2
- package/dist/source-Bk_0ZaEY.js +0 -2
- package/dist/src-8rRXy4u9.js.map +0 -1
- package/dist/src-BGWuWp6I.js +0 -3
- package/dist/src-BRt0O02i.js +0 -3
- package/dist/src-Bx2x1ekD.js +0 -3
- package/dist/src-C_4Umgh2.js.map +0 -1
- package/dist/src-D95O-Smv.js.map +0 -1
- package/dist/src-DDNXsD9H.js.map +0 -1
- package/dist/src-DLa-ESW0.js +0 -4
- package/dist/src-cN0aB-WR.js +0 -3
- package/dist/src-cdIVjC5b.js +0 -2
- package/dist/src-sIsDHWYq.js +0 -3
- package/dist/state-Bo5zg4da.js +0 -2
- package/dist/state-CiXyLqg0.js.map +0 -1
- package/dist/studio-app/assets/index-B9MCTfuu.css +0 -2
- package/dist/summary-BTksrIxn.js.map +0 -1
- package/dist/tool-target-BU1JnSE9.js.map +0 -1
- package/dist/tools-B10Uj7OI.js.map +0 -1
- package/dist/views-Cs2Oqayd.js +0 -3
- package/dist/views-Dqa5ojfZ.js.map +0 -1
- package/dist/worker-entry-BAikHl9F.js.map +0 -1
- package/dist/workspace-Do_SiWBs.js +0 -2
package/dist/index.js
CHANGED
|
@@ -1,2139 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { t as
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { t as InfraError } from "./types-C5vI3g1C.js";
|
|
8
|
-
import { _ as layoutOf, a as unenforceable, h as expectedBlockedFor, n as allowedHosts, o as createFakeSandbox, p as brokeringFor, r as egressLabel, t as DEFAULT_EGRESS_ALLOW, v as refusedMcp } from "./egress-BhzNh7Yi.js";
|
|
9
|
-
import { c as relativeFile, i as evalSlug, l as sha256, n as attemptKey, r as evalId, s as recordFileName, t as attemptId } from "./ids-Bp6ut7ZD.js";
|
|
10
|
-
import { c as judgeModelName, d as isApiError, i as badJudgeList, u as runJudge } from "./judge-CeYPN8P_.js";
|
|
11
|
-
import { S as placeholder } from "./src-8rRXy4u9.js";
|
|
12
|
-
import { n as shellQuote, t as run } from "./process-CSVO_jsc.js";
|
|
13
|
-
import { a as evalPlan, d as sandboxId, f as sandboxSetBy, h as unbrokeredError, i as checkOptions, l as matchesTags, m as unbrokered, n as cascade, o as expand, p as secretVarsIn, s as harnessId, t as brokeredIn } from "./options-CKqyPGxX.js";
|
|
14
|
-
import { n as badCharacterAt, s as defineConfig } from "./config-CGvBeewH.js";
|
|
15
|
-
import { i as registerTest, n as getRunContext, o as SUBJECT, r as registerGroup, t as collectingFile, u as expect } from "./runtime-DdsfnRz_.js";
|
|
16
|
-
import { a as withDebug, i as debugTrace, n as attemptDebug, r as debugLog, t as AttemptDebug } from "./attempt-CGPV3BZY.js";
|
|
17
|
-
import { t as attemptDebugDir } from "./files-Ca0d6Uq3.js";
|
|
18
|
-
import { n as clip, r as formatDuration, t as capMiddle } from "./text-gEcKjAyp.js";
|
|
19
|
-
import { a as asCheck, c as snapshot, i as writeCaptureSidecars, n as serveCommand, o as createChecks, s as diffSinceSnapshot, t as capturePage } from "./capture-Bp74Z6Fr.js";
|
|
20
|
-
import { t as contextTaker } from "./context-DbSMEVKs.js";
|
|
21
|
-
import { t as PrepareError } from "./provider-CmAYTQQs.js";
|
|
22
|
-
import { i as preparedWorkspace } from "./workspace-C-TrNIUc.js";
|
|
23
|
-
import { n as judgeDiff } from "./patch-bDV8YFW0.js";
|
|
24
|
-
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
25
|
-
import { basename, isAbsolute, join, relative, sep } from "node:path";
|
|
26
|
-
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
27
|
-
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
28
|
-
import { parse } from "yaml";
|
|
29
|
-
//#region src/attempt/debug.ts
|
|
30
|
-
/** Which config and profile the attempt runs under, and where each resolved option came from. */
|
|
31
|
-
function tracePlan(debug, spec, ctx) {
|
|
32
|
-
const config = ctx.configFile ?? "none (no config file found)";
|
|
33
|
-
const from = (key, fallback = ctx.configFile ? `${config} or its default` : "default") => `(${spec.sources[key] ?? fallback})`;
|
|
34
|
-
const profile = ctx.profile ? `profile ${ctx.profile.name} (${ctx.profile.reason})` : "no profile";
|
|
35
|
-
debug.log("plan", `config ${config} · ${profile} · run ${ctx.runId} · attempt ${spec.cell.key}#${spec.n}`);
|
|
36
|
-
debug.log("plan", `harness ${harnessId(spec.harness)} ${from("harness")}`);
|
|
37
|
-
debug.log("plan", `sandbox ${sandboxId(spec.sandbox)} ${from("sandbox")}`);
|
|
38
|
-
const env = spec.environment;
|
|
39
|
-
const role = spec.cell.role === "variant" ? ` · variant ${spec.cell.variant}` : "";
|
|
40
|
-
const envFrom = spec.cell.role === "variant" ? from("compare") : from("environment", "none set");
|
|
41
|
-
debug.log("plan", `environment ${env.name}${role} ${envFrom}`);
|
|
42
|
-
debug.log("plan", `egress ${egressLabel(spec.egress) || "allow-all"} ${from("egress")}`);
|
|
43
|
-
const gate = typeof spec.gate === "string" ? spec.gate : `passRate ${spec.gate.passRate}`;
|
|
44
|
-
debug.log("plan", `run ${spec.n} · runs ${ctx.runs !== void 0 ? "(--runs)" : from("runs")} · gate ${gate} · timeout ${formatDuration(spec.timeoutMs)} ${from("timeout", "config.timeout or 15m")}`);
|
|
45
|
-
const judge = spec.judge;
|
|
46
|
-
const model = typeof judge.model === "string" ? judge.model : judge.model && "kind" in judge.model ? "fake judge" : judge.model ? "a model object" : "none";
|
|
47
|
-
debug.log("plan", `judge ${model}${judge.threshold !== void 0 ? ` · threshold ${judge.threshold}` : ""} ${from("judge")}`);
|
|
48
|
-
for (const note of spec.notes) debug.log("plan", `profile: ${note}`);
|
|
49
|
-
debug.trace("plan", "config", {
|
|
50
|
-
config: ctx.config,
|
|
51
|
-
profile: ctx.profile
|
|
52
|
-
}, "resolved config");
|
|
53
|
-
debug.trace("plan", "attempt", {
|
|
54
|
-
cell: spec.cell,
|
|
55
|
-
n: spec.n,
|
|
56
|
-
harness: spec.harness,
|
|
57
|
-
sandbox: spec.sandbox,
|
|
58
|
-
egress: spec.egress,
|
|
59
|
-
gate: spec.gate,
|
|
60
|
-
timeoutMs: spec.timeoutMs,
|
|
61
|
-
judge: spec.judge,
|
|
62
|
-
sources: spec.sources,
|
|
63
|
-
env: Object.keys(spec.env).sort()
|
|
64
|
-
}, "resolved attempt");
|
|
65
|
-
}
|
|
66
|
-
/** `POST api2.cursor.sh/auth/exchange`, `GET host/v1/*` or `POST host (path ~ /re/)`. */
|
|
67
|
-
function endpointLabel({ method, host, path }) {
|
|
68
|
-
const where = !path ? host : "exact" in path ? `${host}${path.exact}` : "startsWith" in path ? `${host}${path.startsWith}*` : `${host} (path ~ /${path.regex}/)`;
|
|
69
|
-
return `${method?.join("|") ?? "*"} ${where}`;
|
|
70
|
-
}
|
|
71
|
-
/** The resolved environment and what the gateway brokers, by name. */
|
|
72
|
-
function traceEnvironment(debug, spec, env) {
|
|
73
|
-
const r = env.record;
|
|
74
|
-
debug.log("env", `environment ${r.name} (digest ${r.digest.replace(/^sha256:/, "").slice(0, 12)})${r.parent ? ` · extends ${r.parent}` : ""}${r.overrides.length ? ` · overrides ${r.overrides.join(", ")}` : ""}`);
|
|
75
|
-
const unset = r.unset.length ? ` · unset (optional) ${r.unset.join(", ")}` : "";
|
|
76
|
-
debug.log("env", `vars ${r.vars.join(", ") || "none"} · secrets ${r.secrets.join(", ") || "none"}${unset}`);
|
|
77
|
-
for (const s of env.substitutions) debug.log("broker", `${s.name} is a placeholder in the sandbox · swapped in on HTTPS to ${s.hosts.join(", ")} in the ${s.in.join(", ")} · for ${s.phase === "checks" ? "checks and setup only" : "the agent and checks"}`);
|
|
78
|
-
const brokering = brokeringFor(spec.harness);
|
|
79
|
-
if (brokering) {
|
|
80
|
-
const endpoints = brokering.endpoints.map(endpointLabel).join(", ");
|
|
81
|
-
debug.log("broker", `${brokering.credential} stays on the host · the gateway swaps it in on ${endpoints}`);
|
|
82
|
-
}
|
|
83
|
-
debug.trace("env", "environment", {
|
|
84
|
-
record: r,
|
|
85
|
-
vars: Object.keys(env.vars).sort(),
|
|
86
|
-
checkVars: Object.keys(env.checkVars).sort(),
|
|
87
|
-
files: env.files.map((f) => f.path),
|
|
88
|
-
setup: env.setup,
|
|
89
|
-
skills: env.skills.map((s) => s.name),
|
|
90
|
-
workspaceSkills: env.workspaceSkills.map((s) => s.name),
|
|
91
|
-
mcp: Object.fromEntries(Object.entries(env.mcp).map(([name, m]) => [name, m.transport === "stdio" ? {
|
|
92
|
-
transport: m.transport,
|
|
93
|
-
command: m.command,
|
|
94
|
-
args: m.args,
|
|
95
|
-
env: Object.keys(m.env)
|
|
96
|
-
} : {
|
|
97
|
-
transport: m.transport,
|
|
98
|
-
url: m.url,
|
|
99
|
-
headers: Object.keys(m.headers)
|
|
100
|
-
}])),
|
|
101
|
-
brokered: env.substitutions.map(({ name, hosts, in: where, phase }) => ({
|
|
102
|
-
name,
|
|
103
|
-
hosts,
|
|
104
|
-
in: where,
|
|
105
|
-
phase
|
|
106
|
-
}))
|
|
107
|
-
}, "resolved environment");
|
|
108
|
-
}
|
|
109
|
-
//#endregion
|
|
110
|
-
//#region src/environment/resolve.ts
|
|
111
|
-
const SKILL_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
112
|
-
const DEFAULT_SETUP_TIMEOUT = "5m";
|
|
113
|
-
function broker(secrets, name, value, hosts, where = ["header"], phase = "agent") {
|
|
114
|
-
const at = badCharacterAt(value);
|
|
115
|
-
if (at && where.includes("header")) throw new InfraError(`${name} has whitespace or a line break in it (character ${at}), so it can't go in a request header. A key copied from a wrapped terminal line picks these up; set it again as one line.`);
|
|
116
|
-
const sub = {
|
|
117
|
-
name,
|
|
118
|
-
placeholder: placeholder(),
|
|
119
|
-
value,
|
|
120
|
-
hosts: [...hosts],
|
|
121
|
-
in: [...where],
|
|
122
|
-
phase
|
|
123
|
-
};
|
|
124
|
-
secrets.brokered.push(sub);
|
|
125
|
-
return sub.placeholder;
|
|
126
|
-
}
|
|
127
|
-
/** The host value, or its placeholder when it's brokered to `secret.hosts` (else `hosts`). */
|
|
128
|
-
function hostValue(name, secret, where, secrets, hosts = secret.hosts) {
|
|
129
|
-
const from = secret.from ?? name;
|
|
130
|
-
const value = process.env[from];
|
|
131
|
-
if (!value) {
|
|
132
|
-
if (secret.optional) {
|
|
133
|
-
secrets.unset.push(name);
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
const source = from === name ? name : `${name} (from ${from})`;
|
|
137
|
-
throw new InfraError(`Secret ${source} for ${where} isn't set. Set it in the environment or .env.local.`);
|
|
138
|
-
}
|
|
139
|
-
registerSecret(value);
|
|
140
|
-
if (secret.to === "checks") secrets.checks.push(name);
|
|
141
|
-
else secrets.names.push(name);
|
|
142
|
-
if (!hosts) return value;
|
|
143
|
-
return broker(secrets, name, value, hosts, secret.in, secret.to === "checks" ? "checks" : "agent");
|
|
144
|
-
}
|
|
145
|
-
/** Each host variable in the template, or with `hosts` one placeholder per variable. */
|
|
146
|
-
function template(value, where, secrets, hosts) {
|
|
147
|
-
const placeholders = /* @__PURE__ */ new Map();
|
|
148
|
-
return value.template.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_, name) => {
|
|
149
|
-
const v = process.env[name];
|
|
150
|
-
if (!v) throw new InfraError(`secret.template needs ${name} for ${where}, and it isn't set`);
|
|
151
|
-
registerSecret(v);
|
|
152
|
-
if (!hosts) return v;
|
|
153
|
-
const p = placeholders.get(name) ?? broker(secrets, name, v, hosts);
|
|
154
|
-
placeholders.set(name, p);
|
|
155
|
-
return p;
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
/** A `SKILL.md` split into its YAML frontmatter's `name` and `description` and its body. */
|
|
159
|
-
function parseSkillMd(text, where) {
|
|
160
|
-
const match = /^---\r?\n(?:([\s\S]*?)\r?\n)?---[ \t]*(?:\r?\n|$)([\s\S]*)$/.exec(text);
|
|
161
|
-
if (!match) return { body: text };
|
|
162
|
-
let front;
|
|
163
|
-
try {
|
|
164
|
-
front = parse(match[1] ?? "");
|
|
165
|
-
} catch (error) {
|
|
166
|
-
const reason = error.message.split("\n")[0];
|
|
167
|
-
throw new InfraError(`${where}: the SKILL.md frontmatter isn't valid YAML (${reason}). Quote values that contain ": ".`);
|
|
168
|
-
}
|
|
169
|
-
if (front != null && (typeof front !== "object" || Array.isArray(front))) throw new InfraError(`${where}: the SKILL.md frontmatter must be a YAML mapping`);
|
|
170
|
-
const field = (key) => {
|
|
171
|
-
const value = front?.[key];
|
|
172
|
-
if (value == null) return void 0;
|
|
173
|
-
if (typeof value !== "string") throw new InfraError(`${where}: the SKILL.md ${key} must be a string (got ${typeof value})`);
|
|
174
|
-
return value.trim() || void 0;
|
|
175
|
-
};
|
|
176
|
-
return {
|
|
177
|
-
name: field("name"),
|
|
178
|
-
description: field("description"),
|
|
179
|
-
body: match[2] ?? ""
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
/** A gitignore-style test for `DEFAULT_SKILL_EXCLUDE`-shaped patterns against a skill-relative path. */
|
|
183
|
-
function excludeMatcher(patterns) {
|
|
184
|
-
const rules = patterns.map((pattern) => {
|
|
185
|
-
const dirOnly = pattern.endsWith("/");
|
|
186
|
-
const body = pattern.replace(/\/+$/, "");
|
|
187
|
-
const anchored = body.includes("/");
|
|
188
|
-
const glob = body.replace(/^\/+/, "").split("*").map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join("[^/]*");
|
|
189
|
-
return {
|
|
190
|
-
dirOnly,
|
|
191
|
-
re: new RegExp(`${anchored ? "^" : "(?:^|/)"}${glob}$`)
|
|
192
|
-
};
|
|
193
|
-
});
|
|
194
|
-
return (path, isDir) => rules.some((r) => (isDir || !r.dirOnly) && r.re.test(path));
|
|
195
|
-
}
|
|
196
|
-
function parseSkill(dir, source, exclude) {
|
|
197
|
-
const front = parseSkillMd(readFileSync(join(dir, "SKILL.md"), "utf8"), `Skill ${basename(dir)} in ${source}`);
|
|
198
|
-
const name = front.name || basename(dir);
|
|
199
|
-
checkSkill(name, front.description, source);
|
|
200
|
-
const skip = excludeMatcher(exclude);
|
|
201
|
-
const paths = [];
|
|
202
|
-
const excluded = [];
|
|
203
|
-
const walk = (rel) => {
|
|
204
|
-
for (const entry of readdirSync(join(dir, rel), { withFileTypes: true })) {
|
|
205
|
-
const path = rel ? `${rel}/${entry.name}` : entry.name;
|
|
206
|
-
const isDir = entry.isDirectory();
|
|
207
|
-
if (entry.name === ".git") continue;
|
|
208
|
-
if (skip(path, isDir)) excluded.push(isDir ? `${path}/` : path);
|
|
209
|
-
else if (isDir) walk(path);
|
|
210
|
-
else if (entry.isFile() && path !== "SKILL.md") paths.push(path);
|
|
211
|
-
}
|
|
212
|
-
};
|
|
213
|
-
walk("");
|
|
214
|
-
const files = paths.sort().map((path) => ({
|
|
215
|
-
path,
|
|
216
|
-
content: readFileSync(join(dir, path), "utf8")
|
|
217
|
-
}));
|
|
218
|
-
return {
|
|
219
|
-
skill: {
|
|
220
|
-
name,
|
|
221
|
-
description: front.description ?? "",
|
|
222
|
-
content: front.body,
|
|
223
|
-
files
|
|
224
|
-
},
|
|
225
|
-
excluded: excluded.sort()
|
|
226
|
-
};
|
|
227
|
-
}
|
|
228
|
-
function checkSkill(name, description, source) {
|
|
229
|
-
if (!SKILL_NAME.test(name)) throw new InfraError(`Skill "${name}" in ${source}: the name must be kebab-case`);
|
|
230
|
-
if (!description) throw new InfraError(`Skill "${name}" in ${source} has no description in its SKILL.md frontmatter`);
|
|
231
|
-
}
|
|
232
|
-
function loadSkills(dir, source, only, exclude = DEFAULT_SKILL_EXCLUDE) {
|
|
233
|
-
if (!existsSync(dir) || !statSync(dir).isDirectory()) throw new InfraError(`Skill source ${source} isn't a directory`);
|
|
234
|
-
const dirs = existsSync(join(dir, "SKILL.md")) ? [dir] : readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && existsSync(join(dir, e.name, "SKILL.md"))).map((e) => join(dir, e.name)).sort();
|
|
235
|
-
if (dirs.length === 0) throw new InfraError(`Skill source ${source} has no SKILL.md (and no subdirectories with one)`);
|
|
236
|
-
const skills = dirs.map((d) => parseSkill(d, source, exclude));
|
|
237
|
-
if (!only) return skills;
|
|
238
|
-
const missing = only.filter((n) => !skills.some((s) => s.skill.name === n));
|
|
239
|
-
if (missing.length) throw new InfraError(`only: ${JSON.stringify(missing)} not found in ${source} (found: ${skills.map((s) => s.skill.name).join(", ")})`);
|
|
240
|
-
return skills.filter((s) => only.includes(s.skill.name));
|
|
241
|
-
}
|
|
242
|
-
async function git(args, cwd, what) {
|
|
243
|
-
const r = await run("git", args, { cwd });
|
|
244
|
-
if (r.exitCode !== 0) throw new InfraError(`${what} failed: ${r.stderr.trim()}`);
|
|
245
|
-
return r.stdout.trim();
|
|
246
|
-
}
|
|
247
|
-
/** One clone per source and ref per worker process; the commit is recorded. */
|
|
248
|
-
const clones = /* @__PURE__ */ new Map();
|
|
249
|
-
function cloneSkills(root, url, ref) {
|
|
250
|
-
const key = `${url}#${ref ?? ""}`;
|
|
251
|
-
let clone = clones.get(key);
|
|
252
|
-
if (!clone) {
|
|
253
|
-
clone = (async () => {
|
|
254
|
-
const dir = join(root, ".reeval", "cache", "skills", `${sha256(key).slice(0, 16)}-${process.pid}`);
|
|
255
|
-
await rm(dir, {
|
|
256
|
-
recursive: true,
|
|
257
|
-
force: true
|
|
258
|
-
});
|
|
259
|
-
await mkdir(dir, { recursive: true });
|
|
260
|
-
const what = `Cloning ${url}${ref ? ` at ${ref}` : ""}`;
|
|
261
|
-
await git(["init", "-q"], dir, what);
|
|
262
|
-
await git([
|
|
263
|
-
"fetch",
|
|
264
|
-
"-q",
|
|
265
|
-
"--depth",
|
|
266
|
-
"1",
|
|
267
|
-
url,
|
|
268
|
-
ref ?? "HEAD"
|
|
269
|
-
], dir, what);
|
|
270
|
-
await git([
|
|
271
|
-
"-c",
|
|
272
|
-
"advice.detachedHead=false",
|
|
273
|
-
"checkout",
|
|
274
|
-
"-q",
|
|
275
|
-
"FETCH_HEAD"
|
|
276
|
-
], dir, what);
|
|
277
|
-
return {
|
|
278
|
-
dir,
|
|
279
|
-
commit: await git(["rev-parse", "HEAD"], dir, what)
|
|
280
|
-
};
|
|
281
|
-
})();
|
|
282
|
-
clones.set(key, clone);
|
|
283
|
-
clone.catch(() => clones.delete(key));
|
|
284
|
-
}
|
|
285
|
-
return clone;
|
|
286
|
-
}
|
|
287
|
-
const packs = /* @__PURE__ */ new Map();
|
|
288
|
-
/** `npm pack` into the cache, unpacked; returns the package directory. */
|
|
289
|
-
function unpackNpm(root, spec) {
|
|
290
|
-
let pack = packs.get(spec);
|
|
291
|
-
if (!pack) {
|
|
292
|
-
pack = (async () => {
|
|
293
|
-
const dir = join(root, ".reeval", "cache", "skills", `npm-${sha256(spec).slice(0, 16)}-${process.pid}`);
|
|
294
|
-
await rm(dir, {
|
|
295
|
-
recursive: true,
|
|
296
|
-
force: true
|
|
297
|
-
});
|
|
298
|
-
await mkdir(dir, { recursive: true });
|
|
299
|
-
const packed = await run("npm", [
|
|
300
|
-
"pack",
|
|
301
|
-
spec,
|
|
302
|
-
"--silent",
|
|
303
|
-
"--pack-destination",
|
|
304
|
-
dir
|
|
305
|
-
], { cwd: dir });
|
|
306
|
-
const tarball = packed.stdout.trim().split("\n").at(-1);
|
|
307
|
-
if (packed.exitCode !== 0 || !tarball) throw new InfraError(`npm pack ${spec} failed: ${packed.stderr.trim()}`);
|
|
308
|
-
const untar = await run("tar", ["-xzf", tarball], { cwd: dir });
|
|
309
|
-
if (untar.exitCode !== 0) throw new InfraError(`Unpacking ${spec} failed: ${untar.stderr.trim()}`);
|
|
310
|
-
return join(dir, "package");
|
|
311
|
-
})();
|
|
312
|
-
packs.set(spec, pack);
|
|
313
|
-
pack.catch(() => packs.delete(spec));
|
|
314
|
-
}
|
|
315
|
-
return pack;
|
|
316
|
-
}
|
|
317
|
-
async function resolveSkills(root, s) {
|
|
318
|
-
switch (s.source) {
|
|
319
|
-
case "local": {
|
|
320
|
-
const label = `local:${relative(root, s.path).split(sep).join("/") || "."}`;
|
|
321
|
-
return {
|
|
322
|
-
skills: loadSkills(s.path, label, s.only, s.exclude),
|
|
323
|
-
source: label,
|
|
324
|
-
commit: null
|
|
325
|
-
};
|
|
326
|
-
}
|
|
327
|
-
case "git": {
|
|
328
|
-
const { url } = parseGitRepo(s.repo);
|
|
329
|
-
const { dir, commit } = await cloneSkills(root, url, s.ref);
|
|
330
|
-
const label = `${s.repo}${s.ref ? `#${s.ref}` : ""}${s.path ? `:${s.path}` : ""}`;
|
|
331
|
-
return {
|
|
332
|
-
skills: loadSkills(s.path ? join(dir, s.path) : dir, label, s.only, s.exclude),
|
|
333
|
-
source: label,
|
|
334
|
-
commit
|
|
335
|
-
};
|
|
336
|
-
}
|
|
337
|
-
case "registry": {
|
|
338
|
-
const [repo = "", name = ""] = s.id.split("@");
|
|
339
|
-
const { dir, commit } = await cloneSkills(root, `https://github.com/${repo}.git`, s.version);
|
|
340
|
-
const label = `registry:${s.id}${s.version ? `#${s.version}` : ""}`;
|
|
341
|
-
return {
|
|
342
|
-
skills: loadSkills(existsSync(join(dir, "skills")) ? join(dir, "skills") : dir, label, [name]),
|
|
343
|
-
source: label,
|
|
344
|
-
commit
|
|
345
|
-
};
|
|
346
|
-
}
|
|
347
|
-
case "npm": {
|
|
348
|
-
const dir = await unpackNpm(root, s.spec);
|
|
349
|
-
const label = `npm:${isAbsolute(s.spec) ? relative(root, s.spec).split(sep).join("/") : s.spec}:${s.path}`;
|
|
350
|
-
return {
|
|
351
|
-
skills: loadSkills(join(dir, s.path), label, s.only, s.exclude),
|
|
352
|
-
source: label,
|
|
353
|
-
commit: null
|
|
354
|
-
};
|
|
355
|
-
}
|
|
356
|
-
case "inline": {
|
|
357
|
-
checkSkill(s.name, s.description, "skill.inline");
|
|
358
|
-
const files = Object.entries(s.files).sort(([a], [b]) => a < b ? -1 : 1).map(([path, content]) => ({
|
|
359
|
-
path,
|
|
360
|
-
content
|
|
361
|
-
}));
|
|
362
|
-
return {
|
|
363
|
-
skills: [{
|
|
364
|
-
skill: {
|
|
365
|
-
name: s.name,
|
|
366
|
-
description: s.description,
|
|
367
|
-
content: s.content,
|
|
368
|
-
files
|
|
369
|
-
},
|
|
370
|
-
excluded: []
|
|
371
|
-
}],
|
|
372
|
-
source: "inline",
|
|
373
|
-
commit: null
|
|
374
|
-
};
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
const skillHash = (s) => sha256(JSON.stringify(s));
|
|
379
|
-
const readHostFile = (f, where) => {
|
|
380
|
-
if (!existsSync(f.path)) throw new InfraError(`${f.path} for ${where} doesn't exist`);
|
|
381
|
-
return readFileSync(f.path);
|
|
382
|
-
};
|
|
383
|
-
/**
|
|
384
|
-
* Layers the environment (root first; a later layer wins on shared keys, and each such key is
|
|
385
|
-
* an override), over the options `env` merged down the cascade. `where` names the attempt, and
|
|
386
|
-
* `rulesFile` is the workspace file the harness's agent reads `rules` from.
|
|
387
|
-
*/
|
|
388
|
-
async function resolveEnvironment(environment, optionsEnv, root, where, rulesFile, egress = "allow-all", prepared = () => void 0) {
|
|
389
|
-
const envWhere = `environment ${environment.name} (${where})`;
|
|
390
|
-
const overrides = /* @__PURE__ */ new Set();
|
|
391
|
-
const env = { ...optionsEnv };
|
|
392
|
-
const files = /* @__PURE__ */ new Map();
|
|
393
|
-
const mcp = /* @__PURE__ */ new Map();
|
|
394
|
-
const skills = /* @__PURE__ */ new Map();
|
|
395
|
-
let rules;
|
|
396
|
-
const setup = [];
|
|
397
|
-
const seen = {
|
|
398
|
-
env: /* @__PURE__ */ new Set(),
|
|
399
|
-
files: /* @__PURE__ */ new Set(),
|
|
400
|
-
mcp: /* @__PURE__ */ new Set()
|
|
401
|
-
};
|
|
402
|
-
let egressSet = false;
|
|
403
|
-
let workspace;
|
|
404
|
-
let sandboxSet = false;
|
|
405
|
-
for (const { spec } of environment.layers) {
|
|
406
|
-
if (spec.sandbox !== void 0) {
|
|
407
|
-
if (sandboxSet) overrides.add("sandbox");
|
|
408
|
-
sandboxSet = true;
|
|
409
|
-
}
|
|
410
|
-
if (spec.workspace !== void 0) {
|
|
411
|
-
if (workspace) overrides.add("workspace");
|
|
412
|
-
workspace = spec.workspace;
|
|
413
|
-
}
|
|
414
|
-
if (spec.egress !== void 0) {
|
|
415
|
-
if (egressSet) overrides.add("egress");
|
|
416
|
-
egressSet = true;
|
|
417
|
-
}
|
|
418
|
-
for (const [k, v] of Object.entries(spec.env ?? {})) {
|
|
419
|
-
if (seen.env.has(k)) overrides.add(`env.${k}`);
|
|
420
|
-
seen.env.add(k);
|
|
421
|
-
env[k] = v;
|
|
422
|
-
}
|
|
423
|
-
for (const [path, v] of Object.entries(spec.files ?? {})) {
|
|
424
|
-
if (seen.files.has(path)) overrides.add(`files.${path}`);
|
|
425
|
-
seen.files.add(path);
|
|
426
|
-
files.set(path, v);
|
|
427
|
-
}
|
|
428
|
-
if (spec.rules !== void 0) {
|
|
429
|
-
if (rules !== void 0) overrides.add("rules");
|
|
430
|
-
rules = spec.rules;
|
|
431
|
-
}
|
|
432
|
-
for (const [name, server] of Object.entries(spec.mcp ?? {})) {
|
|
433
|
-
if (seen.mcp.has(name)) overrides.add(`mcp.${name}`);
|
|
434
|
-
seen.mcp.add(name);
|
|
435
|
-
mcp.set(name, server);
|
|
436
|
-
}
|
|
437
|
-
for (const step of spec.setup ?? []) {
|
|
438
|
-
const s = typeof step === "string" ? { run: step } : step;
|
|
439
|
-
setup.push({
|
|
440
|
-
run: s.run,
|
|
441
|
-
timeoutMs: parseDuration(s.timeout ?? DEFAULT_SETUP_TIMEOUT),
|
|
442
|
-
...s.cwd ? { cwd: s.cwd } : {}
|
|
443
|
-
});
|
|
444
|
-
}
|
|
445
|
-
const layerSkills = /* @__PURE__ */ new Set();
|
|
446
|
-
for (const source of spec.skills ?? []) {
|
|
447
|
-
const found = await resolveSkills(root, source);
|
|
448
|
-
for (const loaded of found.skills) {
|
|
449
|
-
const name = loaded.skill.name;
|
|
450
|
-
if (skills.has(name) && !layerSkills.has(name)) overrides.add(`skills.${name}`);
|
|
451
|
-
layerSkills.add(name);
|
|
452
|
-
skills.delete(name);
|
|
453
|
-
skills.set(name, {
|
|
454
|
-
...loaded,
|
|
455
|
-
placement: source.placement,
|
|
456
|
-
source: found.source,
|
|
457
|
-
commit: found.commit
|
|
458
|
-
});
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
const secrets = {
|
|
463
|
-
names: [],
|
|
464
|
-
checks: [],
|
|
465
|
-
unset: [],
|
|
466
|
-
brokered: []
|
|
467
|
-
};
|
|
468
|
-
const vars = {};
|
|
469
|
-
const checkVars = {};
|
|
470
|
-
for (const [name, value] of Object.entries(env)) {
|
|
471
|
-
if (!isSecret(value)) {
|
|
472
|
-
vars[name] = value;
|
|
473
|
-
continue;
|
|
474
|
-
}
|
|
475
|
-
const resolved = hostValue(name, value, envWhere, secrets);
|
|
476
|
-
if (resolved === void 0) continue;
|
|
477
|
-
if (value.to === "checks") checkVars[name] = resolved;
|
|
478
|
-
else vars[name] = resolved;
|
|
479
|
-
}
|
|
480
|
-
const secretOf = (value, key, hosts) => {
|
|
481
|
-
if (typeof value === "string") return value;
|
|
482
|
-
if (isSecretTemplate(value)) return template(value, envWhere, secrets, hosts);
|
|
483
|
-
return hostValue(key, value, envWhere, secrets, value.hosts ?? hosts);
|
|
484
|
-
};
|
|
485
|
-
const resolvedMcp = {};
|
|
486
|
-
for (const [name, s] of mcp) if (s.transport === "stdio") {
|
|
487
|
-
const e = {};
|
|
488
|
-
for (const [k, v] of Object.entries(s.env)) {
|
|
489
|
-
const r = secretOf(v, k);
|
|
490
|
-
if (r !== void 0) e[k] = r;
|
|
491
|
-
}
|
|
492
|
-
resolvedMcp[name] = {
|
|
493
|
-
transport: "stdio",
|
|
494
|
-
command: s.command,
|
|
495
|
-
args: s.args,
|
|
496
|
-
env: e
|
|
497
|
-
};
|
|
498
|
-
} else {
|
|
499
|
-
const h = {};
|
|
500
|
-
const hosts = mcpHosts(s.url);
|
|
501
|
-
for (const [k, v] of Object.entries(s.headers)) {
|
|
502
|
-
const r = secretOf(v, k, hosts);
|
|
503
|
-
if (r !== void 0) h[k] = r;
|
|
504
|
-
}
|
|
505
|
-
resolvedMcp[name] = {
|
|
506
|
-
transport: s.transport,
|
|
507
|
-
url: s.url,
|
|
508
|
-
headers: h
|
|
509
|
-
};
|
|
510
|
-
}
|
|
511
|
-
const fileList = [...files].map(([path, v]) => ({
|
|
512
|
-
path,
|
|
513
|
-
content: isHostFile(v) ? readHostFile(v, envWhere) : v,
|
|
514
|
-
...isHostFile(v) && statSync(v.path).mode & 64 ? { executable: true } : {}
|
|
515
|
-
}));
|
|
516
|
-
const rulesContent = rules === void 0 ? void 0 : isHostFile(rules) ? Buffer.from(readHostFile(rules, envWhere)).toString("utf8") : rules;
|
|
517
|
-
const skillList = [...skills.values()];
|
|
518
|
-
const skillRecords = skillList.map(({ skill, placement, source, commit, excluded }) => ({
|
|
519
|
-
name: skill.name,
|
|
520
|
-
source,
|
|
521
|
-
commit,
|
|
522
|
-
sha256: skillHash(skill),
|
|
523
|
-
placement,
|
|
524
|
-
excluded
|
|
525
|
-
}));
|
|
526
|
-
const fileRecords = fileList.map((f) => ({
|
|
527
|
-
path: f.path,
|
|
528
|
-
sha256: sha256(f.content)
|
|
529
|
-
}));
|
|
530
|
-
const executable = fileList.flatMap((f) => f.executable ? [f.path] : []);
|
|
531
|
-
const rulesRecord = rulesContent === void 0 ? void 0 : {
|
|
532
|
-
path: rulesFile,
|
|
533
|
-
sha256: sha256(rulesContent)
|
|
534
|
-
};
|
|
535
|
-
if (rulesContent !== void 0) fileList.push({
|
|
536
|
-
path: rulesFile,
|
|
537
|
-
content: rulesContent
|
|
538
|
-
});
|
|
539
|
-
const secretNames = [.../* @__PURE__ */ new Set([...secrets.names, ...secrets.checks])].sort();
|
|
540
|
-
const brokered = secrets.brokered.map((b) => JSON.stringify([
|
|
541
|
-
b.name,
|
|
542
|
-
b.hosts,
|
|
543
|
-
b.in,
|
|
544
|
-
b.phase
|
|
545
|
-
])).sort();
|
|
546
|
-
const brokeredNames = [...new Set(secrets.brokered.map((b) => b.name))].sort();
|
|
547
|
-
const varNames = Object.keys(vars).filter((n) => !secrets.names.includes(n)).sort();
|
|
548
|
-
const literalVars = Object.fromEntries(varNames.map((n) => [n, vars[n]]));
|
|
549
|
-
const mcpShape = Object.fromEntries(Object.entries(resolvedMcp).map(([name, s]) => s.transport === "stdio" ? [name, {
|
|
550
|
-
command: s.command,
|
|
551
|
-
args: s.args,
|
|
552
|
-
env: Object.keys(s.env).sort()
|
|
553
|
-
}] : [name, {
|
|
554
|
-
transport: s.transport,
|
|
555
|
-
url: s.url,
|
|
556
|
-
headers: Object.keys(s.headers).sort()
|
|
557
|
-
}]));
|
|
558
|
-
const tree = workspace && prepared(workspace);
|
|
559
|
-
if (workspace && !tree) throw new InfraError(`The workspace git(${JSON.stringify(workspaceLabel(workspace))}) for ${envWhere} wasn't prepared before the run started`);
|
|
560
|
-
const workspaceRecord = workspace && tree ? {
|
|
561
|
-
source: isAbsolute(workspace.url) ? relative(root, workspace.url).split(sep).join("/") || "." : workspace.repo,
|
|
562
|
-
ref: workspace.ref ?? null,
|
|
563
|
-
path: workspace.path ?? null,
|
|
564
|
-
commit: tree.commit,
|
|
565
|
-
tree: tree.tree,
|
|
566
|
-
...workspace.init ? { init: true } : {}
|
|
567
|
-
} : void 0;
|
|
568
|
-
const digest = `sha256:${sha256(JSON.stringify({
|
|
569
|
-
setup,
|
|
570
|
-
skills: skillRecords.map((s) => [
|
|
571
|
-
s.name,
|
|
572
|
-
s.sha256,
|
|
573
|
-
s.placement
|
|
574
|
-
]),
|
|
575
|
-
mcp: mcpShape,
|
|
576
|
-
files: fileRecords,
|
|
577
|
-
...executable.length ? { executable } : {},
|
|
578
|
-
rules: rulesRecord?.sha256,
|
|
579
|
-
vars: literalVars,
|
|
580
|
-
secrets: secretNames,
|
|
581
|
-
checks: [...secrets.checks].sort(),
|
|
582
|
-
unset: [...secrets.unset].sort(),
|
|
583
|
-
brokered,
|
|
584
|
-
...egress === "allow-all" ? {} : { egress: typeof egress === "string" ? egress : [...egress.allow].sort() },
|
|
585
|
-
...workspaceRecord ? { workspace: workspaceRecord.init ? [workspaceRecord.tree, "init"] : workspaceRecord.tree } : {}
|
|
586
|
-
}))}`;
|
|
587
|
-
return {
|
|
588
|
-
record: {
|
|
589
|
-
name: environment.name,
|
|
590
|
-
parent: environment.parent,
|
|
591
|
-
overrides: [...overrides],
|
|
592
|
-
digest,
|
|
593
|
-
setup: setup.map(stepCommand),
|
|
594
|
-
skills: skillRecords,
|
|
595
|
-
mcp: Object.keys(resolvedMcp).sort(),
|
|
596
|
-
files: rulesRecord ? [...fileRecords, rulesRecord] : fileRecords,
|
|
597
|
-
vars: varNames,
|
|
598
|
-
secrets: secretNames,
|
|
599
|
-
brokered: brokeredNames,
|
|
600
|
-
unset: [...secrets.unset].sort(),
|
|
601
|
-
egress,
|
|
602
|
-
...workspaceRecord ? { workspace: workspaceRecord } : {}
|
|
603
|
-
},
|
|
604
|
-
setup,
|
|
605
|
-
skills: skillList.filter((s) => s.placement === "harness").map((s) => s.skill),
|
|
606
|
-
workspaceSkills: skillList.filter((s) => s.placement === "workspace").map((s) => s.skill),
|
|
607
|
-
mcp: resolvedMcp,
|
|
608
|
-
files: fileList,
|
|
609
|
-
vars,
|
|
610
|
-
checkVars,
|
|
611
|
-
substitutions: secrets.brokered,
|
|
612
|
-
...workspace && tree ? { workspace: {
|
|
613
|
-
tar: tree.tar,
|
|
614
|
-
label: workspaceLabel(workspace),
|
|
615
|
-
commit: tree.commit,
|
|
616
|
-
...workspace.init ? { init: true } : {}
|
|
617
|
-
} } : {}
|
|
618
|
-
};
|
|
619
|
-
}
|
|
620
|
-
/** A skill as a `SKILL.md` file plus its other files, for writing into a directory. */
|
|
621
|
-
function skillFiles(skill) {
|
|
622
|
-
return [{
|
|
623
|
-
path: "SKILL.md",
|
|
624
|
-
content: `${`---\nname: ${skill.name}\ndescription: ${JSON.stringify(skill.description)}\n---\n`}${skill.content}`
|
|
625
|
-
}, ...skill.files];
|
|
626
|
-
}
|
|
627
|
-
//#endregion
|
|
628
|
-
//#region src/harness/fake.ts
|
|
629
|
-
const lineCount = (text) => text ? text.split("\n").length - (text.endsWith("\n") ? 1 : 0) : 0;
|
|
630
|
-
/**
|
|
631
|
-
* Writes canned files and returns a canned message on every turn, after running its `script`'s
|
|
632
|
-
* real sandbox actions. Harness-placed skills are written under `~/` where its layout's adapter
|
|
633
|
-
* puts them (`~/.agents/skills` by default, as Cursor's does).
|
|
634
|
-
*/
|
|
635
|
-
const createFakeHarness = (descriptor) => {
|
|
636
|
-
let starts = 0;
|
|
637
|
-
return { async start(sandbox, _signal, { skills, workspaceSkills, mcp }) {
|
|
638
|
-
if (descriptor.failWith) throw new InfraError(descriptor.failWith);
|
|
639
|
-
const hangs = descriptor.startHangs;
|
|
640
|
-
if (hangs === true || starts++ < (hangs ?? 0)) return new Promise(() => {});
|
|
641
|
-
const dir = `~/${layoutOf(descriptor).harnessSkillDir}`;
|
|
642
|
-
for (const skill of skills) {
|
|
643
|
-
for (const f of skillFiles(skill)) await sandbox.writeFile(`${dir}/${skill.name}/${f.path}`, f.content);
|
|
644
|
-
debugLog("env", `skill ${skill.name} placed at ${dir}/${skill.name}/ by the fake agent`);
|
|
645
|
-
}
|
|
646
|
-
const names = [...skills.map((s) => s.name), ...workspaceSkills];
|
|
647
|
-
const loaded = [names.length ? `skills: ${names.join(", ")}` : "", Object.keys(mcp).length ? `mcp: ${Object.keys(mcp).join(", ")}` : ""].filter(Boolean);
|
|
648
|
-
let calls = 0;
|
|
649
|
-
let turns = 0;
|
|
650
|
-
const skilled = names.length > 0 && descriptor.withSkill;
|
|
651
|
-
const files = (skilled ? descriptor.withSkill?.files : void 0) ?? descriptor.files ?? {};
|
|
652
|
-
const reply = (skilled ? descriptor.withSkill?.text : void 0) ?? descriptor.text ?? "Done.";
|
|
653
|
-
const flaked = Math.random() < (descriptor.flakeRate ?? 0);
|
|
654
|
-
return {
|
|
655
|
-
async send(prompt, signal) {
|
|
656
|
-
const startedAt = Date.now();
|
|
657
|
-
const script = descriptor.script ?? [];
|
|
658
|
-
const steps = script[Math.min(turns++, script.length - 1)] ?? [];
|
|
659
|
-
const at = () => {
|
|
660
|
-
const ms = Date.now() - startedAt;
|
|
661
|
-
return {
|
|
662
|
-
ts: new Date(startedAt + ms).toISOString(),
|
|
663
|
-
atMs: ms
|
|
664
|
-
};
|
|
665
|
-
};
|
|
666
|
-
const transcript = [{
|
|
667
|
-
...at(),
|
|
668
|
-
type: "text",
|
|
669
|
-
text: `Working on: ${prompt}`,
|
|
670
|
-
durationMs: 0
|
|
671
|
-
}, ...loaded.length ? [{
|
|
672
|
-
...at(),
|
|
673
|
-
type: "text",
|
|
674
|
-
text: `Loaded ${loaded.join("; ")}`,
|
|
675
|
-
durationMs: 0
|
|
676
|
-
}] : []];
|
|
677
|
-
const done = (text, status) => ({
|
|
678
|
-
text,
|
|
679
|
-
status,
|
|
680
|
-
startedAt: new Date(startedAt).toISOString(),
|
|
681
|
-
durationMs: Date.now() - startedAt,
|
|
682
|
-
transcript,
|
|
683
|
-
usage: null,
|
|
684
|
-
model: descriptor.servedModel ?? null,
|
|
685
|
-
unreported: []
|
|
686
|
-
});
|
|
687
|
-
const write = async (path, content) => {
|
|
688
|
-
const time = at();
|
|
689
|
-
const before = await sandbox.readFile(path);
|
|
690
|
-
await sandbox.writeFile(path, content);
|
|
691
|
-
debugLog("harness", `tool write ${path} (${before === null ? "create" : "modify"})`);
|
|
692
|
-
transcript.push({
|
|
693
|
-
...time,
|
|
694
|
-
type: "file-edit",
|
|
695
|
-
toolCallId: `call_${++calls}`,
|
|
696
|
-
path,
|
|
697
|
-
kind: before === null ? "create" : "modify",
|
|
698
|
-
added: lineCount(content),
|
|
699
|
-
removed: before === null ? 0 : lineCount(before),
|
|
700
|
-
durationMs: Date.now() - startedAt - time.atMs
|
|
701
|
-
});
|
|
702
|
-
};
|
|
703
|
-
if (descriptor.reasoning) transcript.push({
|
|
704
|
-
...at(),
|
|
705
|
-
type: "reasoning",
|
|
706
|
-
text: descriptor.reasoning,
|
|
707
|
-
durationMs: 0
|
|
708
|
-
});
|
|
709
|
-
for (const tool of descriptor.tools ?? []) transcript.push({
|
|
710
|
-
...at(),
|
|
711
|
-
type: "tool",
|
|
712
|
-
toolCallId: `call_${++calls}`,
|
|
713
|
-
tool: tool.tool,
|
|
714
|
-
input: tool.input,
|
|
715
|
-
output: tool.output,
|
|
716
|
-
ok: tool.ok ?? true,
|
|
717
|
-
durationMs: 0
|
|
718
|
-
});
|
|
719
|
-
if (flaked) {
|
|
720
|
-
const text = "I ran out of ideas and left the workspace unchanged.";
|
|
721
|
-
transcript.push({
|
|
722
|
-
...at(),
|
|
723
|
-
type: "text",
|
|
724
|
-
text,
|
|
725
|
-
durationMs: 0
|
|
726
|
-
});
|
|
727
|
-
return done(text, "completed");
|
|
728
|
-
}
|
|
729
|
-
let scripted;
|
|
730
|
-
for (const step of steps) if ("exec" in step) {
|
|
731
|
-
const time = at();
|
|
732
|
-
debugLog("harness", `tool shell called: $ ${step.exec}`);
|
|
733
|
-
const r = await sandbox.exec(step.exec, { signal });
|
|
734
|
-
debugLog("harness", `tool shell returned exit ${r.exitCode} in ${Date.now() - startedAt - time.atMs}ms`);
|
|
735
|
-
transcript.push({
|
|
736
|
-
...time,
|
|
737
|
-
type: "tool",
|
|
738
|
-
toolCallId: `call_${++calls}`,
|
|
739
|
-
tool: "shell",
|
|
740
|
-
input: { command: step.exec },
|
|
741
|
-
output: r.stdout,
|
|
742
|
-
ok: r.exitCode === 0,
|
|
743
|
-
durationMs: Date.now() - startedAt - time.atMs
|
|
744
|
-
});
|
|
745
|
-
} else if ("write" in step) for (const [path, content] of Object.entries(step.write)) await write(path, content);
|
|
746
|
-
else if ("mcp" in step) {
|
|
747
|
-
const time = at();
|
|
748
|
-
const [server = "", tool = ""] = step.mcp.split("/");
|
|
749
|
-
debugLog("harness", `tool mcp__${server}__${tool} called`);
|
|
750
|
-
const r = await callMcpTool(sandbox, mcp, server, tool, signal);
|
|
751
|
-
debugLog("harness", `tool mcp__${server}__${tool} ${r.ok ? "returned" : "failed"}`);
|
|
752
|
-
transcript.push({
|
|
753
|
-
...time,
|
|
754
|
-
type: "tool",
|
|
755
|
-
toolCallId: `call_${++calls}`,
|
|
756
|
-
tool: `mcp__${server}__${tool}`,
|
|
757
|
-
input: {},
|
|
758
|
-
output: r.output,
|
|
759
|
-
ok: r.ok,
|
|
760
|
-
durationMs: Date.now() - startedAt - time.atMs
|
|
761
|
-
});
|
|
762
|
-
} else if ("error" in step) {
|
|
763
|
-
debugLog("harness", `turn failed: ${step.error}`);
|
|
764
|
-
transcript.push({
|
|
765
|
-
...at(),
|
|
766
|
-
type: "error",
|
|
767
|
-
message: step.error
|
|
768
|
-
});
|
|
769
|
-
return done("", "error");
|
|
770
|
-
} else scripted = step.reply;
|
|
771
|
-
if (descriptor.delayMs) try {
|
|
772
|
-
await setTimeout$1(descriptor.delayMs, void 0, descriptor.ignoreAbort ? {} : { signal });
|
|
773
|
-
} catch {
|
|
774
|
-
return done("", "aborted");
|
|
775
|
-
}
|
|
776
|
-
for (const [path, content] of Object.entries(files)) await write(path, content);
|
|
777
|
-
const text = scripted ?? reply;
|
|
778
|
-
transcript.push({
|
|
779
|
-
...at(),
|
|
780
|
-
type: "text",
|
|
781
|
-
text,
|
|
782
|
-
durationMs: 0
|
|
783
|
-
});
|
|
784
|
-
return done(text, "completed");
|
|
785
|
-
},
|
|
786
|
-
async close() {}
|
|
787
|
-
};
|
|
788
|
-
} };
|
|
789
|
-
};
|
|
790
|
-
/**
|
|
791
|
-
* Starts a stdio MCP server in the sandbox, as an agent would, and calls one tool with no
|
|
792
|
-
* arguments: initialize, then the call.
|
|
793
|
-
*/
|
|
794
|
-
async function callMcpTool(sandbox, servers, name, tool, signal) {
|
|
795
|
-
const server = servers[name];
|
|
796
|
-
if (!server) {
|
|
797
|
-
const known = Object.keys(servers);
|
|
798
|
-
throw new InfraError(`The fake agent's script calls MCP server "${name}", which the environment doesn't have (it has ${known.length ? known.join(", ") : "none"})`);
|
|
799
|
-
}
|
|
800
|
-
if (server.transport !== "stdio") throw new InfraError(`The fake agent calls stdio MCP servers only; "${name}" is ${server.transport}`);
|
|
801
|
-
const messages = [
|
|
802
|
-
{
|
|
803
|
-
jsonrpc: "2.0",
|
|
804
|
-
id: 1,
|
|
805
|
-
method: "initialize",
|
|
806
|
-
params: {
|
|
807
|
-
protocolVersion: "2024-11-05",
|
|
808
|
-
capabilities: {},
|
|
809
|
-
clientInfo: {
|
|
810
|
-
name: "reeval-fake-agent",
|
|
811
|
-
version: "1"
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
},
|
|
815
|
-
{
|
|
816
|
-
jsonrpc: "2.0",
|
|
817
|
-
method: "notifications/initialized"
|
|
818
|
-
},
|
|
819
|
-
{
|
|
820
|
-
jsonrpc: "2.0",
|
|
821
|
-
id: 2,
|
|
822
|
-
method: "tools/call",
|
|
823
|
-
params: {
|
|
824
|
-
name: tool,
|
|
825
|
-
arguments: {}
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
].map((m) => shellQuote(JSON.stringify(m)));
|
|
829
|
-
const command = [server.command, ...server.args].map(shellQuote).join(" ");
|
|
830
|
-
const script = [
|
|
831
|
-
"d=$(mktemp -d) && : > \"$d/out\" && : > \"$d/err\" || exit 1",
|
|
832
|
-
"{",
|
|
833
|
-
" trap '' PIPE",
|
|
834
|
-
` printf '%s\\n' ${messages.join(" ")} 2>/dev/null`,
|
|
835
|
-
" i=0",
|
|
836
|
-
` while [ $i -lt 300 ] && [ ! -f "$d/code" ] && ! grep -Eq '"id": ?2[,}]' "$d/out"; do sleep 0.1; i=$((i + 1)); done`,
|
|
837
|
-
`} | { ${command} > "$d/out" 2> "$d/err"; echo $? > "$d/code"; }`,
|
|
838
|
-
"code=$(cat \"$d/code\")",
|
|
839
|
-
"cat \"$d/out\"; cat \"$d/err\" >&2; rm -rf \"$d\"; exit $code"
|
|
840
|
-
].join("\n");
|
|
841
|
-
const r = await sandbox.exec(script, {
|
|
842
|
-
signal,
|
|
843
|
-
env: server.env
|
|
844
|
-
});
|
|
845
|
-
const reply = r.stdout.split("\n").flatMap((line) => {
|
|
846
|
-
try {
|
|
847
|
-
return [JSON.parse(line)];
|
|
848
|
-
} catch {
|
|
849
|
-
return [];
|
|
850
|
-
}
|
|
851
|
-
}).find((m) => m.id === 2);
|
|
852
|
-
if (!reply) return {
|
|
853
|
-
output: r.stderr.trim() || `no reply (exit ${r.exitCode})`,
|
|
854
|
-
ok: false
|
|
855
|
-
};
|
|
856
|
-
if (reply.error) return {
|
|
857
|
-
output: reply.error.message,
|
|
858
|
-
ok: false
|
|
859
|
-
};
|
|
860
|
-
return {
|
|
861
|
-
output: (reply.result?.content ?? []).flatMap((c) => c.type === "text" ? [c.text] : []).join("\n"),
|
|
862
|
-
ok: !reply.result?.isError
|
|
863
|
-
};
|
|
864
|
-
}
|
|
865
|
-
//#endregion
|
|
866
|
-
//#region src/harness/index.ts
|
|
867
|
-
async function createHarness(descriptor) {
|
|
868
|
-
if (descriptor.kind === "fake") return createFakeHarness(descriptor);
|
|
869
|
-
const { createAgentHarness } = await import("./agent-WSmTaesf.js");
|
|
870
|
-
return createAgentHarness(descriptor);
|
|
871
|
-
}
|
|
872
|
-
/** Where each harness's agent looks for repo skills, relative to the workspace. */
|
|
873
|
-
const workspaceSkillDir = (descriptor) => layoutOf(descriptor).workspaceSkillDir;
|
|
874
|
-
//#endregion
|
|
875
|
-
//#region src/attempt/environment.ts
|
|
876
|
-
/**
|
|
877
|
-
* Resolves the attempt's environment (files, skills, secrets and their placeholders)
|
|
878
|
-
* and finds the image a Docker sandbox starts from, which preflight prepared. Runs on the test's
|
|
879
|
-
* signal, before the eval timer starts.
|
|
880
|
-
*/
|
|
881
|
-
async function prepareEnvironment(a, root, runId, where, signal) {
|
|
882
|
-
const { spec, record } = a;
|
|
883
|
-
const env = await resolveEnvironment(spec.environment, spec.env, root, where, layoutOf(spec.harness).rulesFile, spec.egress.policy, (w) => preparedWorkspace(root, runId, w));
|
|
884
|
-
a.env = env;
|
|
885
|
-
record.environment = env.record;
|
|
886
|
-
const { secrets, brokered } = env.record;
|
|
887
|
-
record.env = {
|
|
888
|
-
vars: Object.keys(env.vars).filter((n) => !secrets.includes(n)).sort(),
|
|
889
|
-
secrets,
|
|
890
|
-
brokered
|
|
891
|
-
};
|
|
892
|
-
const debug = attemptDebug();
|
|
893
|
-
if (debug) traceEnvironment(debug, spec, env);
|
|
894
|
-
record.image = null;
|
|
895
|
-
if (spec.sandbox.kind === "docker") {
|
|
896
|
-
const { preparedFor } = await import("./preflight-D5LW7Oqi.js");
|
|
897
|
-
const { dockerProvider } = await import("./providers-CpEfNU1F.js");
|
|
898
|
-
signal.throwIfAborted();
|
|
899
|
-
const prepared = preparedFor(root, runId, dockerProvider(), spec.sandbox, spec.harness);
|
|
900
|
-
if (!prepared) throw new InfraError(`The sandbox image for ${where} wasn't prepared before the run started`);
|
|
901
|
-
record.image = prepared;
|
|
902
|
-
a.image = record.image.ref;
|
|
903
|
-
const { ref, id, base, install, agentCli } = record.image;
|
|
904
|
-
debugLog("image", `ready ${ref} (${id.replace(/^sha256:/, "").slice(0, 12)}) · prepared before the run${ref !== base ? ` · on ${base}` : ""}${agentCli ? ` · agent CLI ${agentCli}` : ""}${install.length ? ` · install: ${install.join(" && ")}` : ""}`);
|
|
905
|
-
} else debugLog("image", `none: the ${spec.sandbox.kind} sandbox runs on the host`);
|
|
906
|
-
}
|
|
907
|
-
//#endregion
|
|
908
|
-
//#region src/judge/artifacts.ts
|
|
909
|
-
/** A capture handle: a name plus the file the attempt wrote. */
|
|
910
|
-
function isCaptureHandle(value) {
|
|
911
|
-
if (typeof value !== "object" || value === null) return false;
|
|
912
|
-
const v = value;
|
|
913
|
-
return typeof v.name === "string" && typeof v.file?.path === "string";
|
|
914
|
-
}
|
|
915
|
-
/** The first `artifacts` value that isn't a list of capture handles or names. */
|
|
916
|
-
function badJudgeArtifacts(artifacts) {
|
|
917
|
-
if (artifacts === void 0) return void 0;
|
|
918
|
-
if (!Array.isArray(artifacts)) return `must be a list of capture handles or names (got ${JSON.stringify(artifacts)})`;
|
|
919
|
-
for (const item of artifacts) {
|
|
920
|
-
if (typeof item === "string" && item.trim()) continue;
|
|
921
|
-
if (isCaptureHandle(item)) continue;
|
|
922
|
-
return `must be a list of capture handles or names (got ${JSON.stringify(item)})`;
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
const nameOf = (item) => typeof item === "string" ? item.trim() : item.name;
|
|
926
|
-
/**
|
|
927
|
-
* Resolves call `artifacts` (a capture handle or its name) to PNG bytes this attempt captured.
|
|
928
|
-
* Unknown names throw: a mistake in the call, before the provider is asked.
|
|
929
|
-
*/
|
|
930
|
-
function resolveJudgeArtifacts(artifacts, blobs) {
|
|
931
|
-
if (!artifacts?.length) return [];
|
|
932
|
-
return artifacts.map((item) => {
|
|
933
|
-
const name = nameOf(item);
|
|
934
|
-
const data = blobs.get(name);
|
|
935
|
-
if (!data) throw new Error(`c.judge() artifacts names a capture this attempt doesn't have: ${JSON.stringify(name)}`);
|
|
936
|
-
return {
|
|
937
|
-
name,
|
|
938
|
-
path: `artifacts/${name}.png`,
|
|
939
|
-
sha256: sha256(data),
|
|
940
|
-
bytes: data.byteLength,
|
|
941
|
-
mediaType: "image/png",
|
|
942
|
-
data
|
|
943
|
-
};
|
|
944
|
-
});
|
|
945
|
-
}
|
|
946
|
-
const filePartsOf = (resolved) => resolved.map((a) => ({
|
|
947
|
-
type: "file",
|
|
948
|
-
mediaType: a.mediaType,
|
|
949
|
-
data: a.data
|
|
950
|
-
}));
|
|
951
|
-
const artifactRefsOf = (resolved) => resolved.map(({ data: _data, ...ref }) => ref);
|
|
952
|
-
/**
|
|
953
|
-
* JSON-only state stays an object so TypeSafe and other JSON judges are unchanged.
|
|
954
|
-
* File parts are an ordered decide-state array: one JSON part, then each capture.
|
|
955
|
-
*/
|
|
956
|
-
function decideState(json, files) {
|
|
957
|
-
if (files.length === 0) return json;
|
|
958
|
-
return [{
|
|
959
|
-
type: "json",
|
|
960
|
-
value: json
|
|
961
|
-
}, ...files];
|
|
962
|
-
}
|
|
963
|
-
//#endregion
|
|
964
|
-
//#region src/judge/size.ts
|
|
965
|
-
/** Roughly four characters a token, as `reeval analyze` counts: an estimate, not the model's. */
|
|
966
|
-
const CHARS_PER_TOKEN = 4;
|
|
967
|
-
const tokensOf = (chars) => Math.ceil(chars / CHARS_PER_TOKEN);
|
|
968
|
-
const k = (tokens) => tokens < 1e3 ? String(tokens) : `${Math.round(tokens / 1e3)}k`;
|
|
969
|
-
const diffItems = (patch) => patch.split(/(?=^diff --git )/m).map((part) => ({
|
|
970
|
-
name: /^diff --git a\/.+ b\/(.+)$/m.exec(part)?.[1] ?? "",
|
|
971
|
-
chars: part.length
|
|
972
|
-
})).filter((item) => item.name);
|
|
973
|
-
/** The state's parts by size, each with its biggest files where it has any. */
|
|
974
|
-
function partsOf(state, question) {
|
|
975
|
-
return [...Object.entries(state).map(([name, value]) => {
|
|
976
|
-
const text = typeof value === "string" ? value : JSON.stringify(value);
|
|
977
|
-
const items = name === "diff" && typeof value === "string" ? diffItems(value) : value && typeof value === "object" && !Array.isArray(value) ? Object.entries(value).map(([file, content]) => ({
|
|
978
|
-
name: file,
|
|
979
|
-
chars: typeof content === "string" ? content.length : JSON.stringify(content).length
|
|
980
|
-
})) : [];
|
|
981
|
-
return {
|
|
982
|
-
name,
|
|
983
|
-
chars: text?.length ?? 0,
|
|
984
|
-
items
|
|
985
|
-
};
|
|
986
|
-
}), {
|
|
987
|
-
name: "question",
|
|
988
|
-
chars: question.length,
|
|
989
|
-
items: []
|
|
990
|
-
}].sort((a, b) => b.chars - a.chars);
|
|
991
|
-
}
|
|
992
|
-
/** The estimated size of what the judge reads, in tokens. */
|
|
993
|
-
const inputTokens = (state, question) => tokensOf(JSON.stringify(state).length + question.length);
|
|
994
|
-
/**
|
|
995
|
-
* `diff 306k, of which dist/assets/index-….js is 296k; output 1k; …`: the state's parts by
|
|
996
|
-
* estimated tokens, naming the biggest files in each.
|
|
997
|
-
*/
|
|
998
|
-
function describeSize(state, question) {
|
|
999
|
-
const parts = partsOf(state, question);
|
|
1000
|
-
const big = parts.filter((p) => tokensOf(p.chars) >= 1e3);
|
|
1001
|
-
return (big.length ? big : parts.slice(0, 1)).map((p) => {
|
|
1002
|
-
const top = p.items.sort((a, b) => b.chars - a.chars).slice(0, 3).filter((item) => tokensOf(item.chars) >= 1e3).map((item) => `${item.name} is ${k(tokensOf(item.chars))}`);
|
|
1003
|
-
return `${p.name} ${k(tokensOf(p.chars))}${top.length ? `, of which ${top.join(", ")}` : ""}`;
|
|
1004
|
-
}).join("; ");
|
|
1005
|
-
}
|
|
1006
|
-
const HINT = "Narrow what the judge sees with include, e.g. c.judge(question, { include: ['src/'] }), or set judge: { include } in the config; files shows chosen files in full.";
|
|
1007
|
-
/**
|
|
1008
|
-
* A provider's error, as the provider worded it, then what the judge's input held: its estimated
|
|
1009
|
-
* size and biggest parts and files, and how to narrow it.
|
|
1010
|
-
*/
|
|
1011
|
-
const withInputSize = (message, state, question) => `${message}\nThe judge's input was ~${k(inputTokens(state, question))} tokens (estimated): ${describeSize(state, question)}. ${HINT}`;
|
|
1012
|
-
//#endregion
|
|
1013
|
-
//#region src/attempt/judge.ts
|
|
1014
|
-
const noJudgeModel = (configFile) => `c.judge() has no model. Set judge: { model: 'typesafe-ai/jev-1.13.0' } in ${configFile}, on the group or eval, or pass { model } to this call.`;
|
|
1015
|
-
const isTurn = (value) => typeof value === "object" && value !== null && "transcript" in value && "diff" in value;
|
|
1016
|
-
const workspacePath = (path) => path.replace(/^\.\//, "");
|
|
1017
|
-
/**
|
|
1018
|
-
* The turn's task and output, its changed files and diff as `judgeDiff` shows them, and the whole
|
|
1019
|
-
* current content of each of `files` (null when there's no such file).
|
|
1020
|
-
*/
|
|
1021
|
-
async function judgeState(turn, output, { include, files }, readFile) {
|
|
1022
|
-
const inFull = files?.length ? files : void 0;
|
|
1023
|
-
const shown = judgeDiff(turn.diff, {
|
|
1024
|
-
...include ? { include } : {},
|
|
1025
|
-
...inFull ? { inFull: new Set(inFull.map(workspacePath)) } : {}
|
|
1026
|
-
});
|
|
1027
|
-
const state = {
|
|
1028
|
-
task: turn.prompt,
|
|
1029
|
-
output,
|
|
1030
|
-
files: shown.files,
|
|
1031
|
-
diff: shown.patch
|
|
1032
|
-
};
|
|
1033
|
-
if (inFull) {
|
|
1034
|
-
const contents = {};
|
|
1035
|
-
for (const path of inFull) contents[workspacePath(path)] = await readFile(path);
|
|
1036
|
-
state.contents = contents;
|
|
1037
|
-
}
|
|
1038
|
-
return {
|
|
1039
|
-
state,
|
|
1040
|
-
dropped: shown.dropped
|
|
1041
|
-
};
|
|
1042
|
-
}
|
|
1043
|
-
/**
|
|
1044
|
-
* Grades a turn (the last by default) or a value against a question, under
|
|
1045
|
-
* the judge provider's concurrency lease. Every call's input is recorded, including calls that
|
|
1046
|
-
* fail. A call that fails returns a `JudgeFailure` and fails the attempt as a judge error after the eval's body, so the judges and
|
|
1047
|
-
* checks after it still run.
|
|
1048
|
-
*/
|
|
1049
|
-
function judgeCaller(a, readFile) {
|
|
1050
|
-
const ask = async (question, { on, ...call } = {}) => {
|
|
1051
|
-
if (typeof question !== "string" || !question.trim()) throw new Error(`c.judge() needs a question: a non-empty string (got ${JSON.stringify(question)})`);
|
|
1052
|
-
const { threshold } = call;
|
|
1053
|
-
if (threshold !== void 0 && !(typeof threshold === "number" && threshold >= 0 && threshold <= 1)) throw new Error(`c.judge() threshold must be between 0 and 1 (got ${String(threshold)})`);
|
|
1054
|
-
const bad = badJudgeList(call);
|
|
1055
|
-
if (bad) throw new Error(`c.judge() ${bad.key} must be a list of non-empty strings (got ${bad.got})`);
|
|
1056
|
-
const badArts = badJudgeArtifacts(call.artifacts);
|
|
1057
|
-
if (badArts) throw new Error(`c.judge() artifacts ${badArts}`);
|
|
1058
|
-
const subject = isTurn(on) ? on : a.turns.at(-1);
|
|
1059
|
-
if (!subject) throw new Error("c.judge() has no turn to grade: call c.send() first");
|
|
1060
|
-
const options = {
|
|
1061
|
-
...a.spec.judge,
|
|
1062
|
-
...call
|
|
1063
|
-
};
|
|
1064
|
-
const { model } = options;
|
|
1065
|
-
if (model === void 0) {
|
|
1066
|
-
a.judgeErrored = true;
|
|
1067
|
-
throw new Error(noJudgeModel(getRunContext().config.file));
|
|
1068
|
-
}
|
|
1069
|
-
const { include, files, maxConcurrent, ...judgeOptions } = options;
|
|
1070
|
-
const { state, dropped } = await judgeState(subject, on === void 0 || isTurn(on) ? subject.text : on, {
|
|
1071
|
-
include,
|
|
1072
|
-
files
|
|
1073
|
-
}, readFile);
|
|
1074
|
-
const resolved = resolveJudgeArtifacts(call.artifacts, a.artifactBlobs);
|
|
1075
|
-
const refs = artifactRefsOf(resolved);
|
|
1076
|
-
const decide = decideState(state, filePartsOf(resolved));
|
|
1077
|
-
const name = judgeModelName(model);
|
|
1078
|
-
const input = {
|
|
1079
|
-
question,
|
|
1080
|
-
model: name,
|
|
1081
|
-
state,
|
|
1082
|
-
...refs.length ? { artifacts: refs } : {},
|
|
1083
|
-
...dropped.length ? { dropped } : {}
|
|
1084
|
-
};
|
|
1085
|
-
a.record.judgeInputs?.push(input);
|
|
1086
|
-
const start = performance.now();
|
|
1087
|
-
const ms = () => Math.round(performance.now() - start);
|
|
1088
|
-
const atLeast = options.threshold ?? .8;
|
|
1089
|
-
a.report({
|
|
1090
|
-
kind: "judge-start",
|
|
1091
|
-
question
|
|
1092
|
-
});
|
|
1093
|
-
debugLog("judge", `asking ${name}: ${JSON.stringify(clip(question, 160))}`);
|
|
1094
|
-
if (dropped.length) debugLog("judge", `left out of the judge's input: ${dropped.join(" · ")}`);
|
|
1095
|
-
debugTrace("judge", "prompt", input, "judge prompt");
|
|
1096
|
-
const failed = (message, retries) => {
|
|
1097
|
-
a.judgeErrored = true;
|
|
1098
|
-
a.judgeErrors.push(`c.judge(${JSON.stringify(clip(question, 80))}) failed: ${message}`);
|
|
1099
|
-
input.error = message;
|
|
1100
|
-
if (retries?.length) input.retries = retries;
|
|
1101
|
-
debugLog("judge", `${name} · ${formatDuration(ms())} · error: ${message}`);
|
|
1102
|
-
debugTrace("judge", "error", {
|
|
1103
|
-
error: message,
|
|
1104
|
-
retries
|
|
1105
|
-
}, "judge error");
|
|
1106
|
-
a.report({
|
|
1107
|
-
kind: "judge-end",
|
|
1108
|
-
question,
|
|
1109
|
-
verdict: "error",
|
|
1110
|
-
score: null,
|
|
1111
|
-
threshold: null,
|
|
1112
|
-
ms: ms()
|
|
1113
|
-
});
|
|
1114
|
-
return {
|
|
1115
|
-
verdict: "error",
|
|
1116
|
-
score: null,
|
|
1117
|
-
threshold: atLeast,
|
|
1118
|
-
question,
|
|
1119
|
-
model: name,
|
|
1120
|
-
error: message,
|
|
1121
|
-
...refs.length ? { artifacts: refs } : {}
|
|
1122
|
-
};
|
|
1123
|
-
};
|
|
1124
|
-
const provider = name.split("/")[0];
|
|
1125
|
-
try {
|
|
1126
|
-
const { verdict, retries } = await a.timed("judgeMs", () => withLease(maxConcurrent ? { [`judge:${provider}`]: maxConcurrent } : {}, () => runJudge(question, decide, {
|
|
1127
|
-
...judgeOptions,
|
|
1128
|
-
model,
|
|
1129
|
-
threshold: atLeast
|
|
1130
|
-
}, a.signal), a.signal));
|
|
1131
|
-
const recorded = refs.length ? {
|
|
1132
|
-
...verdict,
|
|
1133
|
-
artifacts: refs
|
|
1134
|
-
} : verdict;
|
|
1135
|
-
a.record.judgements.push(retries.length ? {
|
|
1136
|
-
...recorded,
|
|
1137
|
-
retries
|
|
1138
|
-
} : recorded);
|
|
1139
|
-
const { score } = verdict;
|
|
1140
|
-
debugLog("judge", `${verdict.resolvedModel ?? verdict.model} · ${formatDuration(ms())} · score ${score.toFixed(2)} ${score >= verdict.threshold ? "≥" : "<"} ${verdict.threshold} · ${verdict.verdict}${retries.length ? ` · ${retries.length} retries` : ""} · reason: P(yes) ${score.toFixed(2)} is ${score >= verdict.threshold ? "at or above" : "below"} the ${verdict.threshold} threshold`);
|
|
1141
|
-
debugTrace("judge", "response", {
|
|
1142
|
-
verdict,
|
|
1143
|
-
retries
|
|
1144
|
-
}, "judge response");
|
|
1145
|
-
a.report({
|
|
1146
|
-
kind: "judge-end",
|
|
1147
|
-
question,
|
|
1148
|
-
verdict: verdict.verdict,
|
|
1149
|
-
score,
|
|
1150
|
-
threshold: verdict.threshold,
|
|
1151
|
-
ms: ms()
|
|
1152
|
-
});
|
|
1153
|
-
return recorded;
|
|
1154
|
-
} catch (error) {
|
|
1155
|
-
if (a.signal.aborted) {
|
|
1156
|
-
a.judgeErrored = true;
|
|
1157
|
-
throw error;
|
|
1158
|
-
}
|
|
1159
|
-
const { retries } = error;
|
|
1160
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1161
|
-
return failed(isApiError(error) ? withInputSize(message, state, question) : message, retries);
|
|
1162
|
-
}
|
|
1163
|
-
};
|
|
1164
|
-
return async (question, options) => Object.defineProperty(await ask(question, options), SUBJECT, { value: question });
|
|
1165
|
-
}
|
|
1166
|
-
//#endregion
|
|
1167
|
-
//#region src/sandbox/index.ts
|
|
1168
|
-
async function createSandbox(descriptor, options) {
|
|
1169
|
-
try {
|
|
1170
|
-
switch (descriptor.kind) {
|
|
1171
|
-
case "fake": return await createFakeSandbox(descriptor, options.env);
|
|
1172
|
-
case "docker": {
|
|
1173
|
-
const { createDockerSandbox } = await import("./docker-Df_YOxxB.js");
|
|
1174
|
-
return await createDockerSandbox(descriptor, options);
|
|
1175
|
-
}
|
|
1176
|
-
}
|
|
1177
|
-
} catch (error) {
|
|
1178
|
-
if (error instanceof InfraError || options.signal.aborted) throw error;
|
|
1179
|
-
throw new InfraError(`Failed to create ${descriptor.kind} sandbox: ${error.message}`);
|
|
1180
|
-
}
|
|
1181
|
-
}
|
|
1182
|
-
//#endregion
|
|
1183
|
-
//#region src/attempt/record.ts
|
|
1184
|
-
function classify(error, signal) {
|
|
1185
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1186
|
-
if (signal.aborted) return isTimeout(signal.reason) ? {
|
|
1187
|
-
kind: "timeout",
|
|
1188
|
-
message: "The eval timed out"
|
|
1189
|
-
} : {
|
|
1190
|
-
kind: "aborted",
|
|
1191
|
-
message: "The run was cancelled"
|
|
1192
|
-
};
|
|
1193
|
-
if (error instanceof InfraError) return {
|
|
1194
|
-
kind: "infra",
|
|
1195
|
-
message
|
|
1196
|
-
};
|
|
1197
|
-
return {
|
|
1198
|
-
kind: "assertion",
|
|
1199
|
-
message
|
|
1200
|
-
};
|
|
1201
|
-
}
|
|
1202
|
-
/**
|
|
1203
|
-
* Why the attempt failed, into the record: the eval's own timeout, a cancel, an infra error, or an
|
|
1204
|
-
* assertion, which is the judge's when a judge call failed or gave a failing verdict.
|
|
1205
|
-
*/
|
|
1206
|
-
function recordFailure(a, error, timedOut) {
|
|
1207
|
-
const { record } = a;
|
|
1208
|
-
record.error = timedOut ? {
|
|
1209
|
-
kind: "timeout",
|
|
1210
|
-
message: "The eval timed out"
|
|
1211
|
-
} : classify(error, a.signal);
|
|
1212
|
-
record.error.message = capMiddle(record.error.message);
|
|
1213
|
-
if (error instanceof PrepareError && !timedOut) {
|
|
1214
|
-
const { class: kind, stage, ref, cause, log, setBy } = error.failure;
|
|
1215
|
-
record.error.prepare = {
|
|
1216
|
-
class: kind,
|
|
1217
|
-
stage,
|
|
1218
|
-
ref,
|
|
1219
|
-
cause,
|
|
1220
|
-
log,
|
|
1221
|
-
setBy: setBy ?? []
|
|
1222
|
-
};
|
|
1223
|
-
}
|
|
1224
|
-
const judgeFailed = a.judgeErrored || record.judgements.some((j) => j.verdict !== "pass");
|
|
1225
|
-
if (record.error.kind === "assertion" && judgeFailed) record.error.kind = "judge";
|
|
1226
|
-
record.outcome = record.error.kind === "infra" ? "infra_error" : "fail";
|
|
1227
|
-
}
|
|
1228
|
-
/**
|
|
1229
|
-
* A process the kernel killed at the sandbox's memory limit makes the attempt an infra error,
|
|
1230
|
-
* whatever its checks said: the limit, not the agent, decided it. Returns the error to fail it with.
|
|
1231
|
-
*/
|
|
1232
|
-
async function recordOom(a, sandbox) {
|
|
1233
|
-
const oom = await sandbox?.oomKills?.().catch(() => null);
|
|
1234
|
-
const at = oom && `the ${oom.limit} limit${oom.swap ? ` (+${oom.swap} swap)` : ""}`;
|
|
1235
|
-
if (oom) debugLog("teardown", `oom_kill ${oom.kills} at ${at}`);
|
|
1236
|
-
if (!oom?.kills) return void 0;
|
|
1237
|
-
const message = `out of memory: ${oom.kills === 1 ? "1 process" : `${oom.kills} processes`} killed at ${at}; raise docker({ memory })`;
|
|
1238
|
-
a.record.error = {
|
|
1239
|
-
kind: "infra",
|
|
1240
|
-
message,
|
|
1241
|
-
resource: {
|
|
1242
|
-
cause: "oom",
|
|
1243
|
-
kills: oom.kills
|
|
1244
|
-
}
|
|
1245
|
-
};
|
|
1246
|
-
a.record.outcome = "infra_error";
|
|
1247
|
-
return new InfraError(message);
|
|
1248
|
-
}
|
|
1249
|
-
/**
|
|
1250
|
-
* The attempt's egress policy and, when its sandbox records it, what it contacted and was refused.
|
|
1251
|
-
* Refused hosts the harness always tries are `expectedBlocked`, summarised in one `-v` line.
|
|
1252
|
-
*/
|
|
1253
|
-
function recordEgress(a, sandbox) {
|
|
1254
|
-
const traffic = sandbox?.traffic?.();
|
|
1255
|
-
const expected = expectedBlockedFor(a.spec.harness);
|
|
1256
|
-
const refused = traffic?.blocked ?? [];
|
|
1257
|
-
const isExpected = (h) => !!expected?.hosts.includes(h.host);
|
|
1258
|
-
const expectedBlocked = refused.filter(isExpected);
|
|
1259
|
-
a.record.egress = {
|
|
1260
|
-
policy: a.spec.egress.policy,
|
|
1261
|
-
allowed: allowedHosts(a.spec.egress),
|
|
1262
|
-
recorded: !!traffic,
|
|
1263
|
-
contacted: traffic?.contacted ?? [],
|
|
1264
|
-
blocked: refused.filter((h) => !isExpected(h)),
|
|
1265
|
-
expectedBlocked
|
|
1266
|
-
};
|
|
1267
|
-
if (expected && expectedBlocked.length) {
|
|
1268
|
-
const hosts = expectedBlocked.map((h) => `${h.host}:${h.port} ×${h.connections}`).join(", ");
|
|
1269
|
-
debugLog("gateway", `expected blocks, not logged per request: ${hosts} · ${expected.why}`);
|
|
1270
|
-
}
|
|
1271
|
-
a.record.servedModels = sandbox?.servedModels?.() ?? null;
|
|
1272
|
-
}
|
|
1273
|
-
/** Writes the attempt's record, redacted, and returns what the runner reports. */
|
|
1274
|
-
async function writeRecord(a, stagingDir, id) {
|
|
1275
|
-
const { record, spec } = a;
|
|
1276
|
-
const recordPath = join(stagingDir, "evals", recordFileName(attemptKey(id, spec.cell.key, spec.n)));
|
|
1277
|
-
await mkdir(join(stagingDir, "evals"), { recursive: true });
|
|
1278
|
-
await writeCaptureSidecars(stagingDir, recordFileName(attemptKey(id, spec.cell.key, spec.n)), a.artifactBlobs);
|
|
1279
|
-
const tmp = `${recordPath}.${process.pid}.tmp`;
|
|
1280
|
-
await writeFile(tmp, redactSecrets(JSON.stringify(record, null, 2)));
|
|
1281
|
-
await rename(tmp, recordPath);
|
|
1282
|
-
return {
|
|
1283
|
-
outcome: record.outcome,
|
|
1284
|
-
errorKind: record.error?.kind ?? null,
|
|
1285
|
-
judgements: record.judgements.map((j) => ({
|
|
1286
|
-
verdict: j.verdict,
|
|
1287
|
-
score: j.score,
|
|
1288
|
-
threshold: j.threshold,
|
|
1289
|
-
question: clip(j.question, 200)
|
|
1290
|
-
})),
|
|
1291
|
-
turnErrors: turnErrors(record.turns.map((t) => t.transcript)),
|
|
1292
|
-
...record.error?.prepare ? { prepare: {
|
|
1293
|
-
ref: record.error.prepare.ref,
|
|
1294
|
-
stage: record.error.prepare.stage
|
|
1295
|
-
} } : {}
|
|
1296
|
-
};
|
|
1297
|
-
}
|
|
1298
|
-
/** `turn N error: <message>` for each turn with an error event. */
|
|
1299
|
-
function turnErrors(transcripts) {
|
|
1300
|
-
return transcripts.flatMap((events, i) => {
|
|
1301
|
-
const found = events.find((e) => e.type === "error");
|
|
1302
|
-
return found?.type === "error" ? [`turn ${i + 1} error: ${redactSecrets(clip(found.message.trim(), 500))}`] : [];
|
|
1303
|
-
});
|
|
1304
|
-
}
|
|
1305
|
-
//#endregion
|
|
1306
|
-
//#region src/sandbox/phases.ts
|
|
1307
|
-
const strip = ({ phase: _, ...sub }) => sub;
|
|
1308
|
-
/**
|
|
1309
|
-
* Which brokered secrets the gateway swaps in when. `to: 'checks'` secrets are active only while
|
|
1310
|
-
* at least one check or setup step is running, so a request the agent makes between them gets
|
|
1311
|
-
* the placeholder.
|
|
1312
|
-
*/
|
|
1313
|
-
function brokerPhases(box, subs) {
|
|
1314
|
-
const agent = subs.filter((s) => s.phase === "agent").map(strip);
|
|
1315
|
-
const all = subs.map(strip);
|
|
1316
|
-
const perPhase = box.setSubstitutions && all.length > agent.length;
|
|
1317
|
-
let running = 0;
|
|
1318
|
-
return {
|
|
1319
|
-
async start() {
|
|
1320
|
-
if (all.length > 0) await box.setSubstitutions?.(agent);
|
|
1321
|
-
},
|
|
1322
|
-
async checks(fn) {
|
|
1323
|
-
if (!perPhase) return fn();
|
|
1324
|
-
if (running === 0) await box.setSubstitutions?.(all);
|
|
1325
|
-
running++;
|
|
1326
|
-
try {
|
|
1327
|
-
return await fn();
|
|
1328
|
-
} finally {
|
|
1329
|
-
if (--running === 0) await box.setSubstitutions?.(agent);
|
|
1330
|
-
}
|
|
1331
|
-
}
|
|
1332
|
-
};
|
|
1333
|
-
}
|
|
1334
|
-
//#endregion
|
|
1335
|
-
//#region src/attempt/state.ts
|
|
1336
|
-
/** Extra time an attempt gets, outside its eval timeout, when its image may need building. */
|
|
1337
|
-
const SETUP_BUDGET_MS = 12e5;
|
|
1338
|
-
function createAttempt(spec, signal, progress) {
|
|
1339
|
-
const record = {
|
|
1340
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1341
|
-
outcome: "pass",
|
|
1342
|
-
error: null,
|
|
1343
|
-
turns: [],
|
|
1344
|
-
diff: null,
|
|
1345
|
-
checks: [],
|
|
1346
|
-
judgements: [],
|
|
1347
|
-
timings: {
|
|
1348
|
-
setupMs: 0,
|
|
1349
|
-
agentMs: 0,
|
|
1350
|
-
diffMs: 0,
|
|
1351
|
-
checksMs: 0,
|
|
1352
|
-
judgeMs: 0
|
|
1353
|
-
},
|
|
1354
|
-
judgeInputs: []
|
|
1355
|
-
};
|
|
1356
|
-
return {
|
|
1357
|
-
spec,
|
|
1358
|
-
record,
|
|
1359
|
-
turns: [],
|
|
1360
|
-
report: (update) => progress({
|
|
1361
|
-
at: Date.now(),
|
|
1362
|
-
...update
|
|
1363
|
-
}),
|
|
1364
|
-
async timed(phase, fn) {
|
|
1365
|
-
const start = performance.now();
|
|
1366
|
-
try {
|
|
1367
|
-
return await fn();
|
|
1368
|
-
} finally {
|
|
1369
|
-
record.timings[phase] += Math.round(performance.now() - start);
|
|
1370
|
-
}
|
|
1371
|
-
},
|
|
1372
|
-
signal,
|
|
1373
|
-
judgeErrored: false,
|
|
1374
|
-
judgeErrors: [],
|
|
1375
|
-
captureNames: /* @__PURE__ */ new Set(),
|
|
1376
|
-
backgrounds: [],
|
|
1377
|
-
artifactBlobs: /* @__PURE__ */ new Map()
|
|
1378
|
-
};
|
|
1379
|
-
}
|
|
1380
|
-
const startTimeoutMs = (harness) => parseDuration(harness.startTimeout ?? "90s");
|
|
1381
|
-
/**
|
|
1382
|
-
* Setup steps and the harness's start (once retried) run outside the eval timeout, so the
|
|
1383
|
-
* attempt's own limit makes room for them.
|
|
1384
|
-
*/
|
|
1385
|
-
function setupAllowance(planned) {
|
|
1386
|
-
const setupMs = planned.environment.layers.flatMap((l) => l.spec.setup ?? []).reduce((ms, step) => ms + parseDuration((typeof step === "string" ? void 0 : step.timeout) ?? "5m"), 0);
|
|
1387
|
-
const { sandbox } = planned;
|
|
1388
|
-
return setupMs + (sandbox.kind === "docker" && !!(sandbox.from || sandbox.dockerfile || sandbox.install?.length) ? SETUP_BUDGET_MS : 0) + 2 * startTimeoutMs(planned.harness);
|
|
1389
|
-
}
|
|
1390
|
-
/** The eval's timer, paused while the sandbox and its setup steps start. */
|
|
1391
|
-
function evalTimer(ms) {
|
|
1392
|
-
const controller = new AbortController();
|
|
1393
|
-
let left = ms;
|
|
1394
|
-
let since = Date.now();
|
|
1395
|
-
let timer;
|
|
1396
|
-
const fire = () => controller.abort(new DOMException(`Eval timed out after ${ms / 1e3}s`, "TimeoutError"));
|
|
1397
|
-
const start = () => {
|
|
1398
|
-
since = Date.now();
|
|
1399
|
-
timer = setTimeout(fire, Math.max(0, left));
|
|
1400
|
-
};
|
|
1401
|
-
start();
|
|
1402
|
-
return {
|
|
1403
|
-
signal: controller.signal,
|
|
1404
|
-
pause() {
|
|
1405
|
-
clearTimeout(timer);
|
|
1406
|
-
left -= Date.now() - since;
|
|
1407
|
-
},
|
|
1408
|
-
resume: start,
|
|
1409
|
-
clear: () => clearTimeout(timer)
|
|
1410
|
-
};
|
|
1411
|
-
}
|
|
1412
|
-
//#endregion
|
|
1413
|
-
//#region src/attempt/session.ts
|
|
1414
|
-
const tailOf = (text) => text.trim().split("\n").slice(-10).join("\n");
|
|
1415
|
-
/** Setup steps in order, each with its own timeout; a failure is an infra error. */
|
|
1416
|
-
async function runSetup(a, box, env) {
|
|
1417
|
-
const { record, spec } = a;
|
|
1418
|
-
const steps = env.setup;
|
|
1419
|
-
record.setup = [];
|
|
1420
|
-
for (const [i, step] of steps.entries()) {
|
|
1421
|
-
const what = `setup step ${i + 1}/${steps.length} "${clip(step.run, 80)}" in environment ${spec.environment.name}`;
|
|
1422
|
-
const start = performance.now();
|
|
1423
|
-
const stepSignal = AbortSignal.any([a.signal, AbortSignal.timeout(step.timeoutMs)]);
|
|
1424
|
-
const r = await asCheck(a, () => box.exec(step.run, {
|
|
1425
|
-
signal: stepSignal,
|
|
1426
|
-
env: env.checkVars,
|
|
1427
|
-
...step.cwd ? { cwd: step.cwd } : {}
|
|
1428
|
-
})).catch((error) => {
|
|
1429
|
-
if (a.signal.aborted) throw error;
|
|
1430
|
-
if (!stepSignal.aborted) throw new InfraError(`${what} couldn't run: ${error instanceof Error ? error.message : error}`);
|
|
1431
|
-
return null;
|
|
1432
|
-
});
|
|
1433
|
-
const durationMs = Math.round(performance.now() - start);
|
|
1434
|
-
const output = r ? [r.stdout, r.stderr].filter(Boolean).join("\n") : "";
|
|
1435
|
-
record.setup.push({
|
|
1436
|
-
run: step.run,
|
|
1437
|
-
exitCode: r?.exitCode ?? null,
|
|
1438
|
-
durationMs,
|
|
1439
|
-
output
|
|
1440
|
-
});
|
|
1441
|
-
debugLog("env", `setup ${i + 1}/${steps.length} $ ${clip(step.run, 120)} → ${r ? `exit ${r.exitCode}` : `timed out after ${formatDuration(step.timeoutMs)}`} · ${formatDuration(durationMs)}${step.cwd ? ` · in ${step.cwd}` : ""}`);
|
|
1442
|
-
debugTrace("env", "setup", {
|
|
1443
|
-
run: step.run,
|
|
1444
|
-
cwd: step.cwd,
|
|
1445
|
-
exitCode: r?.exitCode ?? null,
|
|
1446
|
-
durationMs,
|
|
1447
|
-
stdout: r?.stdout,
|
|
1448
|
-
stderr: r?.stderr
|
|
1449
|
-
}, `setup ${i + 1} output`);
|
|
1450
|
-
if (r?.exitCode === 0) continue;
|
|
1451
|
-
throw new InfraError(r ? `${what} exited ${r.exitCode} after ${formatDuration(durationMs)} (see setup.log):\n${tailOf(r.stderr || r.stdout)}` : `${what} timed out after ${formatDuration(step.timeoutMs)} (see setup.log)`);
|
|
1452
|
-
}
|
|
1453
|
-
}
|
|
1454
|
-
const WORKSPACE_TAR = ".reeval-workspace.tar";
|
|
1455
|
-
/** Unpacks the git workspace's tarball into the (empty) workspace. */
|
|
1456
|
-
async function copyWorkspace(box, workspace, signal) {
|
|
1457
|
-
const start = performance.now();
|
|
1458
|
-
const tar = await readFile(workspace.tar);
|
|
1459
|
-
await box.writeFile(WORKSPACE_TAR, tar);
|
|
1460
|
-
const r = await box.exec(`tar -xf ${WORKSPACE_TAR} && rm -f ${WORKSPACE_TAR}`, { signal });
|
|
1461
|
-
if (r.exitCode !== 0) throw new InfraError(`Couldn't unpack the workspace ${workspace.label} into the sandbox (the image needs tar): ${tailOf(r.stderr || r.stdout)}`);
|
|
1462
|
-
if (workspace.init) {
|
|
1463
|
-
const at = "2000-01-01T00:00:00Z";
|
|
1464
|
-
const init = await box.exec(`git init -q -b main && git add -A && git -c commit.gpgsign=false commit -q --no-verify -m ${shellQuote(`${workspace.label} at ${workspace.commit}`)}`, {
|
|
1465
|
-
signal,
|
|
1466
|
-
env: {
|
|
1467
|
-
GIT_AUTHOR_NAME: "reeval",
|
|
1468
|
-
GIT_AUTHOR_EMAIL: "reeval@localhost",
|
|
1469
|
-
GIT_AUTHOR_DATE: at,
|
|
1470
|
-
GIT_COMMITTER_NAME: "reeval",
|
|
1471
|
-
GIT_COMMITTER_EMAIL: "reeval@localhost",
|
|
1472
|
-
GIT_COMMITTER_DATE: at
|
|
1473
|
-
}
|
|
1474
|
-
});
|
|
1475
|
-
if (init.exitCode !== 0) throw new InfraError(`Couldn't give the workspace ${workspace.label} a .git (init: true needs git in the image): ${tailOf(init.stderr || init.stdout)}`);
|
|
1476
|
-
}
|
|
1477
|
-
debugLog("env", `workspace ${workspace.label} at ${workspace.commit.slice(0, 12)} (${tar.byteLength} bytes of tar${workspace.init ? ", with a .git" : ""}) in ${formatDuration(performance.now() - start)}`);
|
|
1478
|
-
}
|
|
1479
|
-
/**
|
|
1480
|
-
* Starts the harness session within its `startTimeout`. A start that doesn't become ready in time
|
|
1481
|
-
* is aborted (and closed, should it finish later) and started once more: a stuck bootstrap is
|
|
1482
|
-
* usually transient, and nothing has run yet, so the retry can't change what the attempt
|
|
1483
|
-
* measures. A second timeout is an infra error; a start that fails outright isn't retried.
|
|
1484
|
-
*/
|
|
1485
|
-
async function startHarness(a, harness, box, options) {
|
|
1486
|
-
const { harness: descriptor } = a.spec;
|
|
1487
|
-
const ms = startTimeoutMs(descriptor);
|
|
1488
|
-
const late = `${descriptor.kind}: session did not become ready within ${formatDuration(ms)}`;
|
|
1489
|
-
for (let tries = 1;; tries++) {
|
|
1490
|
-
const stop = new AbortController();
|
|
1491
|
-
let timer;
|
|
1492
|
-
const timedOut = new Promise((resolve) => {
|
|
1493
|
-
timer = setTimeout(() => resolve(null), ms);
|
|
1494
|
-
});
|
|
1495
|
-
const starting = harness.start(box, AbortSignal.any([a.signal, stop.signal]), options);
|
|
1496
|
-
const session = await Promise.race([starting, timedOut]).finally(() => clearTimeout(timer));
|
|
1497
|
-
if (session) return session;
|
|
1498
|
-
stop.abort(new InfraError(late));
|
|
1499
|
-
starting.then((s) => s.close().catch(() => {}), () => {});
|
|
1500
|
-
a.signal.throwIfAborted();
|
|
1501
|
-
if (tries === 2) throw new InfraError(`${late} (tried twice)`);
|
|
1502
|
-
a.record.startRetries = [{
|
|
1503
|
-
error: late,
|
|
1504
|
-
waitedMs: ms
|
|
1505
|
-
}];
|
|
1506
|
-
debugLog("harness", `${late}; starting it once more`);
|
|
1507
|
-
}
|
|
1508
|
-
}
|
|
1509
|
-
/**
|
|
1510
|
-
* Starts the sandbox (with the brokered secrets its gateway swaps in), writes the
|
|
1511
|
-
* environment's files and workspace skills, runs its setup steps, snapshots the workspace for
|
|
1512
|
-
* the diff, and starts the harness session with the harness-placed skills and MCP servers.
|
|
1513
|
-
*/
|
|
1514
|
-
async function startSession(a, runId, setupStart) {
|
|
1515
|
-
const { spec } = a;
|
|
1516
|
-
const env = a.env;
|
|
1517
|
-
const box = await createSandbox(spec.sandbox, {
|
|
1518
|
-
runId,
|
|
1519
|
-
timeoutMs: spec.timeoutMs + setupAllowance(spec) + 6e4,
|
|
1520
|
-
signal: a.signal,
|
|
1521
|
-
env: env.vars,
|
|
1522
|
-
image: a.image,
|
|
1523
|
-
brokering: brokeringFor(spec.harness),
|
|
1524
|
-
expectedBlocked: expectedBlockedFor(spec.harness)?.hosts,
|
|
1525
|
-
substitutions: env.substitutions,
|
|
1526
|
-
egress: spec.egress
|
|
1527
|
-
});
|
|
1528
|
-
a.sandbox = box;
|
|
1529
|
-
if (spec.sandbox.kind === "fake") debugLog("container", `fake sandbox at ${box.workdir}`);
|
|
1530
|
-
a.phases = brokerPhases(box, env.substitutions);
|
|
1531
|
-
if (a.signal.aborted) box.destroy();
|
|
1532
|
-
a.signal.throwIfAborted();
|
|
1533
|
-
await a.phases.start();
|
|
1534
|
-
if (env.substitutions.length) {
|
|
1535
|
-
const agent = env.substitutions.filter((s) => s.phase === "agent").map((s) => s.name);
|
|
1536
|
-
debugLog("broker", `gateway swaps in ${agent.join(", ") || "nothing"} for the agent`);
|
|
1537
|
-
}
|
|
1538
|
-
if (env.workspace) await copyWorkspace(box, env.workspace, a.signal);
|
|
1539
|
-
for (const f of env.files) {
|
|
1540
|
-
await box.writeFile(f.path, f.content);
|
|
1541
|
-
const size = typeof f.content === "string" ? Buffer.byteLength(f.content) : f.content.byteLength;
|
|
1542
|
-
debugLog("env", `file ${f.path} (${size} bytes${f.executable ? ", executable" : ""})`);
|
|
1543
|
-
}
|
|
1544
|
-
const executable = env.files.flatMap((f) => f.executable ? [f.path.startsWith("~/") ? `${box.home}/${f.path.slice(2)}` : f.path] : []);
|
|
1545
|
-
if (executable.length) {
|
|
1546
|
-
const paths = executable.map((p) => shellQuote(p.startsWith("-") ? `./${p}` : p));
|
|
1547
|
-
const r = await box.exec(`chmod +x ${paths.join(" ")}`, { signal: a.signal });
|
|
1548
|
-
if (r.exitCode !== 0) throw new InfraError(`Couldn't make ${executable.join(", ")} executable: ${tailOf(r.stderr || r.stdout)}`);
|
|
1549
|
-
}
|
|
1550
|
-
const skillDir = workspaceSkillDir(spec.harness);
|
|
1551
|
-
for (const skill of env.workspaceSkills) {
|
|
1552
|
-
const files = skillFiles(skill);
|
|
1553
|
-
for (const f of files) await box.writeFile(`${skillDir}/${skill.name}/${f.path}`, f.content);
|
|
1554
|
-
debugLog("env", `skill ${skill.name} → ${skillDir}/${skill.name}/ (${files.length} files, workspace)`);
|
|
1555
|
-
}
|
|
1556
|
-
const layout = layoutOf(spec.harness);
|
|
1557
|
-
for (const skill of env.skills) debugLog("env", `skill ${skill.name} → the harness adapter places it (~/${layout.harnessSkillDir}/${skill.name}/ for ${spec.harness.kind === "fake" ? `the fake ${spec.harness.layout ?? "cursor"} layout` : spec.harness.kind})`);
|
|
1558
|
-
for (const [name, server] of Object.entries(env.mcp)) debugLog("env", `mcp ${name} (${server.transport}${server.transport === "stdio" ? `: ${server.command} ${server.args.join(" ")}` : `: ${server.url}`}) → the harness adapter writes its MCP config`);
|
|
1559
|
-
await runSetup(a, box, env);
|
|
1560
|
-
await snapshot(box, a.signal);
|
|
1561
|
-
debugLog("env", "workspace snapshot taken: the diff starts here");
|
|
1562
|
-
const harness = await createHarness(spec.harness);
|
|
1563
|
-
const harnessStart = performance.now();
|
|
1564
|
-
debugLog("harness", `starting ${harnessId(spec.harness)}: bootstrap and session`);
|
|
1565
|
-
const session = await startHarness(a, harness, box, {
|
|
1566
|
-
skills: env.skills,
|
|
1567
|
-
workspaceSkills: env.workspaceSkills.map((s) => s.name),
|
|
1568
|
-
mcp: env.mcp
|
|
1569
|
-
});
|
|
1570
|
-
debugLog("harness", `${harnessId(spec.harness)} session ready in ${formatDuration(performance.now() - harnessStart)}`);
|
|
1571
|
-
a.session = session;
|
|
1572
|
-
a.report({
|
|
1573
|
-
kind: "sandbox-ready",
|
|
1574
|
-
ms: Math.round(performance.now() - setupStart)
|
|
1575
|
-
});
|
|
1576
|
-
return [box, session];
|
|
1577
|
-
}
|
|
1578
|
-
//#endregion
|
|
1579
|
-
//#region src/attempt/turns.ts
|
|
1580
|
-
/** How long a turn gets to stop once its attempt is aborted, before reeval stops waiting for it. */
|
|
1581
|
-
const TURN_STOP_GRACE_MS = 3e3;
|
|
1582
|
-
/**
|
|
1583
|
-
* Resolves `grace` ms after `signal` aborts, with the turn as cut off; `cancel` drops the timer
|
|
1584
|
-
* once the turn settles on its own.
|
|
1585
|
-
*/
|
|
1586
|
-
function cutOff(signal, startedAt, grace = TURN_STOP_GRACE_MS) {
|
|
1587
|
-
let timer;
|
|
1588
|
-
let onAbort = () => {};
|
|
1589
|
-
const cutoff = new Promise((resolve) => {
|
|
1590
|
-
onAbort = () => {
|
|
1591
|
-
timer = setTimeout(() => {
|
|
1592
|
-
const message = `The agent didn't stop within ${grace / 1e3}s of being cancelled, so reeval stopped waiting for its turn`;
|
|
1593
|
-
const at = Date.now() - Date.parse(startedAt);
|
|
1594
|
-
resolve({
|
|
1595
|
-
text: "",
|
|
1596
|
-
status: "aborted",
|
|
1597
|
-
transcript: [{
|
|
1598
|
-
type: "error",
|
|
1599
|
-
message,
|
|
1600
|
-
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1601
|
-
atMs: at
|
|
1602
|
-
}],
|
|
1603
|
-
usage: null,
|
|
1604
|
-
model: null,
|
|
1605
|
-
unreported: [],
|
|
1606
|
-
startedAt,
|
|
1607
|
-
durationMs: at
|
|
1608
|
-
});
|
|
1609
|
-
}, grace);
|
|
1610
|
-
};
|
|
1611
|
-
if (signal.aborted) onAbort();
|
|
1612
|
-
else signal.addEventListener("abort", onAbort, { once: true });
|
|
1613
|
-
});
|
|
1614
|
-
const cancel = () => {
|
|
1615
|
-
clearTimeout(timer);
|
|
1616
|
-
signal.removeEventListener("abort", onAbort);
|
|
1617
|
-
};
|
|
1618
|
-
return {
|
|
1619
|
-
cutoff,
|
|
1620
|
-
cancel
|
|
1621
|
-
};
|
|
1622
|
-
}
|
|
1623
|
-
/**
|
|
1624
|
-
* One turn. Sends the prompt to the started session, records the turn as it goes (an
|
|
1625
|
-
* errored entry first, so a turn that throws is still in the record), takes the agent's context
|
|
1626
|
-
* after the first, and diffs the workspace since the snapshot.
|
|
1627
|
-
*/
|
|
1628
|
-
function turnSender(a, start, takeContext, checks) {
|
|
1629
|
-
return async (prompt) => {
|
|
1630
|
-
const [box, agent] = await start();
|
|
1631
|
-
const entry = {
|
|
1632
|
-
prompt,
|
|
1633
|
-
text: "",
|
|
1634
|
-
status: "error",
|
|
1635
|
-
transcript: [],
|
|
1636
|
-
usage: null,
|
|
1637
|
-
model: null,
|
|
1638
|
-
unreported: [],
|
|
1639
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1640
|
-
durationMs: 0
|
|
1641
|
-
};
|
|
1642
|
-
a.record.turns.push(entry);
|
|
1643
|
-
const turnNumber = a.record.turns.length;
|
|
1644
|
-
a.report({
|
|
1645
|
-
kind: "turn-start",
|
|
1646
|
-
turn: turnNumber
|
|
1647
|
-
});
|
|
1648
|
-
debugLog("harness", `turn ${turnNumber} started: ${JSON.stringify(clip(prompt, 160))}`);
|
|
1649
|
-
debugTrace("harness", "prompt", {
|
|
1650
|
-
turn: turnNumber,
|
|
1651
|
-
prompt
|
|
1652
|
-
}, `turn ${turnNumber} prompt`);
|
|
1653
|
-
const { cutoff, cancel } = cutOff(a.signal, entry.startedAt);
|
|
1654
|
-
const { raw, ...result } = await a.timed("agentMs", () => Promise.race([agent.send(prompt, a.signal), cutoff])).finally(cancel);
|
|
1655
|
-
Object.assign(entry, result, raw ? { raw } : {});
|
|
1656
|
-
for (const event of result.transcript) debugTrace("harness", "transcript", {
|
|
1657
|
-
turn: turnNumber,
|
|
1658
|
-
...event
|
|
1659
|
-
}, `transcript ${event.type}`);
|
|
1660
|
-
await takeContext(box, a.signal);
|
|
1661
|
-
const count = (...types) => result.transcript.filter((e) => types.includes(e.type)).length;
|
|
1662
|
-
const failure = result.transcript.find((e) => e.type === "error");
|
|
1663
|
-
a.report({
|
|
1664
|
-
kind: "turn-end",
|
|
1665
|
-
turn: turnNumber,
|
|
1666
|
-
status: result.status,
|
|
1667
|
-
ms: result.durationMs,
|
|
1668
|
-
tools: count("tool", "web-search", "web-fetch"),
|
|
1669
|
-
fileEdits: count("file-edit"),
|
|
1670
|
-
...failure?.type === "error" ? { error: redactSecrets(clip(failure.message.trim(), 300)) } : {}
|
|
1671
|
-
});
|
|
1672
|
-
const plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
1673
|
-
debugLog("harness", `turn ${turnNumber} ${result.status} in ${formatDuration(result.durationMs)} · ${plural(count("tool", "web-search", "web-fetch"), "tool call")} · ${plural(count("file-edit"), "file edit")}${failure && "message" in failure ? ` · error: ${clip(String(failure.message), 200)}` : ""}${result.model ? ` · model ${result.model}` : ""}`);
|
|
1674
|
-
a.signal.throwIfAborted();
|
|
1675
|
-
const diff = await a.timed("diffMs", () => diffSinceSnapshot(box, a.signal));
|
|
1676
|
-
a.record.diff = diff;
|
|
1677
|
-
const data = {
|
|
1678
|
-
prompt,
|
|
1679
|
-
...result,
|
|
1680
|
-
diff
|
|
1681
|
-
};
|
|
1682
|
-
const turn = Object.freeze({
|
|
1683
|
-
...data,
|
|
1684
|
-
...checks
|
|
1685
|
-
});
|
|
1686
|
-
a.turns.push(turn);
|
|
1687
|
-
return turn;
|
|
1688
|
-
};
|
|
1689
|
-
}
|
|
1690
|
-
//#endregion
|
|
1691
|
-
//#region src/attempt/index.ts
|
|
1692
|
-
const TRACE_BUFFER_DEFAULT = 8 * 2 ** 20;
|
|
1693
|
-
async function runAttempt(spec, fn, handle) {
|
|
1694
|
-
const ctx = getRunContext();
|
|
1695
|
-
const { config, runId } = ctx;
|
|
1696
|
-
const verbosity = ctx.verbosity ?? 0;
|
|
1697
|
-
const id = evalId(relativeFile(config.root, handle.file), [...spec.groups, spec.name]);
|
|
1698
|
-
const debug = new AttemptDebug({
|
|
1699
|
-
dir: attemptDebugDir(config.root, runId, id, spec.cell.key, spec.n),
|
|
1700
|
-
verbosity,
|
|
1701
|
-
traceLimit: config.debug?.traceBufferBytes ?? TRACE_BUFFER_DEFAULT,
|
|
1702
|
-
...verbosity > 0 ? { emit: handle.debug } : {}
|
|
1703
|
-
});
|
|
1704
|
-
return withDebug(debug, () => attemptBody(spec, fn, handle, id, debug));
|
|
1705
|
-
}
|
|
1706
|
-
async function attemptBody(spec, fn, { signal: testSignal, setMeta, progress, unawaited, assertions }, id, debug) {
|
|
1707
|
-
const setupStart = performance.now();
|
|
1708
|
-
const ctx = getRunContext();
|
|
1709
|
-
const { runId, stagingDir, config } = ctx;
|
|
1710
|
-
tracePlan(debug, spec, ctx);
|
|
1711
|
-
const a = createAttempt(spec, testSignal, progress);
|
|
1712
|
-
let timer;
|
|
1713
|
-
let clock;
|
|
1714
|
-
const destroy = () => void a.sandbox?.destroy();
|
|
1715
|
-
testSignal.addEventListener("abort", destroy, { once: true });
|
|
1716
|
-
let starting;
|
|
1717
|
-
const start = () => {
|
|
1718
|
-
starting ??= (async () => {
|
|
1719
|
-
clock?.pause();
|
|
1720
|
-
try {
|
|
1721
|
-
return await a.timed("setupMs", () => startSession(a, runId, setupStart));
|
|
1722
|
-
} finally {
|
|
1723
|
-
clock?.resume();
|
|
1724
|
-
}
|
|
1725
|
-
})();
|
|
1726
|
-
return starting;
|
|
1727
|
-
};
|
|
1728
|
-
const takeContext = contextTaker(a);
|
|
1729
|
-
const { exec, readFile, writeFile } = createChecks(a);
|
|
1730
|
-
const c = {
|
|
1731
|
-
get signal() {
|
|
1732
|
-
return a.signal;
|
|
1733
|
-
},
|
|
1734
|
-
turns: a.turns,
|
|
1735
|
-
exec,
|
|
1736
|
-
readFile,
|
|
1737
|
-
writeFile,
|
|
1738
|
-
serve: (command, port) => serveCommand(a, command, port),
|
|
1739
|
-
capture: (target, options) => capturePage(a, target, options),
|
|
1740
|
-
attempt: {
|
|
1741
|
-
id: attemptId(spec.cell.key, spec.n),
|
|
1742
|
-
cell: spec.cell,
|
|
1743
|
-
n: spec.n
|
|
1744
|
-
},
|
|
1745
|
-
async sandbox() {
|
|
1746
|
-
await start();
|
|
1747
|
-
return {
|
|
1748
|
-
exec,
|
|
1749
|
-
readFile
|
|
1750
|
-
};
|
|
1751
|
-
},
|
|
1752
|
-
send: turnSender(a, start, takeContext, {
|
|
1753
|
-
exec,
|
|
1754
|
-
readFile
|
|
1755
|
-
}),
|
|
1756
|
-
judge: judgeCaller(a, readFile)
|
|
1757
|
-
};
|
|
1758
|
-
const timedOut = () => !!timer?.aborted && !testSignal.aborted;
|
|
1759
|
-
let failure;
|
|
1760
|
-
let oom;
|
|
1761
|
-
try {
|
|
1762
|
-
a.report({ kind: "sandbox" });
|
|
1763
|
-
const where = `${id} [${attemptId(spec.cell.key, spec.n)}]`;
|
|
1764
|
-
await a.timed("setupMs", () => prepareEnvironment(a, config.root, runId, where, testSignal));
|
|
1765
|
-
clock = evalTimer(spec.timeoutMs);
|
|
1766
|
-
timer = clock.signal;
|
|
1767
|
-
a.signal = AbortSignal.any([testSignal, timer]);
|
|
1768
|
-
timer.addEventListener("abort", destroy, { once: true });
|
|
1769
|
-
await fn(c);
|
|
1770
|
-
a.signal.throwIfAborted();
|
|
1771
|
-
const missed = await unawaited();
|
|
1772
|
-
if (missed) throw missed;
|
|
1773
|
-
const [only, ...more] = a.judgeErrors;
|
|
1774
|
-
if (only) throw new Error(more.length ? `${more.length + 1} judge calls failed:\n${a.judgeErrors.join("\n")}` : only);
|
|
1775
|
-
} catch (error) {
|
|
1776
|
-
recordFailure(a, error, timedOut());
|
|
1777
|
-
failure = { error: timedOut() ? new TimeoutError(`Eval timed out after ${spec.timeoutMs / 1e3}s`) : error };
|
|
1778
|
-
} finally {
|
|
1779
|
-
if (!a.signal.aborted) await unawaited().catch(() => {});
|
|
1780
|
-
clock?.clear();
|
|
1781
|
-
for (const bg of a.backgrounds) await bg.kill().catch(() => {});
|
|
1782
|
-
const { sandbox } = a;
|
|
1783
|
-
if (sandbox && !a.signal.aborted) await takeContext(sandbox, AbortSignal.timeout(3e4));
|
|
1784
|
-
if (sandbox) a.report({ kind: "cleanup" });
|
|
1785
|
-
testSignal.removeEventListener("abort", destroy);
|
|
1786
|
-
const teardown = performance.now();
|
|
1787
|
-
await a.session?.close().catch(() => {});
|
|
1788
|
-
if (a.session) debug.log("teardown", "harness session closed");
|
|
1789
|
-
recordEgress(a, sandbox);
|
|
1790
|
-
oom = a.signal.aborted ? void 0 : await recordOom(a, sandbox);
|
|
1791
|
-
await sandbox?.destroy();
|
|
1792
|
-
if (sandbox) debug.log("teardown", `sandbox destroyed (${formatDuration(performance.now() - teardown)} for teardown)`);
|
|
1793
|
-
const { record } = a;
|
|
1794
|
-
const checked = assertions();
|
|
1795
|
-
if (checked.length) record.assertions = checked;
|
|
1796
|
-
for (const x of checked) {
|
|
1797
|
-
const why = x.message ? ` · ${clip(x.message.split("\n")[0] ?? "", 160)}` : "";
|
|
1798
|
-
debug.log("check", `assert ${x.status === "pass" ? "✓" : "✗"} ${x.label} (${x.matcher}, ${x.status})${why}`);
|
|
1799
|
-
}
|
|
1800
|
-
const error = record.error ? ` · ${record.error.kind}: ${record.error.message}` : "";
|
|
1801
|
-
debug.log("teardown", `outcome ${record.outcome}${error}`);
|
|
1802
|
-
const rel = (path) => relativeFile(config.root, path) || ".";
|
|
1803
|
-
const scratch = `.reeval/runs/${runId}`;
|
|
1804
|
-
debug.log("teardown", `record ${scratch}.json · artifacts ${scratch}/${evalSlug(id)}/${evalSlug(spec.cell.key)}/${spec.n} · debug ${rel(debug.dir)}`);
|
|
1805
|
-
setMeta(await writeRecord(a, stagingDir, id));
|
|
1806
|
-
await debug.finish(debug.verbosity === 2 || record.outcome !== "pass");
|
|
1807
|
-
}
|
|
1808
|
-
if (oom) throw oom;
|
|
1809
|
-
if (failure) throw failure.error;
|
|
1810
|
-
}
|
|
1811
|
-
//#endregion
|
|
1812
|
-
//#region src/plan-eval.ts
|
|
1813
|
-
/** Lets the attempt's own eval timer fire before the engine's backstop, so the message and cleanup are its own. */
|
|
1814
|
-
const GRACE_MS = 5e3;
|
|
1815
|
-
function configLevel(config) {
|
|
1816
|
-
return {
|
|
1817
|
-
harness: config.harness,
|
|
1818
|
-
sandbox: config.sandbox,
|
|
1819
|
-
environment: config.environment,
|
|
1820
|
-
compare: void 0,
|
|
1821
|
-
runs: config.runs,
|
|
1822
|
-
gate: config.gate,
|
|
1823
|
-
env: config.env,
|
|
1824
|
-
egress: config.egress !== void 0 ? { inherited: config.egress } : {},
|
|
1825
|
-
judge: config.judge,
|
|
1826
|
-
capture: config.capture,
|
|
1827
|
-
timeout: void 0,
|
|
1828
|
-
tags: [],
|
|
1829
|
-
pin: [],
|
|
1830
|
-
sources: {}
|
|
1831
|
-
};
|
|
1832
|
-
}
|
|
1833
|
-
/**
|
|
1834
|
-
* The tests an eval registers, or `null` when the run's tags leave it out (it isn't counted in
|
|
1835
|
-
* `order`). Throws a UsageError for options the plan refuses.
|
|
1836
|
-
*/
|
|
1837
|
-
function planEval(declaration, ctx) {
|
|
1838
|
-
const { name, mode, groups: enclosing, options: own } = declaration;
|
|
1839
|
-
const groups = enclosing.map((g) => g.name);
|
|
1840
|
-
const where = `evaluate("${[...groups, name].join(" › ")}")`;
|
|
1841
|
-
checkOptions(own, where);
|
|
1842
|
-
const levels = [...enclosing.map((g) => ({
|
|
1843
|
-
options: g.options,
|
|
1844
|
-
where: `group("${g.name}")`
|
|
1845
|
-
})), {
|
|
1846
|
-
options: own,
|
|
1847
|
-
where
|
|
1848
|
-
}];
|
|
1849
|
-
const resolvedEval = cascade(configLevel(ctx.config), levels, where, {
|
|
1850
|
-
profile: ctx.profile,
|
|
1851
|
-
runs: ctx.runs
|
|
1852
|
-
});
|
|
1853
|
-
const { resolved } = resolvedEval;
|
|
1854
|
-
if (!matchesTags(resolved.tags, ctx.tags)) return null;
|
|
1855
|
-
const timeoutMs = resolved.timeout === void 0 ? ctx.config.timeoutMs : parseDuration(resolved.timeout);
|
|
1856
|
-
const plannedAttempts = resolvedEval.skip ? [] : expand(resolved);
|
|
1857
|
-
if (ctx.config.secrets.unbrokered === "error") {
|
|
1858
|
-
const [first] = unbrokered(resolved.env, plannedAttempts);
|
|
1859
|
-
if (first) throw new UsageError(unbrokeredError(first, where, ctx.config.file));
|
|
1860
|
-
}
|
|
1861
|
-
for (const planned of plannedAttempts) {
|
|
1862
|
-
const why = unenforceable(planned.egress, planned.sandbox, where);
|
|
1863
|
-
if (why) throw new UsageError(why);
|
|
1864
|
-
const mcp = refusedMcp(planned.harness, planned.environment.layers);
|
|
1865
|
-
if (mcp) throw new UsageError(`${where} on ${harnessId(planned.harness)}: ${mcp}`);
|
|
1866
|
-
}
|
|
1867
|
-
const path = ctx.file ? relativeFile(ctx.config.root, ctx.file) : null;
|
|
1868
|
-
const rankFile = relativeFile(ctx.config.root, ctx.file ?? "");
|
|
1869
|
-
const family = declaration.family && {
|
|
1870
|
-
id: evalId(rankFile, [...groups, declaration.family.name]),
|
|
1871
|
-
...declaration.family
|
|
1872
|
-
};
|
|
1873
|
-
const harnessBy = resolved.sources?.harness ?? ctx.config.file;
|
|
1874
|
-
const plan = evalPlan(resolvedEval, plannedAttempts, {
|
|
1875
|
-
configFile: ctx.config.file,
|
|
1876
|
-
file: path
|
|
1877
|
-
});
|
|
1878
|
-
const setByOf = (planned) => ({
|
|
1879
|
-
sandbox: sandboxSetBy(planned, resolved, ctx.config.file),
|
|
1880
|
-
harness: harnessBy
|
|
1881
|
-
});
|
|
1882
|
-
if (resolvedEval.skip) {
|
|
1883
|
-
const [first] = expand(resolved);
|
|
1884
|
-
if (!first) return [];
|
|
1885
|
-
const attempt = {
|
|
1886
|
-
name,
|
|
1887
|
-
groups,
|
|
1888
|
-
tags: resolved.tags,
|
|
1889
|
-
cell: first.cell,
|
|
1890
|
-
n: 1,
|
|
1891
|
-
label: "",
|
|
1892
|
-
harness: first.harness,
|
|
1893
|
-
sandbox: first.sandbox,
|
|
1894
|
-
setBy: setByOf(first),
|
|
1895
|
-
gate: "none",
|
|
1896
|
-
brokered: [],
|
|
1897
|
-
secretVars: [],
|
|
1898
|
-
egress: first.egress,
|
|
1899
|
-
plan,
|
|
1900
|
-
...family ? { family } : {}
|
|
1901
|
-
};
|
|
1902
|
-
return [{
|
|
1903
|
-
name: `${name} [${attemptId(first.cell.key, 1)}]`,
|
|
1904
|
-
mode: "skip",
|
|
1905
|
-
attempt
|
|
1906
|
-
}];
|
|
1907
|
-
}
|
|
1908
|
-
const cells = [];
|
|
1909
|
-
return plannedAttempts.map((planned) => {
|
|
1910
|
-
if (!cells.includes(planned.cell.key)) cells.push(planned.cell.key);
|
|
1911
|
-
const workspace = workspaceOf(planned.environment.layers);
|
|
1912
|
-
const files = filesOf(planned.environment.layers);
|
|
1913
|
-
const setBy = setByOf(planned);
|
|
1914
|
-
const id = evalId(rankFile, [...groups, name]);
|
|
1915
|
-
const key = attemptKey(id, planned.cell.key, planned.n);
|
|
1916
|
-
const skip = ctx.completed?.includes(key) ? "already completed in this run" : void 0;
|
|
1917
|
-
const attempt = {
|
|
1918
|
-
name,
|
|
1919
|
-
groups,
|
|
1920
|
-
tags: resolved.tags,
|
|
1921
|
-
cell: planned.cell,
|
|
1922
|
-
n: planned.n,
|
|
1923
|
-
label: planned.label,
|
|
1924
|
-
harness: planned.harness,
|
|
1925
|
-
sandbox: planned.sandbox,
|
|
1926
|
-
setBy,
|
|
1927
|
-
gate: planned.gate,
|
|
1928
|
-
...planned.minLift !== void 0 ? { minLift: planned.minLift } : {},
|
|
1929
|
-
brokered: brokeredIn(resolved.env, planned),
|
|
1930
|
-
secretVars: secretVarsIn(resolved.env, planned),
|
|
1931
|
-
egress: planned.egress,
|
|
1932
|
-
...workspace ? { workspace } : {},
|
|
1933
|
-
...files.length ? { files } : {},
|
|
1934
|
-
plan: skip ? {
|
|
1935
|
-
...plan,
|
|
1936
|
-
skip
|
|
1937
|
-
} : plan,
|
|
1938
|
-
...skip ? { skip } : {},
|
|
1939
|
-
...family ? { family } : {}
|
|
1940
|
-
};
|
|
1941
|
-
return {
|
|
1942
|
-
name: `${name} [${attemptId(planned.cell.key, planned.n)}]`,
|
|
1943
|
-
mode: skip ? "skip" : mode,
|
|
1944
|
-
attempt,
|
|
1945
|
-
timeout: skip ? void 0 : timeoutMs + setupAllowance(planned) + GRACE_MS,
|
|
1946
|
-
gate: skip ? void 0 : {
|
|
1947
|
-
limits: attemptLimits(planned, ctx.concurrency),
|
|
1948
|
-
rank: {
|
|
1949
|
-
eval: id,
|
|
1950
|
-
file: rankFile,
|
|
1951
|
-
order: ctx.order,
|
|
1952
|
-
n: planned.n,
|
|
1953
|
-
cell: cells.indexOf(planned.cell.key)
|
|
1954
|
-
}
|
|
1955
|
-
},
|
|
1956
|
-
spec: skip ? void 0 : {
|
|
1957
|
-
...planned,
|
|
1958
|
-
name,
|
|
1959
|
-
groups,
|
|
1960
|
-
timeoutMs,
|
|
1961
|
-
env: resolved.env,
|
|
1962
|
-
judge: resolved.judge,
|
|
1963
|
-
capture: resolved.capture,
|
|
1964
|
-
sources: {
|
|
1965
|
-
...resolved.sources,
|
|
1966
|
-
sandbox: setBy.sandbox
|
|
1967
|
-
},
|
|
1968
|
-
notes: resolvedEval.notes
|
|
1969
|
-
}
|
|
1970
|
-
};
|
|
1971
|
-
});
|
|
1972
|
-
}
|
|
1973
|
-
/** The run-wide cap, plus the sandbox provider's and harness's own caps when they set one. */
|
|
1974
|
-
function attemptLimits(planned, concurrency) {
|
|
1975
|
-
const limits = { attempts: concurrency };
|
|
1976
|
-
const { sandbox, harness } = planned;
|
|
1977
|
-
if (sandbox.maxConcurrent) limits[`sandbox:${sandbox.kind}`] = sandbox.maxConcurrent;
|
|
1978
|
-
if (harness.maxConcurrent) limits[`harness:${harness.kind}`] = harness.maxConcurrent;
|
|
1979
|
-
return limits;
|
|
1980
|
-
}
|
|
1981
|
-
//#endregion
|
|
1982
|
-
//#region src/evaluate.ts
|
|
1983
|
-
let groupStack = [];
|
|
1984
|
-
/** Each eval file's declarations as `evaluate` and `group` were called, and each eval's body by id. */
|
|
1985
|
-
const trees = /* @__PURE__ */ new Map();
|
|
1986
|
-
const bodies = /* @__PURE__ */ new Map();
|
|
1987
|
-
/** Options as the tree holds them; from TypeScript they may still hold function forms. */
|
|
1988
|
-
const data = (options) => options;
|
|
1989
|
-
function declare(node) {
|
|
1990
|
-
const parent = groupStack.at(-1)?.node;
|
|
1991
|
-
if (parent) return void parent.children.push(node);
|
|
1992
|
-
const file = collectingFile() ?? "";
|
|
1993
|
-
const tree = trees.get(file) ?? { children: [] };
|
|
1994
|
-
trees.set(file, tree);
|
|
1995
|
-
tree.children.push(node);
|
|
1996
|
-
}
|
|
1997
|
-
/** `(name, fn)`, `(name, options, fn)` as in Vitest, or `(name, fn, options)`. */
|
|
1998
|
-
function splitArgs(what, name, a, b) {
|
|
1999
|
-
if (typeof a === "function" && typeof b === "function") throw new UsageError(`${what}("${name}") got two functions. Options must be a plain object; helpers such as secret are only available in defineConfig and defineEnvironment callbacks. Put the secret in an environment (environment: …) or in the config's env.`);
|
|
2000
|
-
if (typeof a === "function") return [b ?? {}, a];
|
|
2001
|
-
if (typeof b === "function") return [a ?? {}, b];
|
|
2002
|
-
throw new UsageError(what === "group" ? `group("${name}") has no function. Pass the group's body, e.g. group("${name}", () => { … }).` : `${what}("${name}") has no function. Pass the eval's body, e.g. ${what}("${name}", async (c) => { … }).`);
|
|
2003
|
-
}
|
|
2004
|
-
/** `evaluate.only` and `group.only` are refused in CI, where they'd silently skip the rest. */
|
|
2005
|
-
function checkOnly(mode, what) {
|
|
2006
|
-
if (mode !== "only" || !inCI()) return;
|
|
2007
|
-
const error = /* @__PURE__ */ new Error(`${what}.only isn't allowed in CI`);
|
|
2008
|
-
error.name = "OnlyInCiError";
|
|
2009
|
-
throw error;
|
|
2010
|
-
}
|
|
2011
|
-
const defineGroup = (mode) => (name, a, b) => {
|
|
2012
|
-
checkOnly(mode, "group");
|
|
2013
|
-
const [options, fn] = splitArgs("group", name, a, b);
|
|
2014
|
-
checkOptions(options, `group("${name}")`);
|
|
2015
|
-
const node = {
|
|
2016
|
-
type: "group",
|
|
2017
|
-
name,
|
|
2018
|
-
mode,
|
|
2019
|
-
options: data(options),
|
|
2020
|
-
children: []
|
|
2021
|
-
};
|
|
2022
|
-
declare(node);
|
|
2023
|
-
const stack = [...groupStack, {
|
|
2024
|
-
name,
|
|
2025
|
-
options,
|
|
2026
|
-
node
|
|
2027
|
-
}];
|
|
2028
|
-
registerGroup(name, mode, () => {
|
|
2029
|
-
const outer = groupStack;
|
|
2030
|
-
groupStack = stack;
|
|
2031
|
-
try {
|
|
2032
|
-
fn();
|
|
2033
|
-
} finally {
|
|
2034
|
-
groupStack = outer;
|
|
2035
|
-
}
|
|
2036
|
-
});
|
|
2037
|
-
};
|
|
2038
|
-
const group = Object.assign(defineGroup("run"), {
|
|
2039
|
-
skip: defineGroup("skip"),
|
|
2040
|
-
only: defineGroup("only"),
|
|
2041
|
-
todo: (name) => registerTodo(name)
|
|
2042
|
-
});
|
|
2043
|
-
/**
|
|
2044
|
-
* A placeholder, `evaluate.todo` or `group.todo`: one eval entry that never runs, counted as
|
|
2045
|
-
* skipped and shown as todo. The enclosing groups' tags filter it as they do an eval.
|
|
2046
|
-
*/
|
|
2047
|
-
function registerTodo(name) {
|
|
2048
|
-
declare({
|
|
2049
|
-
type: "todo",
|
|
2050
|
-
name
|
|
2051
|
-
});
|
|
2052
|
-
const tags = [...new Set(groupStack.flatMap((g) => g.options.tags ?? []))];
|
|
2053
|
-
if (matchesTags(tags, getRunContext().tags)) registerTest(name, { mode: "todo" }, async () => {});
|
|
2054
|
-
}
|
|
2055
|
-
/** The evals declared so far, by file and name path, so a second one with the same id is refused. */
|
|
2056
|
-
const declared = /* @__PURE__ */ new Set();
|
|
2057
|
-
function checkUnique(where, groups, name, hint) {
|
|
2058
|
-
const key = `${collectingFile() ?? ""}\0${[...groups, name].join("\0")}`;
|
|
2059
|
-
if (declared.has(key)) throw new UsageError(`${where} is declared twice in this file. Each eval needs its own name, or its attempts are merged into one eval.${hint}`);
|
|
2060
|
-
declared.add(key);
|
|
2061
|
-
}
|
|
2062
|
-
function register(mode, name, own, fn, hint = "", family) {
|
|
2063
|
-
const groups = groupStack.map((g) => g.name);
|
|
2064
|
-
checkUnique(`evaluate("${[...groups, name].join(" › ")}")`, groups, name, hint);
|
|
2065
|
-
const body = `b${bodies.size + 1}`;
|
|
2066
|
-
bodies.set(body, fn);
|
|
2067
|
-
declare({
|
|
2068
|
-
type: "eval",
|
|
2069
|
-
name,
|
|
2070
|
-
mode,
|
|
2071
|
-
options: data(own),
|
|
2072
|
-
body,
|
|
2073
|
-
...family ? { family } : {}
|
|
2074
|
-
});
|
|
2075
|
-
const tests = planEval({
|
|
2076
|
-
name,
|
|
2077
|
-
mode,
|
|
2078
|
-
groups: groupStack.map(({ name, options }) => ({
|
|
2079
|
-
name,
|
|
2080
|
-
options
|
|
2081
|
-
})),
|
|
2082
|
-
options: own,
|
|
2083
|
-
...family ? { family } : {}
|
|
2084
|
-
}, {
|
|
2085
|
-
...getRunContext(),
|
|
2086
|
-
file: collectingFile(),
|
|
2087
|
-
order: registered
|
|
2088
|
-
});
|
|
2089
|
-
if (!tests) return;
|
|
2090
|
-
registered++;
|
|
2091
|
-
for (const { name: testName, spec, ...options } of tests) registerTest(testName, options, spec ? (handle) => runAttempt(spec, fn, handle) : async () => {});
|
|
2092
|
-
}
|
|
2093
|
-
/** Evals registered so far in this worker, for the scheduler's tie-break. */
|
|
2094
|
-
let registered = 0;
|
|
2095
|
-
const defineEval = (mode) => (name, a, b) => {
|
|
2096
|
-
checkOnly(mode, "evaluate");
|
|
2097
|
-
const [own, fn] = splitArgs("evaluate", name, a, b);
|
|
2098
|
-
register(mode, name, own, fn);
|
|
2099
|
-
};
|
|
2100
|
-
/** `$key` is a property of the case, `%s` the case itself, `%#` its index. */
|
|
2101
|
-
function formatName(template, item, index) {
|
|
2102
|
-
return template.replace(/%#/g, String(index)).replace(/%s/g, typeof item === "string" ? item : JSON.stringify(item)).replace(/\$([A-Za-z_][A-Za-z0-9_]*)/g, (whole, key) => typeof item === "object" && item !== null && key in item ? String(item[key]) : whole);
|
|
2103
|
-
}
|
|
2104
|
-
function checkEachOptions(options) {
|
|
2105
|
-
if (options === void 0) return {};
|
|
2106
|
-
const keys = typeof options === "object" && options !== null ? Object.keys(options) : null;
|
|
2107
|
-
const pool = options?.pool;
|
|
2108
|
-
if (keys?.every((k) => k === "pool") && (pool === void 0 || typeof pool === "boolean")) return options;
|
|
2109
|
-
throw new UsageError(`evaluate.each(cases, options) takes { pool?: boolean } (got ${JSON.stringify(options)}). Eval options such as compare go after the name: evaluate.each(cases)(name, { compare }, fn).`);
|
|
2110
|
-
}
|
|
2111
|
-
/**
|
|
2112
|
-
* One eval per case, pooled into one suite:
|
|
2113
|
-
* `evaluate.each(cases)('adds $a and $b', async (c, { a, b }) => …)`.
|
|
2114
|
-
*/
|
|
2115
|
-
const evalEach = (cases, options) => (name, a, b) => {
|
|
2116
|
-
const { pool = true } = checkEachOptions(options);
|
|
2117
|
-
if (!Array.isArray(cases)) throw new UsageError(`evaluate.each takes an array of cases (got ${typeof cases}), e.g. evaluate.each([{ n: 1 }, { n: 2 }])("case $n", async (c, item) => { … }).`);
|
|
2118
|
-
const [own, fn] = splitArgs("evaluate.each", name, a, b);
|
|
2119
|
-
const hint = ` For evaluate.each, put $key, %s or %# in the name ("${name}") so each case's name differs.`;
|
|
2120
|
-
const pooled = pool && cases.length >= 2;
|
|
2121
|
-
cases.forEach((item, i) => {
|
|
2122
|
-
const family = pooled ? {
|
|
2123
|
-
name,
|
|
2124
|
-
case: i,
|
|
2125
|
-
cases: cases.length
|
|
2126
|
-
} : void 0;
|
|
2127
|
-
register("run", formatName(name, item, i), own, (c) => fn(c, item), hint, family);
|
|
2128
|
-
});
|
|
2129
|
-
};
|
|
2130
|
-
const evaluate = Object.assign(defineEval("run"), {
|
|
2131
|
-
skip: defineEval("skip"),
|
|
2132
|
-
only: defineEval("only"),
|
|
2133
|
-
todo: registerTodo,
|
|
2134
|
-
each: evalEach
|
|
2135
|
-
});
|
|
2136
|
-
//#endregion
|
|
1
|
+
import { t as HarnessCapabilityError } from "./assert-EUIoRdIt.js";
|
|
2
|
+
import { c as defineEnvironment, n as DEFAULT_SKILL_EXCLUDE, t as DEFAULT_DIRECTORY_IGNORES } from "./environment-DNjRk0nU.js";
|
|
3
|
+
import { s as defineConfig } from "./config-DEgnUiU-.js";
|
|
4
|
+
import { t as DEFAULT_EGRESS_ALLOW } from "./egress-C4PZTSSe.js";
|
|
5
|
+
import { l as expect } from "./runtime-PX_oXiCS.js";
|
|
6
|
+
import { n as evaluate, r as group } from "./evaluate-D4zeRGBF.js";
|
|
2137
7
|
export { DEFAULT_DIRECTORY_IGNORES, DEFAULT_EGRESS_ALLOW, DEFAULT_SKILL_EXCLUDE, HarnessCapabilityError, defineConfig, defineEnvironment, evaluate, expect, group };
|
|
2138
|
-
|
|
2139
|
-
//# sourceMappingURL=index.js.map
|