zidane 7.2.4 → 8.0.4
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/LICENSE +21 -0
- package/README.md +34 -6
- package/dist/{acp-D-tVyUjw.js → acp-DQDhi2dj.js} +230 -77
- package/dist/acp-DQDhi2dj.js.map +1 -0
- package/dist/acp-cli.d.ts +1 -1
- package/dist/acp-cli.js +9 -66
- package/dist/acp-cli.js.map +1 -1
- package/dist/acp.d.ts +10 -590
- package/dist/acp.d.ts.map +1 -1
- package/dist/acp.js +3 -3
- package/dist/{agent-D0zZnGbt.js → agent-DSLNmBp0.js} +14401 -13214
- package/dist/agent-DSLNmBp0.js.map +1 -0
- package/dist/agent.d.ts +1 -1
- package/dist/agent.js +3 -2
- package/dist/aliasing-lVFbm81n.js +217 -0
- package/dist/aliasing-lVFbm81n.js.map +1 -0
- package/dist/{anthropic-C2aole0G.js → anthropic-CD00LhUP.js} +1442 -1264
- package/dist/anthropic-CD00LhUP.js.map +1 -0
- package/dist/artifacts-CV2hUKnL.js.map +1 -1
- package/dist/{atomic-write-B7zr1ddF.js → atomic-write-Ijgf2Mhc.js} +13 -3
- package/dist/atomic-write-Ijgf2Mhc.js.map +1 -0
- package/dist/call-tool-dHlbGOIY.js +70 -0
- package/dist/call-tool-dHlbGOIY.js.map +1 -0
- package/dist/chat/pure.d.ts +372 -6
- package/dist/chat/pure.d.ts.map +1 -1
- package/dist/chat/pure.js +35 -244
- package/dist/chat/pure.js.map +1 -1
- package/dist/{approvals-DKaJZYlm.js → chat-JPJJ8uu5.js} +15879 -14631
- package/dist/chat-JPJJ8uu5.js.map +1 -0
- package/dist/chat.d.ts +4 -6
- package/dist/chat.js +23 -12
- package/dist/cli-shared-C-YCaglL.js +159 -0
- package/dist/cli-shared-C-YCaglL.js.map +1 -0
- package/dist/client-core-BQSyDALT.d.ts +2115 -0
- package/dist/client-core-BQSyDALT.d.ts.map +1 -0
- package/dist/codemode-worker.d.ts +1 -0
- package/dist/codemode-worker.js +155 -0
- package/dist/codemode-worker.js.map +1 -0
- package/dist/codemode.d.ts +352 -0
- package/dist/codemode.d.ts.map +1 -0
- package/dist/codemode.js +5 -0
- package/dist/connection-Dr-Js4yZ.d.ts +28 -0
- package/dist/connection-Dr-Js4yZ.d.ts.map +1 -0
- package/dist/connection-loop-gdh22LTI.js +112 -0
- package/dist/connection-loop-gdh22LTI.js.map +1 -0
- package/dist/context-segments-BHKlagL6.js +1016 -0
- package/dist/context-segments-BHKlagL6.js.map +1 -0
- package/dist/{context-transport-T_6AYIvy.js → context-transport-DSMTUsK1.js} +3 -3
- package/dist/{context-transport-T_6AYIvy.js.map → context-transport-DSMTUsK1.js.map} +1 -1
- package/dist/contexts/daytona.d.ts +19 -8
- package/dist/contexts/daytona.d.ts.map +1 -1
- package/dist/contexts/daytona.js +1 -440
- package/dist/contexts/docker.d.ts +36 -7
- package/dist/contexts/docker.d.ts.map +1 -1
- package/dist/contexts/docker.js +1 -456
- package/dist/contexts/e2b.d.ts +68 -13
- package/dist/contexts/e2b.d.ts.map +1 -1
- package/dist/contexts/e2b.js +1 -382
- package/dist/contexts/sandbox.d.ts +1 -1
- package/dist/contexts/sandbox.js +5 -0
- package/dist/contexts/sandbox.js.map +1 -1
- package/dist/{contexts-CRE9pw07.js → contexts-d7UolYot.js} +1059 -524
- package/dist/contexts-d7UolYot.js.map +1 -0
- package/dist/contexts.d.ts +3 -3
- package/dist/contexts.js +1 -1
- package/dist/cred-keys-GpBg71cP.js +105 -0
- package/dist/cred-keys-GpBg71cP.js.map +1 -0
- package/dist/{auth-BG2rVKSt.js → credentials-JttEuRON.js} +55 -206
- package/dist/credentials-JttEuRON.js.map +1 -0
- package/dist/{cursor-iojRXHF9.js → cursor-CdjaQTBW.js} +3 -3
- package/dist/{cursor-iojRXHF9.js.map → cursor-CdjaQTBW.js.map} +1 -1
- package/dist/daemon-cli.d.ts +1 -1
- package/dist/daemon-cli.js +3 -2
- package/dist/daemon-cli.js.map +1 -1
- package/dist/daytona-DG3omipZ.js +513 -0
- package/dist/daytona-DG3omipZ.js.map +1 -0
- package/dist/defaults-Cx3zvDOV.js +23 -0
- package/dist/defaults-Cx3zvDOV.js.map +1 -0
- package/dist/dispatcher-BMlu2BmZ.js +4941 -0
- package/dist/dispatcher-BMlu2BmZ.js.map +1 -0
- package/dist/docker-BZhMMJUp.js +727 -0
- package/dist/docker-BZhMMJUp.js.map +1 -0
- package/dist/e2b-CEVOdRib.js +480 -0
- package/dist/e2b-CEVOdRib.js.map +1 -0
- package/dist/edit-utils-EGosADZq.js +234 -0
- package/dist/edit-utils-EGosADZq.js.map +1 -0
- package/dist/environment-8wsKE2Ln.js +214 -0
- package/dist/environment-8wsKE2Ln.js.map +1 -0
- package/dist/eval-BNcvle_J.js +1595 -0
- package/dist/eval-BNcvle_J.js.map +1 -0
- package/dist/eval.d.ts +2 -2
- package/dist/eval.js +1 -1489
- package/dist/executor-Cx5-8yqi.js +621 -0
- package/dist/executor-Cx5-8yqi.js.map +1 -0
- package/dist/extension-authoring-DyGGmq9V.js +385 -0
- package/dist/extension-authoring-DyGGmq9V.js.map +1 -0
- package/dist/extensions.d.ts +2 -2
- package/dist/extensions.js +3 -3
- package/dist/{fetch-url-DX75gpL0.js → fetch-url-Ct2TjDWt.js} +4 -4
- package/dist/{fetch-url-DX75gpL0.js.map → fetch-url-Ct2TjDWt.js.map} +1 -1
- package/dist/{format-BF745pLL.js → format-C2NQEVUH.js} +39 -2
- package/dist/format-C2NQEVUH.js.map +1 -0
- package/dist/format-bytes-C7tfc3Zm.js +55 -0
- package/dist/format-bytes-C7tfc3Zm.js.map +1 -0
- package/dist/headless-C_rNUuYC.js +960 -0
- package/dist/headless-C_rNUuYC.js.map +1 -0
- package/dist/headless.d.ts +1 -1
- package/dist/headless.js +1 -413
- package/dist/{history-search-C4bk1Wz_.js → history-search-CcYzF8-0.js} +7 -3
- package/dist/history-search-CcYzF8-0.js.map +1 -0
- package/dist/history-search-worker.d.ts +1 -1
- package/dist/history-search-worker.js +1 -1
- package/dist/host-D7NrI03V.js +378 -0
- package/dist/host-D7NrI03V.js.map +1 -0
- package/dist/host-signals-BcP5QKUD.js +118 -0
- package/dist/host-signals-BcP5QKUD.js.map +1 -0
- package/dist/identity-CwfO8K-t.js +26 -0
- package/dist/identity-CwfO8K-t.js.map +1 -0
- package/dist/{index-BgN1T3lk.d.ts → index-BGe1fQT7.d.ts} +10987 -9320
- package/dist/index-BGe1fQT7.d.ts.map +1 -0
- package/dist/index-BVQsIKGn.d.ts +448 -0
- package/dist/index-BVQsIKGn.d.ts.map +1 -0
- package/dist/{index-C6NEd5y_.d.ts → index-CHABJd-L.d.ts} +3386 -502
- package/dist/index-CHABJd-L.d.ts.map +1 -0
- package/dist/{index-C37_ppWv.d.ts → index-D6a7qXpg.d.ts} +2 -3
- package/dist/index-D6a7qXpg.d.ts.map +1 -0
- package/dist/index.d.ts +6 -6
- package/dist/index.js +27 -23
- package/dist/{completion-core-BRo86R_L.js → inline-ghost-BVRQeGoL.js} +50 -2
- package/dist/inline-ghost-BVRQeGoL.js.map +1 -0
- package/dist/ipc/remote.d.ts +313 -0
- package/dist/ipc/remote.d.ts.map +1 -0
- package/dist/ipc/remote.js +4 -0
- package/dist/ipc.d.ts +6 -964
- package/dist/ipc.js +403 -4
- package/dist/ipc.js.map +1 -0
- package/dist/{isolation-Kfoof0MM.d.ts → isolation-D9L650W5.d.ts} +100 -15
- package/dist/isolation-D9L650W5.d.ts.map +1 -0
- package/dist/json-rpc--HXQti1E.js +350 -0
- package/dist/json-rpc--HXQti1E.js.map +1 -0
- package/dist/links-j8rED1sR.js +603 -0
- package/dist/links-j8rED1sR.js.map +1 -0
- package/dist/local-terminal-D8QThwo_.js +252 -0
- package/dist/local-terminal-D8QThwo_.js.map +1 -0
- package/dist/{logger--C1VQjwJ.js → logger-Gtqlayx8.js} +199 -173
- package/dist/logger-Gtqlayx8.js.map +1 -0
- package/dist/logs-view-CWG8aUAG.js.map +1 -1
- package/dist/{mcp-CgNd-f3F.js → mcp-_wecH9Qe.js} +608 -562
- package/dist/mcp-_wecH9Qe.js.map +1 -0
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.js +2 -2
- package/dist/media-sniff-UKijEaiQ.js.map +1 -1
- package/dist/{messages-2jcOA_5R.js → messages-BqiwcD2Q.js} +1017 -1014
- package/dist/messages-BqiwcD2Q.js.map +1 -0
- package/dist/{oauth-DxHO17JE.js → oauth-Px_pBB9W.js} +5 -5
- package/dist/{oauth-DxHO17JE.js.map → oauth-Px_pBB9W.js.map} +1 -1
- package/dist/{openai-compat-Dy56weK8.js → openai-compat-CFrpH7uq.js} +432 -589
- package/dist/openai-compat-CFrpH7uq.js.map +1 -0
- package/dist/orchestration-BCGmB8Ps.js +511 -0
- package/dist/orchestration-BCGmB8Ps.js.map +1 -0
- package/dist/output/stream-json.d.ts +1 -2
- package/dist/output/stream-json.d.ts.map +1 -1
- package/dist/output/stream-json.js +1 -1
- package/dist/output/terminal.d.ts +1 -2
- package/dist/output/terminal.d.ts.map +1 -1
- package/dist/output/terminal.js +13 -7
- package/dist/output/terminal.js.map +1 -1
- package/dist/{plugins-gbI7SBA6.js → plugins-DS7mahzZ.js} +20 -101
- package/dist/plugins-DS7mahzZ.js.map +1 -0
- package/dist/plugins.d.ts +1 -1
- package/dist/plugins.js +1 -2
- package/dist/prelude-Cxo4t74B.js +517 -0
- package/dist/prelude-Cxo4t74B.js.map +1 -0
- package/dist/presets.d.ts +1 -1
- package/dist/presets.js +1 -2
- package/dist/project-root-B8zADr4o.js +167 -0
- package/dist/project-root-B8zADr4o.js.map +1 -0
- package/dist/project-user-paths-BSgtrRxl.js +58 -0
- package/dist/project-user-paths-BSgtrRxl.js.map +1 -0
- package/dist/{prompts-Pqc3JdW_.d.ts → prompts-DYcvqShf.d.ts} +14 -11
- package/dist/prompts-DYcvqShf.d.ts.map +1 -0
- package/dist/protocol-DnhqqSmE.js +13 -0
- package/dist/protocol-DnhqqSmE.js.map +1 -0
- package/dist/providers/anthropic.d.ts +2 -2
- package/dist/providers/anthropic.js +1 -2
- package/dist/providers/arcee.d.ts +1 -1
- package/dist/providers/arcee.js +1 -1
- package/dist/providers/baseten.d.ts +1 -1
- package/dist/providers/baseten.js +1 -1
- package/dist/providers/cerebras.d.ts +1 -1
- package/dist/providers/cerebras.js +3 -2
- package/dist/providers/cerebras.js.map +1 -1
- package/dist/providers/local.d.ts +1 -1
- package/dist/providers/local.js +1 -1
- package/dist/providers/openai-compat.d.ts +1 -1
- package/dist/providers/openai-compat.js +1 -2
- package/dist/providers/openai.d.ts +1 -1
- package/dist/providers/openai.js +15 -12
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/openrouter.d.ts +1 -1
- package/dist/providers/openrouter.js +3 -2
- package/dist/providers/openrouter.js.map +1 -1
- package/dist/providers/xai.d.ts +1 -1
- package/dist/providers/xai.js +3 -3
- package/dist/{providers-BVhnFyiY.js → providers-XUR7F4Hi.js} +4 -4
- package/dist/{providers-BVhnFyiY.js.map → providers-XUR7F4Hi.js.map} +1 -1
- package/dist/providers.d.ts +1 -1
- package/dist/providers.js +4 -4
- package/dist/registry-D6ShBWJU.js +5234 -0
- package/dist/registry-D6ShBWJU.js.map +1 -0
- package/dist/remote-hosts-BtA_9ZGp.js +97 -0
- package/dist/remote-hosts-BtA_9ZGp.js.map +1 -0
- package/dist/restate.d.ts +2 -3
- package/dist/restate.d.ts.map +1 -1
- package/dist/restate.js +631 -572
- package/dist/restate.js.map +1 -1
- package/dist/run-summary-CklLiZ4y.js +288 -0
- package/dist/run-summary-CklLiZ4y.js.map +1 -0
- package/dist/{sandbox-BH0WQkXn.d.ts → sandbox-C5dR5DwQ.d.ts} +6 -3
- package/dist/sandbox-C5dR5DwQ.d.ts.map +1 -0
- package/dist/session/sqlite.d.ts +1 -2
- package/dist/session/sqlite.d.ts.map +1 -1
- package/dist/session/sqlite.js +1 -616
- package/dist/session-factory-BUGY5P40.js +1576 -0
- package/dist/session-factory-BUGY5P40.js.map +1 -0
- package/dist/{session-BqRP1hS7.js → session-xh9JXu6i.js} +94 -52
- package/dist/session-xh9JXu6i.js.map +1 -0
- package/dist/session.d.ts +1 -1
- package/dist/session.js +2 -2
- package/dist/settings-keys-Ckf29NeM.js +677 -0
- package/dist/settings-keys-Ckf29NeM.js.map +1 -0
- package/dist/shell-path-Dqaoow2J.js +412 -0
- package/dist/shell-path-Dqaoow2J.js.map +1 -0
- package/dist/{skills-BC3dodWp.js → skills-DKqZJm4X.js} +430 -470
- package/dist/skills-DKqZJm4X.js.map +1 -0
- package/dist/skills.d.ts +2 -2
- package/dist/skills.js +1 -2
- package/dist/source-mPExaUm8.js +381 -0
- package/dist/source-mPExaUm8.js.map +1 -0
- package/dist/sqlite-DV1DQzN2.js +698 -0
- package/dist/sqlite-DV1DQzN2.js.map +1 -0
- package/dist/{src-CkNQKFKr.js → src-BRMYRJib.js} +903 -1121
- package/dist/src-BRMYRJib.js.map +1 -0
- package/dist/{stdio-DPpBDPx5.js → stdio-C6YyoW3I.js} +4 -4
- package/dist/{stdio-DPpBDPx5.js.map → stdio-C6YyoW3I.js.map} +1 -1
- package/dist/{stdio-loader-Bbx19dMZ.js → stdio-loader-C830Vg-Z.js} +4 -4
- package/dist/{stdio-loader-Bbx19dMZ.js.map → stdio-loader-C830Vg-Z.js.map} +1 -1
- package/dist/system-prompt-MNVHatSD.d.ts.map +1 -1
- package/dist/timeout-DEaqslLO.d.ts.map +1 -1
- package/dist/todo-edits-Dn251YUP.js +1790 -0
- package/dist/todo-edits-Dn251YUP.js.map +1 -0
- package/dist/{turn-operations-DrbJlpq9.js → tool-formatters-IQFDrdrs.js} +1456 -2120
- package/dist/tool-formatters-IQFDrdrs.js.map +1 -0
- package/dist/tool-state-Dku6YqAB.js +989 -0
- package/dist/tool-state-Dku6YqAB.js.map +1 -0
- package/dist/tools/fetch-url.d.ts +1 -2
- package/dist/tools/fetch-url.d.ts.map +1 -1
- package/dist/tools/fetch-url.js +1 -2
- package/dist/tools/prompts.d.ts +1 -1
- package/dist/tools/prompts.js +7 -7
- package/dist/tools/prompts.js.map +1 -1
- package/dist/tools/web-search.d.ts +1 -2
- package/dist/tools/web-search.d.ts.map +1 -1
- package/dist/tools/web-search.js +2 -2
- package/dist/tools/web-search.js.map +1 -1
- package/dist/tools-_DnT3AGM.js +578 -0
- package/dist/tools-_DnT3AGM.js.map +1 -0
- package/dist/tools.d.ts +3 -3
- package/dist/tools.js +4 -2
- package/dist/tui.d.ts +534 -517
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +34933 -27242
- package/dist/tui.js.map +1 -1
- package/dist/turn-operations-B8sokwi5.js +333 -0
- package/dist/turn-operations-B8sokwi5.js.map +1 -0
- package/dist/turn-operations-DQX5ZZni.d.ts +3089 -0
- package/dist/turn-operations-DQX5ZZni.d.ts.map +1 -0
- package/dist/types-C7ZFOztE.d.ts +132 -0
- package/dist/types-C7ZFOztE.d.ts.map +1 -0
- package/dist/types-CH5XpIHw.js +114 -0
- package/dist/types-CH5XpIHw.js.map +1 -0
- package/dist/{types-CWgENVLY.js → types-Cu0ZGDu9.js} +10 -6
- package/dist/types-Cu0ZGDu9.js.map +1 -0
- package/dist/types-IqegbJEE.d.ts +624 -0
- package/dist/types-IqegbJEE.d.ts.map +1 -0
- package/dist/types.d.ts +4 -4
- package/dist/types.js +1 -1
- package/dist/url-D8HCl65b.js +1259 -0
- package/dist/url-D8HCl65b.js.map +1 -0
- package/dist/wasm-BVT-j5cl.js +33 -0
- package/dist/wasm-BVT-j5cl.js.map +1 -0
- package/dist/workspace-exec-stream-SV2R9d5S.js +355 -0
- package/dist/workspace-exec-stream-SV2R9d5S.js.map +1 -0
- package/dist/ws-CIHMvlSh.js +952 -0
- package/dist/ws-CIHMvlSh.js.map +1 -0
- package/dist/ws-client-N7NxzvQp.js +230 -0
- package/dist/ws-client-N7NxzvQp.js.map +1 -0
- package/dist/{xai-CazjStOK.js → xai-CvJTeLrI.js} +165 -5
- package/dist/xai-CvJTeLrI.js.map +1 -0
- package/dist/xdg-zweed0-t.js +117 -0
- package/dist/xdg-zweed0-t.js.map +1 -0
- package/docs/ACP.md +52 -0
- package/docs/ARCHITECTURE.md +103 -6
- package/docs/ARTIFACTS.md +8 -4
- package/docs/CHAT.md +165 -14
- package/docs/EXECUTION_CONTEXT.md +8 -0
- package/docs/EXTENSIONS.md +123 -2
- package/docs/HEADLESS.md +2 -0
- package/docs/IPC.md +775 -64
- package/docs/MACHINES.md +456 -0
- package/docs/PANELS.md +3 -0
- package/docs/README.md +4 -0
- package/docs/RESTATE.md +3 -2
- package/docs/RUN_IN_BACKGROUND.md +10 -2
- package/docs/SERVER.md +1085 -0
- package/docs/SKILL.md +6 -1
- package/docs/TERMINAL.md +75 -0
- package/docs/TUI.md +65 -14
- package/package.json +58 -26
- package/scripts/eval.ts +4 -0
- package/dist/acp-D-tVyUjw.js.map +0 -1
- package/dist/agent-D0zZnGbt.js.map +0 -1
- package/dist/anthropic-C2aole0G.js.map +0 -1
- package/dist/approvals-DKaJZYlm.js.map +0 -1
- package/dist/atomic-write-B7zr1ddF.js.map +0 -1
- package/dist/auth-BG2rVKSt.js.map +0 -1
- package/dist/completion-core-BRo86R_L.js.map +0 -1
- package/dist/contexts/daytona.js.map +0 -1
- package/dist/contexts/docker.js.map +0 -1
- package/dist/contexts/e2b.js.map +0 -1
- package/dist/contexts-CRE9pw07.js.map +0 -1
- package/dist/eval.js.map +0 -1
- package/dist/format-BF745pLL.js.map +0 -1
- package/dist/glob-DluQFSw1.js +0 -270
- package/dist/glob-DluQFSw1.js.map +0 -1
- package/dist/headless.js.map +0 -1
- package/dist/history-search-C4bk1Wz_.js.map +0 -1
- package/dist/index-BgN1T3lk.d.ts.map +0 -1
- package/dist/index-C37_ppWv.d.ts.map +0 -1
- package/dist/index-C6NEd5y_.d.ts.map +0 -1
- package/dist/ipc-zVSyo6K1.js +0 -331
- package/dist/ipc-zVSyo6K1.js.map +0 -1
- package/dist/ipc.d.ts.map +0 -1
- package/dist/isolation-Kfoof0MM.d.ts.map +0 -1
- package/dist/json-rpc-rSWMUh9b.js +0 -192
- package/dist/json-rpc-rSWMUh9b.js.map +0 -1
- package/dist/logger--C1VQjwJ.js.map +0 -1
- package/dist/mcp-CgNd-f3F.js.map +0 -1
- package/dist/messages-2jcOA_5R.js.map +0 -1
- package/dist/openai-compat-Dy56weK8.js.map +0 -1
- package/dist/output-cap-C9zukcCj.js +0 -100
- package/dist/output-cap-C9zukcCj.js.map +0 -1
- package/dist/panels-B6gRk4bf.js +0 -789
- package/dist/panels-B6gRk4bf.js.map +0 -1
- package/dist/plugins-gbI7SBA6.js.map +0 -1
- package/dist/prompts-Pqc3JdW_.d.ts.map +0 -1
- package/dist/run-summary-BU_NgwUT.js +0 -269
- package/dist/run-summary-BU_NgwUT.js.map +0 -1
- package/dist/sandbox-BH0WQkXn.d.ts.map +0 -1
- package/dist/session/sqlite.js.map +0 -1
- package/dist/session-BqRP1hS7.js.map +0 -1
- package/dist/shell-path-h5czzllR.js +0 -1549
- package/dist/shell-path-h5czzllR.js.map +0 -1
- package/dist/skills-BC3dodWp.js.map +0 -1
- package/dist/src-CkNQKFKr.js.map +0 -1
- package/dist/todo-edits-UOwgxGKu.js +0 -528
- package/dist/todo-edits-UOwgxGKu.js.map +0 -1
- package/dist/tool-formatters-DoRLnfuD.d.ts +0 -759
- package/dist/tool-formatters-DoRLnfuD.d.ts.map +0 -1
- package/dist/transcript-anchors-BeudMbXx.d.ts +0 -1762
- package/dist/transcript-anchors-BeudMbXx.d.ts.map +0 -1
- package/dist/turn-operations-C8bMBDl_.d.ts +0 -1258
- package/dist/turn-operations-C8bMBDl_.d.ts.map +0 -1
- package/dist/turn-operations-DrbJlpq9.js.map +0 -1
- package/dist/types-CWgENVLY.js.map +0 -1
- package/dist/xai-CazjStOK.js.map +0 -1
|
@@ -0,0 +1,1595 @@
|
|
|
1
|
+
import { t as alwaysQuote } from "./shell-quote-BmnhZmdM.js";
|
|
2
|
+
import { d as createMemoryStore, t as createSession } from "./session-xh9JXu6i.js";
|
|
3
|
+
import { t as createProcessContext } from "./contexts-d7UolYot.js";
|
|
4
|
+
import { a as headlessEventToJsonl, c as runHeadless } from "./headless-C_rNUuYC.js";
|
|
5
|
+
import { join, relative, resolve } from "node:path";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
8
|
+
import chalk from "chalk";
|
|
9
|
+
//#region src/eval-artifacts.ts
|
|
10
|
+
async function writeEvalArtifacts(dir, result) {
|
|
11
|
+
await mkdir(dir, { recursive: true });
|
|
12
|
+
const resultPath = join(dir, "result.json");
|
|
13
|
+
const eventsPath = join(dir, "events.jsonl");
|
|
14
|
+
const transcriptPath = join(dir, "transcript.json");
|
|
15
|
+
const workspacePath = result.workspace ? join(dir, "workspace.json") : void 0;
|
|
16
|
+
await writeFile(resultPath, `${JSON.stringify(result, null, 2)}\n`);
|
|
17
|
+
await writeFile(eventsPath, result.events.map(headlessEventToJsonl).join(""));
|
|
18
|
+
await writeFile(transcriptPath, `${JSON.stringify(result.result.transcript, null, 2)}\n`);
|
|
19
|
+
if (workspacePath) await writeFile(workspacePath, `${JSON.stringify(result.workspace, null, 2)}\n`);
|
|
20
|
+
return {
|
|
21
|
+
dir,
|
|
22
|
+
result: resultPath,
|
|
23
|
+
events: eventsPath,
|
|
24
|
+
transcript: transcriptPath,
|
|
25
|
+
...workspacePath ? { workspace: workspacePath } : {}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function safeSegment(value) {
|
|
29
|
+
const sanitized = value.replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
30
|
+
if (sanitized === value && sanitized.length > 0) return sanitized;
|
|
31
|
+
return `${sanitized || "eval"}-${shortHash(value)}`;
|
|
32
|
+
}
|
|
33
|
+
function shortHash(value) {
|
|
34
|
+
let hash = 2166136261;
|
|
35
|
+
for (let i = 0; i < value.length; i++) {
|
|
36
|
+
hash ^= value.charCodeAt(i);
|
|
37
|
+
hash = Math.imul(hash, 16777619);
|
|
38
|
+
}
|
|
39
|
+
return (hash >>> 0).toString(36).padStart(7, "0").slice(0, 7);
|
|
40
|
+
}
|
|
41
|
+
function artifactPath(root, result) {
|
|
42
|
+
return resolve(root, safeSegment(result.suite ?? "eval"), safeSegment(result.id));
|
|
43
|
+
}
|
|
44
|
+
function relativeArtifactPath(root, path) {
|
|
45
|
+
return relative(root, path);
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region src/eval-metrics.ts
|
|
49
|
+
/**
|
|
50
|
+
* Declare an eval's metric set. Returns the same map, typed; pass it to
|
|
51
|
+
* `EvalCaseOptions.metrics`. Declared metrics that are not emitted are recorded
|
|
52
|
+
* as missing with a normalized score of 0; emitting an undeclared metric is an
|
|
53
|
+
* authoring error and throws.
|
|
54
|
+
*/
|
|
55
|
+
function defineMetrics(spec) {
|
|
56
|
+
return spec;
|
|
57
|
+
}
|
|
58
|
+
/** Always-available efficiency metrics derived from `HeadlessResult`. */
|
|
59
|
+
const EFFICIENCY_METRICS = {
|
|
60
|
+
"execution-time": {
|
|
61
|
+
min: 0,
|
|
62
|
+
max: 3e5,
|
|
63
|
+
direction: "lower-is-better",
|
|
64
|
+
tags: ["@efficiency"],
|
|
65
|
+
description: "Wall-clock run time (ms)."
|
|
66
|
+
},
|
|
67
|
+
"provider-tokens": {
|
|
68
|
+
min: 0,
|
|
69
|
+
max: 2e5,
|
|
70
|
+
direction: "lower-is-better",
|
|
71
|
+
tags: ["@efficiency"],
|
|
72
|
+
description: "Input + output tokens."
|
|
73
|
+
},
|
|
74
|
+
"cache-read-rate": {
|
|
75
|
+
min: 0,
|
|
76
|
+
max: 1,
|
|
77
|
+
direction: "higher-is-better",
|
|
78
|
+
tags: ["@efficiency"],
|
|
79
|
+
description: "Cache-read tokens / input tokens."
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
function normalizeMetric(raw, spec) {
|
|
83
|
+
const span = spec.max - spec.min;
|
|
84
|
+
if (span === 0) return 1;
|
|
85
|
+
const normalized = (Math.min(spec.max, Math.max(spec.min, raw)) - spec.min) / span;
|
|
86
|
+
return spec.direction === "lower-is-better" ? 1 - normalized : normalized;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Thrown by `runEvalCase` when a scorer emits a metric id that was not declared
|
|
90
|
+
* in `defineMetrics`. This is an authoring error (a typo'd or stale metric id),
|
|
91
|
+
* not a low score — it fails the eval test rather than being recorded as a
|
|
92
|
+
* failed scorer. Exported so downstream harnesses can type-narrow on it.
|
|
93
|
+
*/
|
|
94
|
+
var EvalMetricError = class extends Error {
|
|
95
|
+
constructor(message) {
|
|
96
|
+
super(message);
|
|
97
|
+
this.name = "EvalMetricError";
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
function efficiencyMetricValues(result) {
|
|
101
|
+
const u = result.usage;
|
|
102
|
+
const readTotal = u.input + u.cacheRead;
|
|
103
|
+
return {
|
|
104
|
+
"execution-time": result.durationMs,
|
|
105
|
+
"provider-tokens": u.input + u.output,
|
|
106
|
+
"cache-read-rate": readTotal > 0 ? u.cacheRead / readTotal : 0
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function emitEfficiencyMetrics(emit, result) {
|
|
110
|
+
const values = efficiencyMetricValues(result);
|
|
111
|
+
for (const [id, raw] of Object.entries(values)) emit(id, raw);
|
|
112
|
+
}
|
|
113
|
+
function finalizeEvalMetrics(specs, raw) {
|
|
114
|
+
return Object.entries(specs).map(([metricId, spec]) => {
|
|
115
|
+
const emitted = raw.has(metricId);
|
|
116
|
+
const value = emitted ? raw.get(metricId) : spec.min;
|
|
117
|
+
return {
|
|
118
|
+
id: metricId,
|
|
119
|
+
raw: emitted ? value : 0,
|
|
120
|
+
normalized: emitted ? normalizeMetric(value, spec) : 0,
|
|
121
|
+
direction: spec.direction,
|
|
122
|
+
min: spec.min,
|
|
123
|
+
max: spec.max,
|
|
124
|
+
tags: spec.tags ?? [],
|
|
125
|
+
...spec.description ? { description: spec.description } : {},
|
|
126
|
+
...emitted ? {} : { missing: true }
|
|
127
|
+
};
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
function computeEvalTagScores(metrics) {
|
|
131
|
+
const byTag = /* @__PURE__ */ new Map();
|
|
132
|
+
for (const metric of metrics) for (const tag of metric.tags) {
|
|
133
|
+
const list = byTag.get(tag) ?? [];
|
|
134
|
+
list.push(metric.normalized);
|
|
135
|
+
byTag.set(tag, list);
|
|
136
|
+
}
|
|
137
|
+
const out = {};
|
|
138
|
+
for (const [tag, values] of byTag) out[tag] = mean(values);
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
function meanNormalized(metrics) {
|
|
142
|
+
return metrics.length === 0 ? 0 : mean(metrics.map((m) => m.normalized));
|
|
143
|
+
}
|
|
144
|
+
function mean(values) {
|
|
145
|
+
return values.length === 0 ? 0 : values.reduce((a, b) => a + b, 0) / values.length;
|
|
146
|
+
}
|
|
147
|
+
//#endregion
|
|
148
|
+
//#region src/eval-summary.ts
|
|
149
|
+
/** Stable ordering by `variant`, then `suite/id`, so parallel completion order doesn't shuffle output. */
|
|
150
|
+
function sortCases(results) {
|
|
151
|
+
return [...results].sort((a, b) => {
|
|
152
|
+
const va = a.variant ?? "";
|
|
153
|
+
const vb = b.variant ?? "";
|
|
154
|
+
if (va !== vb) return va.localeCompare(vb);
|
|
155
|
+
const ka = `${a.suite ?? ""}/${a.id}`;
|
|
156
|
+
const kb = `${b.suite ?? ""}/${b.id}`;
|
|
157
|
+
return ka.localeCompare(kb);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
/** Distinct variant labels across results, in sorted order. */
|
|
161
|
+
function distinctVariants(results) {
|
|
162
|
+
return [...new Set(results.map((r) => r.variant).filter((v) => Boolean(v)))].sort();
|
|
163
|
+
}
|
|
164
|
+
function buildEvalRunSummary(input) {
|
|
165
|
+
const results = sortCases(input);
|
|
166
|
+
const usage = results.reduce((acc, result) => {
|
|
167
|
+
acc.input += result.result.usage.input;
|
|
168
|
+
acc.output += result.result.usage.output;
|
|
169
|
+
acc.cacheRead += result.result.usage.cacheRead;
|
|
170
|
+
acc.cacheCreation += result.result.usage.cacheCreation;
|
|
171
|
+
acc.cost += result.result.usage.cost ?? 0;
|
|
172
|
+
return acc;
|
|
173
|
+
}, {
|
|
174
|
+
input: 0,
|
|
175
|
+
output: 0,
|
|
176
|
+
cacheRead: 0,
|
|
177
|
+
cacheCreation: 0,
|
|
178
|
+
cost: 0
|
|
179
|
+
});
|
|
180
|
+
const totalScore = results.reduce((sum, result) => sum + result.score, 0);
|
|
181
|
+
return {
|
|
182
|
+
count: results.length,
|
|
183
|
+
passed: results.filter((result) => result.passed).length,
|
|
184
|
+
score: results.length > 0 ? totalScore / results.length : 0,
|
|
185
|
+
durationMs: results.reduce((sum, result) => sum + result.result.durationMs, 0),
|
|
186
|
+
usage,
|
|
187
|
+
cases: results.map((result) => ({
|
|
188
|
+
id: result.id,
|
|
189
|
+
...result.suite ? { suite: result.suite } : {},
|
|
190
|
+
...result.variant ? { variant: result.variant } : {},
|
|
191
|
+
passed: result.passed,
|
|
192
|
+
score: result.score,
|
|
193
|
+
status: result.result.status,
|
|
194
|
+
durationMs: result.result.durationMs,
|
|
195
|
+
scores: result.scores,
|
|
196
|
+
metrics: result.metrics,
|
|
197
|
+
tagScores: result.tagScores,
|
|
198
|
+
trajectory: result.trajectory
|
|
199
|
+
})),
|
|
200
|
+
metrics: aggregateMetrics(results),
|
|
201
|
+
tagScores: aggregateTagScores(results),
|
|
202
|
+
...distinctVariants(results).length > 0 ? { variants: aggregateVariants(results) } : {}
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
function aggregateVariants(results) {
|
|
206
|
+
return distinctVariants(results).map((variant) => {
|
|
207
|
+
const group = results.filter((r) => r.variant === variant);
|
|
208
|
+
const usage = group.reduce((acc, result) => {
|
|
209
|
+
acc.input += result.result.usage.input;
|
|
210
|
+
acc.output += result.result.usage.output;
|
|
211
|
+
acc.cacheRead += result.result.usage.cacheRead;
|
|
212
|
+
acc.cacheCreation += result.result.usage.cacheCreation;
|
|
213
|
+
acc.cost += result.result.usage.cost ?? 0;
|
|
214
|
+
return acc;
|
|
215
|
+
}, {
|
|
216
|
+
input: 0,
|
|
217
|
+
output: 0,
|
|
218
|
+
cacheRead: 0,
|
|
219
|
+
cacheCreation: 0,
|
|
220
|
+
cost: 0
|
|
221
|
+
});
|
|
222
|
+
return {
|
|
223
|
+
variant,
|
|
224
|
+
count: group.length,
|
|
225
|
+
passed: group.filter((r) => r.passed).length,
|
|
226
|
+
score: mean(group.map((r) => r.score)),
|
|
227
|
+
durationMs: group.reduce((sum, r) => sum + r.result.durationMs, 0),
|
|
228
|
+
usage,
|
|
229
|
+
turns: group.reduce((sum, r) => sum + r.result.turns, 0),
|
|
230
|
+
toolCalls: group.reduce((sum, r) => sum + r.result.numToolCalls, 0)
|
|
231
|
+
};
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
function aggregateMetrics(results) {
|
|
235
|
+
const byId = /* @__PURE__ */ new Map();
|
|
236
|
+
for (const result of results) for (const metric of result.metrics) {
|
|
237
|
+
const list = byId.get(metric.id) ?? [];
|
|
238
|
+
list.push(metric);
|
|
239
|
+
byId.set(metric.id, list);
|
|
240
|
+
}
|
|
241
|
+
return [...byId.entries()].map(([id, metrics]) => ({
|
|
242
|
+
id,
|
|
243
|
+
direction: metrics[0].direction,
|
|
244
|
+
tags: metrics[0].tags,
|
|
245
|
+
raw: metricStats(metrics.map((m) => m.raw)),
|
|
246
|
+
normalized: metricStats(metrics.map((m) => m.normalized))
|
|
247
|
+
}));
|
|
248
|
+
}
|
|
249
|
+
function aggregateTagScores(results) {
|
|
250
|
+
const byTag = /* @__PURE__ */ new Map();
|
|
251
|
+
for (const result of results) for (const [tag, value] of Object.entries(result.tagScores)) {
|
|
252
|
+
const list = byTag.get(tag) ?? [];
|
|
253
|
+
list.push(value);
|
|
254
|
+
byTag.set(tag, list);
|
|
255
|
+
}
|
|
256
|
+
const out = {};
|
|
257
|
+
for (const [tag, values] of byTag) out[tag] = mean(values);
|
|
258
|
+
return out;
|
|
259
|
+
}
|
|
260
|
+
function metricStats(values) {
|
|
261
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
262
|
+
return {
|
|
263
|
+
mean: mean(values),
|
|
264
|
+
min: sorted[0] ?? 0,
|
|
265
|
+
max: sorted[sorted.length - 1] ?? 0,
|
|
266
|
+
p50: percentile(sorted, .5),
|
|
267
|
+
p90: percentile(sorted, .9),
|
|
268
|
+
zeroCount: values.filter((v) => v === 0).length,
|
|
269
|
+
values
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
function percentile(sorted, q) {
|
|
273
|
+
if (sorted.length === 0) return 0;
|
|
274
|
+
return sorted[Math.min(sorted.length - 1, Math.max(0, Math.ceil(q * sorted.length) - 1))];
|
|
275
|
+
}
|
|
276
|
+
//#endregion
|
|
277
|
+
//#region src/eval-report.ts
|
|
278
|
+
function formatEvalCaseSummary(result) {
|
|
279
|
+
const lines = [
|
|
280
|
+
`Eval ${result.suite ? `${result.suite}/` : ""}${result.id}`,
|
|
281
|
+
...result.variant ? [`variant: ${result.variant}`] : [],
|
|
282
|
+
`status: ${result.result.status}`,
|
|
283
|
+
`passed: ${result.passed}`,
|
|
284
|
+
`score: ${result.score.toFixed(2)}`,
|
|
285
|
+
`duration: ${result.result.durationMs}ms`,
|
|
286
|
+
`turns: ${result.result.turns}`,
|
|
287
|
+
`tool calls: ${result.result.numToolCalls}`,
|
|
288
|
+
`usage: input=${result.result.usage.input} output=${result.result.usage.output} cacheRead=${result.result.usage.cacheRead} cacheCreation=${result.result.usage.cacheCreation}${result.result.usage.cost !== void 0 ? ` cost=$${result.result.usage.cost.toFixed(6)}` : ""}`
|
|
289
|
+
];
|
|
290
|
+
if (result.workspace) lines.push(`workspace files: ${result.workspace.files.length}`);
|
|
291
|
+
if (result.workspaceError) lines.push(`workspace error: ${result.workspaceError}`);
|
|
292
|
+
if (result.artifacts) lines.push(`artifacts: ${result.artifacts.dir}`);
|
|
293
|
+
if (result.scores.length > 0) {
|
|
294
|
+
lines.push("scores:");
|
|
295
|
+
for (const score of result.scores) {
|
|
296
|
+
const value = score.score !== void 0 ? score.score.toFixed(2) : score.passed ? "1.00" : "0.00";
|
|
297
|
+
lines.push(` - ${score.name}: ${value} ${score.passed ? "pass" : "fail"}`);
|
|
298
|
+
if (typeof score.details === "string" && score.details.length > 0) lines.push(` ${score.details}`);
|
|
299
|
+
else if (isRecord(score.details) && typeof score.details.reasoning === "string") lines.push(` ${score.details.reasoning}`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return lines.join("\n");
|
|
303
|
+
}
|
|
304
|
+
function formatEvalRunSummary(results) {
|
|
305
|
+
return formatEvalRunSummaryWithOptions(results);
|
|
306
|
+
}
|
|
307
|
+
function formatEvalRunSummaryWithOptions(input, options = {}) {
|
|
308
|
+
const results = sortCases(input);
|
|
309
|
+
const color = createEvalColors(options.color);
|
|
310
|
+
if (results.length === 0) return `${color.heading("Eval run summary")}\n${color.muted("no evals ran")}`;
|
|
311
|
+
const summary = buildEvalRunSummary(results);
|
|
312
|
+
const out = [];
|
|
313
|
+
const variants = distinctVariants(results);
|
|
314
|
+
out.push(color.heading("Eval run summary"));
|
|
315
|
+
if (variants.length > 1) {
|
|
316
|
+
out.push(renderComparisonTable(results, variants, color));
|
|
317
|
+
for (const variant of variants) {
|
|
318
|
+
const group = results.filter((r) => r.variant === variant);
|
|
319
|
+
out.push("");
|
|
320
|
+
out.push(color.heading(variant));
|
|
321
|
+
out.push(renderResultsTable(group, buildEvalRunSummary(group), color));
|
|
322
|
+
}
|
|
323
|
+
} else out.push(renderResultsTable(results, summary, color));
|
|
324
|
+
const tagTable = renderTagTable(summary.tagScores, color);
|
|
325
|
+
if (tagTable) {
|
|
326
|
+
out.push("");
|
|
327
|
+
out.push(tagTable);
|
|
328
|
+
}
|
|
329
|
+
out.push("");
|
|
330
|
+
const hyperlinks = options.color !== false;
|
|
331
|
+
const linkText = (target) => {
|
|
332
|
+
const abs = absPath(target);
|
|
333
|
+
return hyperlinks ? oscLink(abs, abs) : abs;
|
|
334
|
+
};
|
|
335
|
+
for (const result of results) {
|
|
336
|
+
const caseLabel = `${result.suite ? `${result.suite}/` : ""}${result.id}`;
|
|
337
|
+
const label = result.variant ? `[${result.variant}] ${caseLabel}` : caseLabel;
|
|
338
|
+
out.push(`${color.dot(result.passed)} ${color.caseStatus(result.passed, label)}`);
|
|
339
|
+
out.push(`${color.muted("score")} ${color.score(result.score)}`);
|
|
340
|
+
const tagEntries = Object.entries(result.tagScores);
|
|
341
|
+
if (tagEntries.length > 0) out.push(`${color.muted("tags:")} ${tagEntries.map(([tag, value]) => `${color.tag(tag)} ${color.score(value)}`).join(" ")}`);
|
|
342
|
+
for (const metric of result.metrics) {
|
|
343
|
+
const note = metric.missing ? color.fail(" (not emitted)") : color.muted(` (${formatMetricRaw(metric)})`);
|
|
344
|
+
out.push(`${color.muted(padEnd(metric.id, 28))} ${color.score(metric.normalized)}${note}`);
|
|
345
|
+
}
|
|
346
|
+
for (const score of result.scores) {
|
|
347
|
+
const detail = scoreDetailLine(score);
|
|
348
|
+
if (!score.passed && detail) out.push(`${color.dot(score.passed)} ${color.muted(`${score.name}: ${truncate(detail, 140)}`)}`);
|
|
349
|
+
}
|
|
350
|
+
if (result.sourceFile) out.push(`${color.muted("case:")} ${color.muted(linkText(result.sourceFile))}`);
|
|
351
|
+
const resultTarget = caseLinkTarget(result);
|
|
352
|
+
if (resultTarget) out.push(`${color.muted("result:")} ${color.muted(linkText(resultTarget))}`);
|
|
353
|
+
if (result.trajectory.steps.length > 0) out.push(`${color.muted(`turns: ${result.trajectory.totalBlocks}`)} ${formatTrajectoryColored(result.trajectory, color)}`);
|
|
354
|
+
out.push("");
|
|
355
|
+
}
|
|
356
|
+
return out.join("\n").replace(/\n+$/, "");
|
|
357
|
+
}
|
|
358
|
+
function renderTagTable(tagScores, color) {
|
|
359
|
+
const entries = Object.entries(tagScores);
|
|
360
|
+
if (entries.length === 0) return void 0;
|
|
361
|
+
const tagWidth = Math.max(3, ...entries.map(([tag]) => tag.length));
|
|
362
|
+
const top = `┌${"─".repeat(tagWidth + 2)}┬${"─".repeat(9)}┐`;
|
|
363
|
+
const mid = `├${"─".repeat(tagWidth + 2)}┼${"─".repeat(9)}┤`;
|
|
364
|
+
const bot = `└${"─".repeat(tagWidth + 2)}┴${"─".repeat(9)}┘`;
|
|
365
|
+
return [
|
|
366
|
+
top,
|
|
367
|
+
`│ ${color.heading(padEnd("TAG", tagWidth))} │ ${color.heading("SCORE")} │`,
|
|
368
|
+
mid,
|
|
369
|
+
...entries.map(([tag, value]) => `│ ${color.muted(padEnd(tag, tagWidth))} │ ${color.score(value)} │`),
|
|
370
|
+
bot
|
|
371
|
+
].join("\n");
|
|
372
|
+
}
|
|
373
|
+
function formatMetricRaw(metric) {
|
|
374
|
+
return `raw ${Number.isInteger(metric.raw) ? formatInt(metric.raw) : metric.raw.toFixed(3)} ${metric.direction === "lower-is-better" ? "↓" : "↑"}`;
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Resolve a filesystem target to link an eval case to. Prefers the written
|
|
378
|
+
* artifact (`result.json` → its dir), so the link opens the case's output.
|
|
379
|
+
* Returns `undefined` when no artifacts were written.
|
|
380
|
+
*/
|
|
381
|
+
function caseLinkTarget(result) {
|
|
382
|
+
return result.artifacts?.result ?? result.artifacts?.dir;
|
|
383
|
+
}
|
|
384
|
+
/** Wrap text in an OSC 8 terminal hyperlink to a local path or URL. */
|
|
385
|
+
function oscLink(target, text) {
|
|
386
|
+
const url = /^(?:file|https?):\/\//.test(target) ? target : `file://${resolve(target)}`;
|
|
387
|
+
const OSC = "\x1B]8;;";
|
|
388
|
+
const ST = "\x1B\\";
|
|
389
|
+
return `${OSC}${url}${ST}${text}${OSC}${ST}`;
|
|
390
|
+
}
|
|
391
|
+
/** Absolute filesystem path (strips any `file://` prefix, resolves relatives). */
|
|
392
|
+
function absPath(target) {
|
|
393
|
+
const clean = target.replace(/^file:\/\//, "");
|
|
394
|
+
return resolve(clean);
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Case key used to line up the same eval across variants in the comparison
|
|
398
|
+
* matrix. Strips the `-repeat-N` suffix `registerEvalTests` appends so a
|
|
399
|
+
* repeated case aggregates into one row (mean score, x/y passed).
|
|
400
|
+
*/
|
|
401
|
+
function comparisonCaseKey(result) {
|
|
402
|
+
const id = result.id.replace(/-repeat-\d+$/, "");
|
|
403
|
+
return `${result.suite ? `${result.suite}/` : ""}${id}`;
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Cross-variant comparison matrix: one row per eval case, one column per
|
|
407
|
+
* variant (provider/model target), plus rollup rows (score / passed / cost /
|
|
408
|
+
* time) so two models can be read side by side at a glance.
|
|
409
|
+
*/
|
|
410
|
+
function renderComparisonTable(input, variants, color) {
|
|
411
|
+
const results = sortCases(input);
|
|
412
|
+
const caseKeys = [...new Set(results.map(comparisonCaseKey))].sort();
|
|
413
|
+
const byCell = /* @__PURE__ */ new Map();
|
|
414
|
+
for (const result of results) {
|
|
415
|
+
const key = `${comparisonCaseKey(result)}\u0000${result.variant ?? ""}`;
|
|
416
|
+
const list = byCell.get(key) ?? [];
|
|
417
|
+
list.push(result);
|
|
418
|
+
byCell.set(key, list);
|
|
419
|
+
}
|
|
420
|
+
const cellFor = (caseKey, variant) => {
|
|
421
|
+
const group = byCell.get(`${caseKey}\u0000${variant}`);
|
|
422
|
+
if (!group || group.length === 0) return { text: "-" };
|
|
423
|
+
const score = mean(group.map((r) => r.score));
|
|
424
|
+
const passedCount = group.filter((r) => r.passed).length;
|
|
425
|
+
const allPassed = passedCount === group.length;
|
|
426
|
+
const status = group.length > 1 ? `${passedCount}/${group.length}` : allPassed ? "pass" : "fail";
|
|
427
|
+
return {
|
|
428
|
+
text: `${score.toFixed(2)} ${status}`,
|
|
429
|
+
score,
|
|
430
|
+
passed: allPassed
|
|
431
|
+
};
|
|
432
|
+
};
|
|
433
|
+
const variantSummaries = aggregateVariants(results);
|
|
434
|
+
const summaryOf = (variant) => variantSummaries.find((s) => s.variant === variant);
|
|
435
|
+
const pair = variants.length === 2;
|
|
436
|
+
const header = [
|
|
437
|
+
"EVAL",
|
|
438
|
+
...variants,
|
|
439
|
+
...pair ? [`Δ ${variants[1]}`] : []
|
|
440
|
+
];
|
|
441
|
+
const rows = caseKeys.map((key) => {
|
|
442
|
+
const cells = variants.map((variant) => cellFor(key, variant));
|
|
443
|
+
if (pair) cells.push(deltaCell(cells[0]?.score, cells[1]?.score, "higher", "points"));
|
|
444
|
+
return {
|
|
445
|
+
label: key,
|
|
446
|
+
cells
|
|
447
|
+
};
|
|
448
|
+
});
|
|
449
|
+
const rollups = [
|
|
450
|
+
{
|
|
451
|
+
label: "SCORE",
|
|
452
|
+
value: (s) => s.score,
|
|
453
|
+
format: (v) => v.toFixed(2),
|
|
454
|
+
better: "higher",
|
|
455
|
+
delta: "points",
|
|
456
|
+
paint: (s) => ({ score: s.score })
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
label: "PASSED",
|
|
460
|
+
value: (s) => s.passed,
|
|
461
|
+
format: (v, s) => `${v}/${s.count}`,
|
|
462
|
+
better: "higher",
|
|
463
|
+
delta: "points",
|
|
464
|
+
paint: (s) => ({ passed: s.passed === s.count })
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
label: "COST",
|
|
468
|
+
value: (s) => s.usage.cost,
|
|
469
|
+
format: (v) => formatCost(v),
|
|
470
|
+
better: "lower",
|
|
471
|
+
delta: "percent"
|
|
472
|
+
},
|
|
473
|
+
{
|
|
474
|
+
label: "TIME",
|
|
475
|
+
value: (s) => s.durationMs,
|
|
476
|
+
format: (v) => formatDuration(v),
|
|
477
|
+
better: "lower",
|
|
478
|
+
delta: "percent"
|
|
479
|
+
},
|
|
480
|
+
{
|
|
481
|
+
label: "TOKENS",
|
|
482
|
+
value: (s) => s.usage.input + s.usage.output,
|
|
483
|
+
format: (v) => formatInt(v),
|
|
484
|
+
better: "lower",
|
|
485
|
+
delta: "percent"
|
|
486
|
+
},
|
|
487
|
+
{
|
|
488
|
+
label: "CACHE R",
|
|
489
|
+
value: (s) => s.usage.cacheRead,
|
|
490
|
+
format: (v) => formatInt(v),
|
|
491
|
+
better: "lower",
|
|
492
|
+
delta: "percent"
|
|
493
|
+
},
|
|
494
|
+
{
|
|
495
|
+
label: "TURNS",
|
|
496
|
+
value: (s) => s.turns,
|
|
497
|
+
format: (v) => String(v),
|
|
498
|
+
better: "lower",
|
|
499
|
+
delta: "percent"
|
|
500
|
+
},
|
|
501
|
+
{
|
|
502
|
+
label: "TOOL CALLS",
|
|
503
|
+
value: (s) => s.toolCalls,
|
|
504
|
+
format: (v) => String(v),
|
|
505
|
+
better: "lower",
|
|
506
|
+
delta: "percent"
|
|
507
|
+
}
|
|
508
|
+
].map((def) => {
|
|
509
|
+
const summaries = variants.map(summaryOf);
|
|
510
|
+
const cells = summaries.map((s) => s ? {
|
|
511
|
+
text: def.format(def.value(s), s),
|
|
512
|
+
...def.paint?.(s)
|
|
513
|
+
} : { text: "-" });
|
|
514
|
+
if (pair) {
|
|
515
|
+
const [a, b] = summaries;
|
|
516
|
+
cells.push(deltaCell(a ? def.value(a) : void 0, b ? def.value(b) : void 0, def.better, def.delta));
|
|
517
|
+
}
|
|
518
|
+
return {
|
|
519
|
+
label: def.label,
|
|
520
|
+
cells,
|
|
521
|
+
rollup: true
|
|
522
|
+
};
|
|
523
|
+
});
|
|
524
|
+
const allRows = [...rows, ...rollups];
|
|
525
|
+
const widths = header.map((h, i) => Math.max(h.length, ...allRows.map((row) => (i === 0 ? row.label : row.cells[i - 1].text).length)));
|
|
526
|
+
const top = `┌${widths.map((w) => "─".repeat(w + 2)).join("┬")}┐`;
|
|
527
|
+
const mid = `├${widths.map((w) => "─".repeat(w + 2)).join("┼")}┤`;
|
|
528
|
+
const bot = `└${widths.map((w) => "─".repeat(w + 2)).join("┴")}┘`;
|
|
529
|
+
const paintComparisonCell = (cell, padded) => {
|
|
530
|
+
if (cell.score === void 0 && cell.passed === void 0) return color.muted(padded);
|
|
531
|
+
if (cell.score !== void 0) {
|
|
532
|
+
const scoreText = cell.score.toFixed(2);
|
|
533
|
+
const rest = cell.text.slice(scoreText.length);
|
|
534
|
+
const coloredRest = cell.passed === void 0 ? color.muted(rest) : cell.passed ? color.pass(rest) : color.fail(rest);
|
|
535
|
+
return padded.replace(cell.text, `${color.score(cell.score)}${coloredRest}`);
|
|
536
|
+
}
|
|
537
|
+
return padded.replace(cell.text, cell.passed ? color.pass(cell.text) : color.fail(cell.text));
|
|
538
|
+
};
|
|
539
|
+
const renderRow = (row) => {
|
|
540
|
+
const labelPadded = padEnd(row.label, widths[0]);
|
|
541
|
+
return `│ ${row.rollup ? color.heading(labelPadded) : color.muted(labelPadded)} │${row.cells.map((cell, i) => {
|
|
542
|
+
const padded = padStart(cell.text, widths[i + 1]);
|
|
543
|
+
return ` ${paintComparisonCell(cell, padded)} `;
|
|
544
|
+
}).join("│")}│`;
|
|
545
|
+
};
|
|
546
|
+
return [
|
|
547
|
+
top,
|
|
548
|
+
`│ ${color.heading(padEnd(header[0], widths[0]))} │${header.slice(1).map((v, i) => ` ${color.heading(padStart(v, widths[i + 1]))} `).join("│")}│`,
|
|
549
|
+
mid,
|
|
550
|
+
...rows.map(renderRow),
|
|
551
|
+
mid,
|
|
552
|
+
...rollups.map(renderRow),
|
|
553
|
+
bot
|
|
554
|
+
].join("\n");
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* The Δ cell of a two-variant comparison: `b` against `a`, as an absolute
|
|
558
|
+
* difference (`points`, for scores and counts of passes) or a relative one
|
|
559
|
+
* (`percent`). Painted pass/fail by whether the change goes the `better` way;
|
|
560
|
+
* unchanged values and missing sides stay muted.
|
|
561
|
+
*/
|
|
562
|
+
function deltaCell(a, b, better, kind) {
|
|
563
|
+
if (a === void 0 || b === void 0) return { text: "-" };
|
|
564
|
+
const diff = b - a;
|
|
565
|
+
if (Math.abs(diff) < 1e-9) return { text: "=" };
|
|
566
|
+
const sign = diff > 0 ? "+" : "−";
|
|
567
|
+
const improved = better === "higher" ? diff > 0 : diff < 0;
|
|
568
|
+
if (kind === "points") {
|
|
569
|
+
const magnitude = Math.abs(diff);
|
|
570
|
+
return {
|
|
571
|
+
text: `${sign}${Number.isInteger(magnitude) ? magnitude : magnitude.toFixed(2)}`,
|
|
572
|
+
passed: improved
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
if (a === 0) return {
|
|
576
|
+
text: `${sign}∞`,
|
|
577
|
+
passed: improved
|
|
578
|
+
};
|
|
579
|
+
return {
|
|
580
|
+
text: `${sign}${Math.round(Math.abs(diff / a) * 100)}%`,
|
|
581
|
+
passed: improved
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
function renderResultsTable(results, summary, color) {
|
|
585
|
+
const header = [
|
|
586
|
+
"EVAL",
|
|
587
|
+
"STATUS",
|
|
588
|
+
"SCORE",
|
|
589
|
+
"IN",
|
|
590
|
+
"OUT",
|
|
591
|
+
"CACHE R",
|
|
592
|
+
"CACHE W",
|
|
593
|
+
"COST",
|
|
594
|
+
"TIME"
|
|
595
|
+
];
|
|
596
|
+
const rows = results.map((result) => {
|
|
597
|
+
const label = `${result.suite ? `${result.suite}/` : ""}${result.id}`;
|
|
598
|
+
const u = result.result.usage;
|
|
599
|
+
return {
|
|
600
|
+
passed: result.passed,
|
|
601
|
+
cells: [
|
|
602
|
+
label,
|
|
603
|
+
result.passed ? "pass" : "fail",
|
|
604
|
+
result.score.toFixed(2),
|
|
605
|
+
formatInt(u.input),
|
|
606
|
+
formatInt(u.output),
|
|
607
|
+
formatInt(u.cacheRead),
|
|
608
|
+
formatInt(u.cacheCreation),
|
|
609
|
+
formatCost(u.cost ?? 0),
|
|
610
|
+
formatDuration(result.result.durationMs)
|
|
611
|
+
],
|
|
612
|
+
score: result.score
|
|
613
|
+
};
|
|
614
|
+
});
|
|
615
|
+
const totals = {
|
|
616
|
+
cells: [
|
|
617
|
+
`TOTAL (${summary.passed}/${summary.count})`,
|
|
618
|
+
summary.passed === summary.count ? "pass" : "fail",
|
|
619
|
+
summary.score.toFixed(2),
|
|
620
|
+
formatInt(summary.usage.input),
|
|
621
|
+
formatInt(summary.usage.output),
|
|
622
|
+
formatInt(summary.usage.cacheRead),
|
|
623
|
+
formatInt(summary.usage.cacheCreation),
|
|
624
|
+
formatCost(summary.usage.cost),
|
|
625
|
+
formatDuration(summary.durationMs)
|
|
626
|
+
],
|
|
627
|
+
passed: summary.passed === summary.count,
|
|
628
|
+
score: summary.score
|
|
629
|
+
};
|
|
630
|
+
const aligns = [
|
|
631
|
+
"left",
|
|
632
|
+
"left",
|
|
633
|
+
"right",
|
|
634
|
+
"right",
|
|
635
|
+
"right",
|
|
636
|
+
"right",
|
|
637
|
+
"right",
|
|
638
|
+
"right",
|
|
639
|
+
"right"
|
|
640
|
+
];
|
|
641
|
+
const widths = header.map((h, i) => Math.max(h.length, ...rows.map((r) => r.cells[i].length), totals.cells[i].length));
|
|
642
|
+
const top = `┌${widths.map((w) => "─".repeat(w + 2)).join("┬")}┐`;
|
|
643
|
+
const mid = `├${widths.map((w) => "─".repeat(w + 2)).join("┼")}┤`;
|
|
644
|
+
const bot = `└${widths.map((w) => "─".repeat(w + 2)).join("┴")}┘`;
|
|
645
|
+
const renderRow = (cells, paint) => {
|
|
646
|
+
return `│${cells.map((cell, i) => {
|
|
647
|
+
const padded = aligns[i] === "right" ? padStart(cell, widths[i]) : padEnd(cell, widths[i]);
|
|
648
|
+
return ` ${paint ? paint(i, cell, padded) : padded} `;
|
|
649
|
+
}).join("│")}│`;
|
|
650
|
+
};
|
|
651
|
+
return [
|
|
652
|
+
top,
|
|
653
|
+
renderRow(header, (_i, _raw, padded) => color.heading(padded)),
|
|
654
|
+
mid,
|
|
655
|
+
...rows.map((r) => renderRow(r.cells, (i, raw, padded) => paintCell(i, raw, padded, r.passed, r.score, color))),
|
|
656
|
+
mid,
|
|
657
|
+
renderRow(totals.cells, (i, raw, padded) => paintCell(i, raw, padded, totals.passed, totals.score, color, true)),
|
|
658
|
+
bot
|
|
659
|
+
].join("\n");
|
|
660
|
+
}
|
|
661
|
+
function paintCell(col, raw, padded, passed, score, color, bold = false) {
|
|
662
|
+
if (col === 0) return bold ? color.heading(padded) : color.caseStatus(passed, padded);
|
|
663
|
+
if (col === 1) return padded.replace(raw, color.status(passed));
|
|
664
|
+
if (col === 2) return padded.replace(raw, color.score(score));
|
|
665
|
+
if (col === 7) return color.cost(padded);
|
|
666
|
+
return color.muted(padded);
|
|
667
|
+
}
|
|
668
|
+
function scoreDetailLine(score) {
|
|
669
|
+
if (typeof score.details === "string") return score.details;
|
|
670
|
+
if (isRecord(score.details) && typeof score.details.reasoning === "string") return score.details.reasoning;
|
|
671
|
+
}
|
|
672
|
+
function formatInt(value) {
|
|
673
|
+
return value.toLocaleString("en-US");
|
|
674
|
+
}
|
|
675
|
+
function formatCost(value) {
|
|
676
|
+
return value > 0 ? `$${value.toFixed(4)}` : "-";
|
|
677
|
+
}
|
|
678
|
+
function formatDuration(ms) {
|
|
679
|
+
const rounded = Math.round(ms);
|
|
680
|
+
if (rounded < 1e3) return `${rounded}ms`;
|
|
681
|
+
const s = rounded / 1e3;
|
|
682
|
+
return `${Number.isInteger(s) ? s : s.toFixed(1)}s`;
|
|
683
|
+
}
|
|
684
|
+
function padEnd(value, width) {
|
|
685
|
+
return value.length >= width ? value : value + " ".repeat(width - value.length);
|
|
686
|
+
}
|
|
687
|
+
function padStart(value, width) {
|
|
688
|
+
return value.length >= width ? value : " ".repeat(width - value.length) + value;
|
|
689
|
+
}
|
|
690
|
+
function truncate(value, max) {
|
|
691
|
+
const oneLine = value.replace(/\s+/g, " ").trim();
|
|
692
|
+
return oneLine.length > max ? `${oneLine.slice(0, max - 1)}…` : oneLine;
|
|
693
|
+
}
|
|
694
|
+
function isRecord(value) {
|
|
695
|
+
return typeof value === "object" && value !== null;
|
|
696
|
+
}
|
|
697
|
+
function createEvalColors(enabled = shouldUseColor()) {
|
|
698
|
+
if (!enabled) return {
|
|
699
|
+
heading: (value) => value,
|
|
700
|
+
pass: (value) => value,
|
|
701
|
+
fail: (value) => value,
|
|
702
|
+
muted: (value) => value,
|
|
703
|
+
cost: (value) => value,
|
|
704
|
+
status: (passed) => passed ? "pass" : "fail",
|
|
705
|
+
score: (value) => value.toFixed(2),
|
|
706
|
+
caseStatus: (_passed, value) => value,
|
|
707
|
+
dot: (passed) => passed ? "+" : "x",
|
|
708
|
+
tag: (value) => value,
|
|
709
|
+
step: (_kind, value) => value
|
|
710
|
+
};
|
|
711
|
+
return {
|
|
712
|
+
heading: (value) => chalk.bold(value),
|
|
713
|
+
pass: (value) => chalk.green(value),
|
|
714
|
+
fail: (value) => chalk.red(value),
|
|
715
|
+
muted: (value) => chalk.gray(value),
|
|
716
|
+
cost: (value) => chalk.cyan(value),
|
|
717
|
+
status: (passed) => passed ? chalk.green("pass") : chalk.red("fail"),
|
|
718
|
+
score: (value) => value >= .9 ? chalk.green(value.toFixed(2)) : value >= .7 ? chalk.yellow(value.toFixed(2)) : chalk.red(value.toFixed(2)),
|
|
719
|
+
caseStatus: (passed, value) => passed ? chalk.green(value) : chalk.red(value),
|
|
720
|
+
dot: (passed) => passed ? chalk.green("●") : chalk.red("●"),
|
|
721
|
+
tag: (value) => chalk.bold(chalk.whiteBright(value)),
|
|
722
|
+
step: (kind, value) => paintStep(kind, value)
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
/** Per-kind trajectory colors: think=magenta, text=blue, tools by family. */
|
|
726
|
+
function paintStep(kind, value) {
|
|
727
|
+
if (kind === "think") return chalk.magenta(value);
|
|
728
|
+
if (kind === "text") return chalk.blue(value);
|
|
729
|
+
if (/write|edit|create/.test(value)) return chalk.green(value);
|
|
730
|
+
if (/read|list|glob|grep|search/.test(value)) return chalk.cyan(value);
|
|
731
|
+
if (/shell|bash|exec|run/.test(value)) return chalk.yellow(value);
|
|
732
|
+
if (/spawn|task|agent/.test(value)) return chalk.magentaBright(value);
|
|
733
|
+
return chalk.white(value);
|
|
734
|
+
}
|
|
735
|
+
function formatTrajectoryColored(trajectory, color) {
|
|
736
|
+
return trajectory.steps.map((step) => {
|
|
737
|
+
const base = step.kind === "tool" ? step.name ?? "tool" : step.kind;
|
|
738
|
+
const labelWithCount = step.count > 1 ? `${base} ×${step.count}` : base;
|
|
739
|
+
const painted = color.step(step.kind, labelWithCount);
|
|
740
|
+
const timing = step.durationMs && step.durationMs >= 100 ? color.muted(`(${formatDuration(step.durationMs)})`) : "";
|
|
741
|
+
return timing ? `${painted} ${timing}` : painted;
|
|
742
|
+
}).join(color.muted(" → "));
|
|
743
|
+
}
|
|
744
|
+
function shouldUseColor() {
|
|
745
|
+
return Boolean(process.stdout.isTTY && !process.env.NO_COLOR);
|
|
746
|
+
}
|
|
747
|
+
//#endregion
|
|
748
|
+
//#region src/eval-scorers.ts
|
|
749
|
+
function statusCompleted(name = "status.completed") {
|
|
750
|
+
return ({ result }) => ({
|
|
751
|
+
name,
|
|
752
|
+
passed: result.status === "completed",
|
|
753
|
+
score: result.status === "completed" ? 1 : 0,
|
|
754
|
+
details: result.status === "completed" ? void 0 : {
|
|
755
|
+
status: result.status,
|
|
756
|
+
error: result.error
|
|
757
|
+
}
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
function fileExists(path, name = `file.exists:${path}`) {
|
|
761
|
+
return ({ workspace }) => {
|
|
762
|
+
const found = workspace?.files.some((file) => file.path === path) ?? false;
|
|
763
|
+
return {
|
|
764
|
+
name,
|
|
765
|
+
passed: found,
|
|
766
|
+
score: found ? 1 : 0,
|
|
767
|
+
details: found ? void 0 : `Missing ${path}`
|
|
768
|
+
};
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
function fileExistsOneOf(paths, name = `file.existsOneOf:${paths.join("|")}`) {
|
|
772
|
+
return ({ workspace }) => {
|
|
773
|
+
const found = paths.find((path) => workspace?.files.some((file) => file.path === path));
|
|
774
|
+
return {
|
|
775
|
+
name,
|
|
776
|
+
passed: Boolean(found),
|
|
777
|
+
score: found ? 1 : 0,
|
|
778
|
+
details: found ? void 0 : `Missing one of: ${paths.join(", ")}`
|
|
779
|
+
};
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
function fileContains(path, expected, name = `file.contains:${path}`) {
|
|
783
|
+
return ({ workspace }) => {
|
|
784
|
+
const file = workspace?.files.find((file) => file.path === path);
|
|
785
|
+
const content = file?.content ?? "";
|
|
786
|
+
const passed = typeof expected === "string" ? content.includes(expected) : expected.test(content);
|
|
787
|
+
return {
|
|
788
|
+
name,
|
|
789
|
+
passed,
|
|
790
|
+
score: passed ? 1 : 0,
|
|
791
|
+
details: passed ? void 0 : file ? `Expected ${path} to contain ${String(expected)}` : `Missing ${path}`
|
|
792
|
+
};
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
function fileContentQuality(path, expected, name = `file.quality:${path}`) {
|
|
796
|
+
return ({ workspace }) => {
|
|
797
|
+
const content = (workspace?.files.find((file) => file.path === path))?.content;
|
|
798
|
+
if (content === void 0) return {
|
|
799
|
+
name,
|
|
800
|
+
passed: false,
|
|
801
|
+
score: 0,
|
|
802
|
+
details: `Missing ${path}`
|
|
803
|
+
};
|
|
804
|
+
const normalized = content.trim();
|
|
805
|
+
const expectedNormalized = expected.trim();
|
|
806
|
+
const score = normalized === expectedNormalized ? 1 : normalized.includes(expectedNormalized) ? .75 : content.includes(expected) ? .5 : 0;
|
|
807
|
+
return {
|
|
808
|
+
name,
|
|
809
|
+
passed: score >= .75,
|
|
810
|
+
score,
|
|
811
|
+
details: score >= .75 ? void 0 : `Expected ${path} to closely match ${JSON.stringify(expectedNormalized)}`
|
|
812
|
+
};
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* Aggregate a group of boolean scorers into a single `0..1` functionality
|
|
817
|
+
* metric (fraction passed) and emit it. Useful for "N of M files present".
|
|
818
|
+
*/
|
|
819
|
+
function functionalityMetric(metricId, scorers, name = metricId) {
|
|
820
|
+
return async (ctx) => {
|
|
821
|
+
const results = await runScorers(ctx, scorers);
|
|
822
|
+
const passed = results.filter((r) => r.passed).length;
|
|
823
|
+
const fraction = results.length === 0 ? 1 : passed / results.length;
|
|
824
|
+
ctx.metric(metricId, fraction);
|
|
825
|
+
return {
|
|
826
|
+
name,
|
|
827
|
+
passed: passed === results.length,
|
|
828
|
+
score: fraction,
|
|
829
|
+
details: {
|
|
830
|
+
passed,
|
|
831
|
+
total: results.length,
|
|
832
|
+
checks: results.map((r) => ({
|
|
833
|
+
name: r.name,
|
|
834
|
+
passed: r.passed
|
|
835
|
+
}))
|
|
836
|
+
}
|
|
837
|
+
};
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
async function runScorers(ctx, scorers) {
|
|
841
|
+
const scores = [];
|
|
842
|
+
for (const scorer of scorers) try {
|
|
843
|
+
scores.push(await scorer(ctx));
|
|
844
|
+
} catch (err) {
|
|
845
|
+
if (err instanceof EvalMetricError) throw err;
|
|
846
|
+
scores.push({
|
|
847
|
+
name: scorer.name || "anonymous-scorer",
|
|
848
|
+
passed: false,
|
|
849
|
+
score: 0,
|
|
850
|
+
details: err instanceof Error ? err.message : String(err)
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
return scores;
|
|
854
|
+
}
|
|
855
|
+
function aggregateScore(scores) {
|
|
856
|
+
if (scores.length === 0) return 1;
|
|
857
|
+
return scores.reduce((sum, score) => sum + (score.score ?? (score.passed ? 1 : 0)), 0) / scores.length;
|
|
858
|
+
}
|
|
859
|
+
//#endregion
|
|
860
|
+
//#region src/eval-trajectory.ts
|
|
861
|
+
/**
|
|
862
|
+
* Build an ordered, grouped trajectory from a transcript. Walks assistant
|
|
863
|
+
* turns in order, emitting `think` / `text` / `tool:<name>` steps, grouping
|
|
864
|
+
* only consecutive identical kinds. Per-step duration is approximated from the
|
|
865
|
+
* owning turn's `createdAt` delta to the next turn.
|
|
866
|
+
*/
|
|
867
|
+
function buildTrajectory(transcript) {
|
|
868
|
+
const steps = [];
|
|
869
|
+
let total = 0;
|
|
870
|
+
const push = (kind, name, durationMs) => {
|
|
871
|
+
total++;
|
|
872
|
+
const last = steps[steps.length - 1];
|
|
873
|
+
if (last && last.kind === kind && last.name === name) {
|
|
874
|
+
last.count++;
|
|
875
|
+
if (durationMs > 0) last.durationMs = (last.durationMs ?? 0) + durationMs;
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
steps.push({
|
|
879
|
+
kind,
|
|
880
|
+
...name ? { name } : {},
|
|
881
|
+
count: 1,
|
|
882
|
+
...durationMs > 0 ? { durationMs } : {}
|
|
883
|
+
});
|
|
884
|
+
};
|
|
885
|
+
for (let i = 0; i < transcript.length; i++) {
|
|
886
|
+
const turn = transcript[i];
|
|
887
|
+
if (turn.role !== "assistant") continue;
|
|
888
|
+
const next = transcript[i + 1];
|
|
889
|
+
const turnDuration = next ? Math.max(0, next.createdAt - turn.createdAt) : 0;
|
|
890
|
+
const blocks = turn.content.filter((b) => b.type === "thinking" || b.type === "redacted_thinking" || b.type === "text" || b.type === "tool_call");
|
|
891
|
+
const toolBlocks = blocks.filter((b) => b.type === "tool_call").length;
|
|
892
|
+
const perTool = toolBlocks > 0 ? Math.round(turnDuration / toolBlocks) : 0;
|
|
893
|
+
for (const block of blocks) if (block.type === "thinking" || block.type === "redacted_thinking") push("think", void 0, 0);
|
|
894
|
+
else if (block.type === "text") push("text", void 0, 0);
|
|
895
|
+
else if (block.type === "tool_call") push("tool", block.name, perTool);
|
|
896
|
+
}
|
|
897
|
+
return {
|
|
898
|
+
steps,
|
|
899
|
+
totalBlocks: total
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
/** Render a trajectory as a one-line timeline string (no color). */
|
|
903
|
+
function formatTrajectoryLine(trajectory) {
|
|
904
|
+
return trajectory.steps.map(stepLabel).join(" → ");
|
|
905
|
+
}
|
|
906
|
+
function stepLabel(step) {
|
|
907
|
+
const base = step.kind === "tool" ? step.name ?? "tool" : step.kind;
|
|
908
|
+
return step.count > 1 ? `${base} ×${step.count}` : base;
|
|
909
|
+
}
|
|
910
|
+
//#endregion
|
|
911
|
+
//#region src/eval-workspace.ts
|
|
912
|
+
async function prepareWorkspace(execution, workspace) {
|
|
913
|
+
if (!workspace) return {
|
|
914
|
+
snapshot: () => void 0,
|
|
915
|
+
error: () => void 0,
|
|
916
|
+
cleanup: async () => {}
|
|
917
|
+
};
|
|
918
|
+
let tempDir;
|
|
919
|
+
let baseExecution = execution;
|
|
920
|
+
if (!baseExecution) {
|
|
921
|
+
const cwd = workspace.cwd ?? await mkdtemp(join(tmpdir(), "zidane-eval-"));
|
|
922
|
+
if (!workspace.cwd) tempDir = cwd;
|
|
923
|
+
baseExecution = createProcessContext({ cwd });
|
|
924
|
+
}
|
|
925
|
+
const captured = {};
|
|
926
|
+
return {
|
|
927
|
+
execution: withWorkspaceLifecycle(baseExecution, workspace, captured),
|
|
928
|
+
snapshot: () => captured.snapshot,
|
|
929
|
+
error: () => captured.error,
|
|
930
|
+
cleanup: async () => {
|
|
931
|
+
if (tempDir && !workspace.retain) await rm(tempDir, {
|
|
932
|
+
recursive: true,
|
|
933
|
+
force: true
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
function withWorkspaceLifecycle(execution, workspace, captured) {
|
|
939
|
+
return {
|
|
940
|
+
...execution,
|
|
941
|
+
async spawn(config) {
|
|
942
|
+
const handle = await execution.spawn({
|
|
943
|
+
...config,
|
|
944
|
+
...workspace.cwd && !config?.cwd ? { cwd: workspace.cwd } : {}
|
|
945
|
+
});
|
|
946
|
+
if (workspace.seedDir) await seedWorkspace(execution, handle, workspace);
|
|
947
|
+
return handle;
|
|
948
|
+
},
|
|
949
|
+
async destroy(handle) {
|
|
950
|
+
try {
|
|
951
|
+
captured.snapshot = await captureWorkspace(execution, handle, workspace);
|
|
952
|
+
} catch (err) {
|
|
953
|
+
captured.error = err instanceof Error ? err.message : String(err);
|
|
954
|
+
} finally {
|
|
955
|
+
await execution.destroy(handle);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
async function seedWorkspace(execution, handle, workspace) {
|
|
961
|
+
const target = workspace.seedTarget ?? ".";
|
|
962
|
+
const result = await execution.exec(handle, `mkdir -p ${alwaysQuote(target)} && cp -R ${alwaysQuote(`${workspace.seedDir}/.`)} ${alwaysQuote(target)}`);
|
|
963
|
+
if (result.exitCode !== 0) throw new Error(`Failed to seed eval workspace: ${result.stderr || result.stdout}`);
|
|
964
|
+
}
|
|
965
|
+
async function captureWorkspace(execution, handle, workspace) {
|
|
966
|
+
const capturePaths = workspace.capture?.length ? workspace.capture : ["."];
|
|
967
|
+
const command = [
|
|
968
|
+
`cd ${alwaysQuote(handle.cwd)}`,
|
|
969
|
+
"&&",
|
|
970
|
+
"for p in",
|
|
971
|
+
capturePaths.map(alwaysQuote).join(" "),
|
|
972
|
+
"; do",
|
|
973
|
+
"if [ -e \"$p\" ]; then",
|
|
974
|
+
"find \"$p\" \\( -path \"*/node_modules/*\" -o -path \"*/.git/*\" \\) -prune -o -type f -print0",
|
|
975
|
+
"; fi",
|
|
976
|
+
"done"
|
|
977
|
+
].join(" ");
|
|
978
|
+
const listed = await execution.exec(handle, command);
|
|
979
|
+
if (listed.exitCode !== 0) throw new Error(`Failed to list eval workspace: ${listed.stderr || listed.stdout}`);
|
|
980
|
+
const maxFileChars = workspace.maxFileChars ?? 262144;
|
|
981
|
+
const files = [];
|
|
982
|
+
const seen = /* @__PURE__ */ new Set();
|
|
983
|
+
for (const rawPath of listed.stdout.split("\0").filter(Boolean)) {
|
|
984
|
+
const path = normalizeSnapshotPath(rawPath);
|
|
985
|
+
if (seen.has(path)) continue;
|
|
986
|
+
seen.add(path);
|
|
987
|
+
const content = await execution.readFile(handle, path);
|
|
988
|
+
const binary = content.includes("\0");
|
|
989
|
+
const truncated = content.length > maxFileChars;
|
|
990
|
+
files.push({
|
|
991
|
+
path,
|
|
992
|
+
size: content.length,
|
|
993
|
+
...binary ? { binary: true } : { content: truncated ? content.slice(0, maxFileChars) : content },
|
|
994
|
+
...truncated ? { truncated: true } : {}
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
return {
|
|
998
|
+
cwd: handle.cwd,
|
|
999
|
+
files
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
function normalizeSnapshotPath(path) {
|
|
1003
|
+
const normalized = path.startsWith("./") ? path.slice(2) : path;
|
|
1004
|
+
return normalized === "" ? "." : normalized;
|
|
1005
|
+
}
|
|
1006
|
+
//#endregion
|
|
1007
|
+
//#region src/eval-execution.ts
|
|
1008
|
+
/**
|
|
1009
|
+
* Wrap an execution context so repeated agent runs share one handle until the
|
|
1010
|
+
* caller disposes it. This is useful for eval fixtures that want per-test
|
|
1011
|
+
* Docker/process lifetime while still using `runHeadless()` per turn.
|
|
1012
|
+
*/
|
|
1013
|
+
function createReusableExecutionContext(base) {
|
|
1014
|
+
let handle;
|
|
1015
|
+
let spawnPromise;
|
|
1016
|
+
let disposed = false;
|
|
1017
|
+
let disposePromise;
|
|
1018
|
+
const ensureHandle = (config) => {
|
|
1019
|
+
if (disposed) throw new Error("Reusable execution context has been disposed");
|
|
1020
|
+
if (handle) return Promise.resolve(handle);
|
|
1021
|
+
spawnPromise ??= base.spawn(config).then((next) => {
|
|
1022
|
+
handle = next;
|
|
1023
|
+
return next;
|
|
1024
|
+
}, (err) => {
|
|
1025
|
+
spawnPromise = void 0;
|
|
1026
|
+
throw err;
|
|
1027
|
+
});
|
|
1028
|
+
return spawnPromise;
|
|
1029
|
+
};
|
|
1030
|
+
return {
|
|
1031
|
+
execution: {
|
|
1032
|
+
...base,
|
|
1033
|
+
async spawn(config) {
|
|
1034
|
+
return ensureHandle(config);
|
|
1035
|
+
},
|
|
1036
|
+
async destroy() {}
|
|
1037
|
+
},
|
|
1038
|
+
dispose() {
|
|
1039
|
+
disposePromise ??= (async () => {
|
|
1040
|
+
disposed = true;
|
|
1041
|
+
const active = handle ?? await spawnPromise?.catch(() => void 0);
|
|
1042
|
+
handle = void 0;
|
|
1043
|
+
spawnPromise = void 0;
|
|
1044
|
+
if (active) await base.destroy(active);
|
|
1045
|
+
})();
|
|
1046
|
+
return disposePromise;
|
|
1047
|
+
},
|
|
1048
|
+
handle: () => handle
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
//#endregion
|
|
1052
|
+
//#region src/eval-agent.ts
|
|
1053
|
+
/**
|
|
1054
|
+
* Multi-turn eval agent over the low-level headless runner. It keeps session
|
|
1055
|
+
* and execution lifetime outside any Playwright-specific fixture layer,
|
|
1056
|
+
* so downstream platforms can wrap it with their own `agent.run(...)` shape.
|
|
1057
|
+
*/
|
|
1058
|
+
function createEvalAgent(options) {
|
|
1059
|
+
const { cwd, execution, session: initialSession, store: initialStore, mcpServers: baseMcpServers, onEvent: baseOnEvent, ...headlessDefaults } = options;
|
|
1060
|
+
const executionOwner = createReusableExecutionContext(execution ?? createProcessContext({ cwd }));
|
|
1061
|
+
const store = initialStore ?? createMemoryStore();
|
|
1062
|
+
let sessionPromise = initialSession ? Promise.resolve(initialSession) : void 0;
|
|
1063
|
+
let disposed = false;
|
|
1064
|
+
let runInFlight = false;
|
|
1065
|
+
let disposePromise;
|
|
1066
|
+
const stats = {
|
|
1067
|
+
runs: 0,
|
|
1068
|
+
input: 0,
|
|
1069
|
+
output: 0,
|
|
1070
|
+
cacheRead: 0,
|
|
1071
|
+
cacheCreation: 0,
|
|
1072
|
+
cost: 0,
|
|
1073
|
+
turns: 0,
|
|
1074
|
+
toolCalls: 0,
|
|
1075
|
+
durationMs: 0
|
|
1076
|
+
};
|
|
1077
|
+
async function getSession() {
|
|
1078
|
+
if (disposed) throw new Error("Eval agent has been disposed");
|
|
1079
|
+
sessionPromise ??= createSession({ store });
|
|
1080
|
+
return sessionPromise;
|
|
1081
|
+
}
|
|
1082
|
+
async function resolveMcpServers(runServers) {
|
|
1083
|
+
const merged = [...(typeof baseMcpServers === "function" ? await baseMcpServers() : baseMcpServers) ?? [], ...runServers ?? []];
|
|
1084
|
+
return merged.length > 0 ? [...merged] : void 0;
|
|
1085
|
+
}
|
|
1086
|
+
async function run(runOptions) {
|
|
1087
|
+
if (runInFlight) throw new Error("Eval agent already has a run in progress");
|
|
1088
|
+
runInFlight = true;
|
|
1089
|
+
try {
|
|
1090
|
+
const session = await getSession();
|
|
1091
|
+
const { mcpServers: runMcpServers, onEvent: runOnEvent, ...restRunOptions } = runOptions;
|
|
1092
|
+
const mcpServers = await resolveMcpServers(runMcpServers);
|
|
1093
|
+
const result = await runHeadless({
|
|
1094
|
+
...headlessDefaults,
|
|
1095
|
+
...restRunOptions,
|
|
1096
|
+
session,
|
|
1097
|
+
execution: executionOwner.execution,
|
|
1098
|
+
...mcpServers ? { mcpServers } : {},
|
|
1099
|
+
onEvent(event) {
|
|
1100
|
+
baseOnEvent?.(event);
|
|
1101
|
+
runOnEvent?.(event);
|
|
1102
|
+
}
|
|
1103
|
+
});
|
|
1104
|
+
const newTranscript = result.transcript.slice();
|
|
1105
|
+
const runStats = {
|
|
1106
|
+
input: result.usage.input,
|
|
1107
|
+
output: result.usage.output,
|
|
1108
|
+
cacheRead: result.usage.cacheRead,
|
|
1109
|
+
cacheCreation: result.usage.cacheCreation,
|
|
1110
|
+
cost: result.usage.cost ?? 0,
|
|
1111
|
+
durationMs: result.durationMs,
|
|
1112
|
+
turns: result.turns,
|
|
1113
|
+
toolCalls: countToolCallsInTurns(newTranscript)
|
|
1114
|
+
};
|
|
1115
|
+
accumulateEvalAgentStats(stats, runStats);
|
|
1116
|
+
return {
|
|
1117
|
+
result,
|
|
1118
|
+
stats: runStats,
|
|
1119
|
+
newTranscript
|
|
1120
|
+
};
|
|
1121
|
+
} finally {
|
|
1122
|
+
runInFlight = false;
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
function dispose() {
|
|
1126
|
+
disposePromise ??= (async () => {
|
|
1127
|
+
disposed = true;
|
|
1128
|
+
await executionOwner.dispose();
|
|
1129
|
+
})();
|
|
1130
|
+
return disposePromise;
|
|
1131
|
+
}
|
|
1132
|
+
return {
|
|
1133
|
+
run,
|
|
1134
|
+
stats,
|
|
1135
|
+
session: getSession,
|
|
1136
|
+
dispose,
|
|
1137
|
+
[Symbol.asyncDispose]: dispose
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
function accumulateEvalAgentStats(acc, turn) {
|
|
1141
|
+
acc.runs += 1;
|
|
1142
|
+
acc.input += turn.input;
|
|
1143
|
+
acc.output += turn.output;
|
|
1144
|
+
acc.cacheRead += turn.cacheRead;
|
|
1145
|
+
acc.cacheCreation += turn.cacheCreation;
|
|
1146
|
+
acc.cost += turn.cost;
|
|
1147
|
+
acc.turns += turn.turns;
|
|
1148
|
+
acc.toolCalls += turn.toolCalls;
|
|
1149
|
+
acc.durationMs += turn.durationMs;
|
|
1150
|
+
}
|
|
1151
|
+
function countToolCallsInTurns(turns) {
|
|
1152
|
+
let count = 0;
|
|
1153
|
+
for (const turn of turns) {
|
|
1154
|
+
if (turn.role !== "assistant") continue;
|
|
1155
|
+
count += turn.content.filter((block) => block.type === "tool_call").length;
|
|
1156
|
+
}
|
|
1157
|
+
return count;
|
|
1158
|
+
}
|
|
1159
|
+
const UNPARSEABLE_REASONING = "Judge did not return a parseable score.";
|
|
1160
|
+
const DEFAULT_JUDGE_SYSTEM = [
|
|
1161
|
+
"You are a strict software eval judge.",
|
|
1162
|
+
"Grade only the supplied output against the rubric.",
|
|
1163
|
+
"Call the `submit_grade` tool with your score and reasoning.",
|
|
1164
|
+
"The score must be between 0 and 1."
|
|
1165
|
+
].join("\n");
|
|
1166
|
+
/** Forced-output tool the judge must call — the schema-enforcement path. */
|
|
1167
|
+
const JUDGE_TOOL = {
|
|
1168
|
+
name: "submit_grade",
|
|
1169
|
+
description: "Submit the grade for the output under evaluation.",
|
|
1170
|
+
inputSchema: {
|
|
1171
|
+
type: "object",
|
|
1172
|
+
properties: {
|
|
1173
|
+
score: {
|
|
1174
|
+
type: "number",
|
|
1175
|
+
minimum: 0,
|
|
1176
|
+
maximum: 1,
|
|
1177
|
+
description: "Quality score from 0 (worst) to 1 (best)."
|
|
1178
|
+
},
|
|
1179
|
+
reasoning: {
|
|
1180
|
+
type: "string",
|
|
1181
|
+
description: "Brief justification for the score."
|
|
1182
|
+
},
|
|
1183
|
+
feedback: {
|
|
1184
|
+
type: "string",
|
|
1185
|
+
description: "Optional actionable feedback."
|
|
1186
|
+
}
|
|
1187
|
+
},
|
|
1188
|
+
required: ["score", "reasoning"],
|
|
1189
|
+
additionalProperties: false
|
|
1190
|
+
}
|
|
1191
|
+
};
|
|
1192
|
+
function llmJudge(options) {
|
|
1193
|
+
const name = options.name ?? "llm.judge";
|
|
1194
|
+
const model = options.model ?? options.provider.meta.defaultModel;
|
|
1195
|
+
return async (ctx) => {
|
|
1196
|
+
const userMessage = options.provider.userMessage(renderJudgePrompt({
|
|
1197
|
+
rubric: options.rubric,
|
|
1198
|
+
input: options.input?.(ctx) ?? defaultJudgeInput(ctx)
|
|
1199
|
+
}));
|
|
1200
|
+
let text = "";
|
|
1201
|
+
const controller = new AbortController();
|
|
1202
|
+
const timeoutMs = options.timeoutMs ?? 6e4;
|
|
1203
|
+
let timedOut = false;
|
|
1204
|
+
const timer = timeoutMs > 0 ? setTimeout(() => {
|
|
1205
|
+
timedOut = true;
|
|
1206
|
+
controller.abort(/* @__PURE__ */ new Error(`LLM judge timed out after ${timeoutMs}ms`));
|
|
1207
|
+
}, timeoutMs) : void 0;
|
|
1208
|
+
const timeoutPromise = timeoutMs > 0 ? new Promise((_resolve, reject) => {
|
|
1209
|
+
controller.signal.addEventListener("abort", () => {
|
|
1210
|
+
if (timedOut) reject(/* @__PURE__ */ new Error(`LLM judge timed out after ${timeoutMs}ms`));
|
|
1211
|
+
}, { once: true });
|
|
1212
|
+
}) : void 0;
|
|
1213
|
+
timeoutPromise?.catch(() => {});
|
|
1214
|
+
const grade = async (maxTokens) => {
|
|
1215
|
+
text = "";
|
|
1216
|
+
const streamPromise = options.provider.stream({
|
|
1217
|
+
model,
|
|
1218
|
+
system: options.system ?? DEFAULT_JUDGE_SYSTEM,
|
|
1219
|
+
tools: options.provider.formatTools([JUDGE_TOOL]),
|
|
1220
|
+
messages: [userMessage],
|
|
1221
|
+
maxTokens,
|
|
1222
|
+
cache: false,
|
|
1223
|
+
signal: controller.signal,
|
|
1224
|
+
toolChoice: {
|
|
1225
|
+
type: "tool",
|
|
1226
|
+
name: JUDGE_TOOL.name
|
|
1227
|
+
}
|
|
1228
|
+
}, { onText(delta) {
|
|
1229
|
+
text += delta;
|
|
1230
|
+
} });
|
|
1231
|
+
return timeoutPromise ? await Promise.race([streamPromise, timeoutPromise]) : await streamPromise;
|
|
1232
|
+
};
|
|
1233
|
+
try {
|
|
1234
|
+
const budget = options.maxTokens ?? 4e3;
|
|
1235
|
+
let result = await grade(budget);
|
|
1236
|
+
const submitted = (r) => r.toolCalls.some((tc) => tc.name === JUDGE_TOOL.name);
|
|
1237
|
+
if (!submitted(result) && result.usage.finishReason === "length") result = await grade(budget * 2);
|
|
1238
|
+
const parsed = readJudgeResult(result.toolCalls, text || result.text);
|
|
1239
|
+
if (!submitted(result) && parsed.reasoning === UNPARSEABLE_REASONING && result.usage.finishReason) parsed.reasoning = `${UNPARSEABLE_REASONING} (finish reason: ${result.usage.finishReason})`;
|
|
1240
|
+
if (options.metric) ctx.metric(options.metric, parsed.score);
|
|
1241
|
+
return {
|
|
1242
|
+
name,
|
|
1243
|
+
passed: parsed.score >= .7,
|
|
1244
|
+
score: parsed.score,
|
|
1245
|
+
details: {
|
|
1246
|
+
reasoning: parsed.reasoning,
|
|
1247
|
+
...parsed.feedback ? { feedback: parsed.feedback } : {},
|
|
1248
|
+
...parsed.raw ? { raw: parsed.raw } : {},
|
|
1249
|
+
usage: result.usage
|
|
1250
|
+
}
|
|
1251
|
+
};
|
|
1252
|
+
} finally {
|
|
1253
|
+
if (timer) clearTimeout(timer);
|
|
1254
|
+
}
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1257
|
+
function readJudgeResult(toolCalls, fallbackText) {
|
|
1258
|
+
const call = toolCalls.find((tc) => tc.name === JUDGE_TOOL.name);
|
|
1259
|
+
if (call) {
|
|
1260
|
+
const input = call.input;
|
|
1261
|
+
return {
|
|
1262
|
+
score: clampScore(input.score),
|
|
1263
|
+
reasoning: typeof input.reasoning === "string" ? input.reasoning : "",
|
|
1264
|
+
...typeof input.feedback === "string" ? { feedback: input.feedback } : {}
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
return parseJudgeOutput(fallbackText);
|
|
1268
|
+
}
|
|
1269
|
+
function renderJudgePrompt(input) {
|
|
1270
|
+
return [
|
|
1271
|
+
"Rubric:",
|
|
1272
|
+
input.rubric,
|
|
1273
|
+
"",
|
|
1274
|
+
"Output to grade:",
|
|
1275
|
+
input.input,
|
|
1276
|
+
"",
|
|
1277
|
+
"Return JSON only."
|
|
1278
|
+
].join("\n");
|
|
1279
|
+
}
|
|
1280
|
+
function defaultJudgeInput(ctx) {
|
|
1281
|
+
const chunks = [`Final answer:\n${ctx.result.finalText}`];
|
|
1282
|
+
if (ctx.workspace) chunks.push(`Workspace files:\n${ctx.workspace.files.map((file) => [`--- ${file.path}${file.truncated ? " (truncated)" : ""} ---`, file.binary ? "[binary omitted]" : file.content ?? ""].join("\n")).join("\n")}`);
|
|
1283
|
+
return chunks.join("\n\n");
|
|
1284
|
+
}
|
|
1285
|
+
function parseJudgeOutput(raw) {
|
|
1286
|
+
const trimmed = stripCodeFences(raw.trim());
|
|
1287
|
+
const jsonText = extractJsonObject(trimmed);
|
|
1288
|
+
if (jsonText) try {
|
|
1289
|
+
const parsed = JSON.parse(jsonText);
|
|
1290
|
+
if (parsed.score !== void 0 || parsed.reasoning !== void 0) return {
|
|
1291
|
+
score: clampScore(parsed.score),
|
|
1292
|
+
reasoning: typeof parsed.reasoning === "string" ? parsed.reasoning : "",
|
|
1293
|
+
...typeof parsed.feedback === "string" ? { feedback: parsed.feedback } : {},
|
|
1294
|
+
...jsonText === trimmed ? {} : { raw: trimmed }
|
|
1295
|
+
};
|
|
1296
|
+
} catch {}
|
|
1297
|
+
const scoreFragment = trimmed.match(/score["']?\s*[:=]\s*(-?\d+(?:\.\d+)?)/i);
|
|
1298
|
+
if (scoreFragment) return {
|
|
1299
|
+
score: clampScore(Number(scoreFragment[1])),
|
|
1300
|
+
reasoning: trimmed.slice(0, 500),
|
|
1301
|
+
raw: trimmed
|
|
1302
|
+
};
|
|
1303
|
+
const bareFraction = trimmed.match(/^(-?\d+(?:\.\d+)?)\s*\/\s*(\d+(?:\.\d+)?)$/);
|
|
1304
|
+
if (bareFraction) {
|
|
1305
|
+
const denom = Number(bareFraction[2]);
|
|
1306
|
+
return {
|
|
1307
|
+
score: clampScore(denom === 0 ? 0 : Number(bareFraction[1]) / denom),
|
|
1308
|
+
reasoning: trimmed,
|
|
1309
|
+
raw: trimmed
|
|
1310
|
+
};
|
|
1311
|
+
}
|
|
1312
|
+
if (trimmed.match(/^-?\d+(?:\.\d+)?$/)) return {
|
|
1313
|
+
score: clampScore(Number(trimmed)),
|
|
1314
|
+
reasoning: "",
|
|
1315
|
+
raw: trimmed
|
|
1316
|
+
};
|
|
1317
|
+
return {
|
|
1318
|
+
score: 0,
|
|
1319
|
+
reasoning: UNPARSEABLE_REASONING,
|
|
1320
|
+
raw: trimmed
|
|
1321
|
+
};
|
|
1322
|
+
}
|
|
1323
|
+
/** Strip a leading/trailing markdown code fence (``` or ```json). */
|
|
1324
|
+
function stripCodeFences(text) {
|
|
1325
|
+
const fence = text.match(/^```(?:json)?[ \t]*\n([\s\S]*?)\n```$/i);
|
|
1326
|
+
return fence ? fence[1].trim() : text;
|
|
1327
|
+
}
|
|
1328
|
+
/** Extract the first balanced `{...}` object, ignoring braces inside strings. */
|
|
1329
|
+
function extractJsonObject(text) {
|
|
1330
|
+
const start = text.indexOf("{");
|
|
1331
|
+
if (start === -1) return void 0;
|
|
1332
|
+
let depth = 0;
|
|
1333
|
+
let inString = false;
|
|
1334
|
+
let escaped = false;
|
|
1335
|
+
for (let i = start; i < text.length; i++) {
|
|
1336
|
+
const ch = text[i];
|
|
1337
|
+
if (inString) {
|
|
1338
|
+
if (escaped) escaped = false;
|
|
1339
|
+
else if (ch === "\\") escaped = true;
|
|
1340
|
+
else if (ch === "\"") inString = false;
|
|
1341
|
+
continue;
|
|
1342
|
+
}
|
|
1343
|
+
if (ch === "\"") inString = true;
|
|
1344
|
+
else if (ch === "{") depth++;
|
|
1345
|
+
else if (ch === "}") {
|
|
1346
|
+
depth--;
|
|
1347
|
+
if (depth === 0) return text.slice(start, i + 1);
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
function clampScore(value) {
|
|
1352
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
1353
|
+
if (!Number.isFinite(n)) return 0;
|
|
1354
|
+
return Math.max(0, Math.min(1, n));
|
|
1355
|
+
}
|
|
1356
|
+
//#endregion
|
|
1357
|
+
//#region src/eval-registry.ts
|
|
1358
|
+
const evalRegistry = [];
|
|
1359
|
+
const STUB_PROVIDER = {
|
|
1360
|
+
name: "stub",
|
|
1361
|
+
meta: { defaultModel: "stub" },
|
|
1362
|
+
formatTools: (tools) => tools,
|
|
1363
|
+
userMessage: () => ({
|
|
1364
|
+
role: "user",
|
|
1365
|
+
content: []
|
|
1366
|
+
}),
|
|
1367
|
+
assistantMessage: () => ({
|
|
1368
|
+
role: "assistant",
|
|
1369
|
+
content: []
|
|
1370
|
+
}),
|
|
1371
|
+
toolResultsMessage: () => ({
|
|
1372
|
+
role: "user",
|
|
1373
|
+
content: []
|
|
1374
|
+
}),
|
|
1375
|
+
stream: async () => {
|
|
1376
|
+
throw new Error("stub provider is not runnable");
|
|
1377
|
+
}
|
|
1378
|
+
};
|
|
1379
|
+
/**
|
|
1380
|
+
* Register an eval definition. Eval files call this at module load; the harness
|
|
1381
|
+
* (`buildRegisteredEvals`) materializes them with a provider at run time. The
|
|
1382
|
+
* factory keeps definitions lazy so a single registry serves any provider/judge
|
|
1383
|
+
* pairing without re-importing eval files.
|
|
1384
|
+
*
|
|
1385
|
+
* Returns the same factory so a file can also `export default` it for direct,
|
|
1386
|
+
* registry-free use (e.g. hermetic unit tests).
|
|
1387
|
+
*/
|
|
1388
|
+
function defineEval(define) {
|
|
1389
|
+
const id = safeRegistryId(define);
|
|
1390
|
+
if (!evalRegistry.some((entry) => entry.id === id)) evalRegistry.push({
|
|
1391
|
+
id,
|
|
1392
|
+
define
|
|
1393
|
+
});
|
|
1394
|
+
return define;
|
|
1395
|
+
}
|
|
1396
|
+
/** Snapshot of every registered eval, materialized for one provider/judge pair. */
|
|
1397
|
+
function buildRegisteredEvals(ctx) {
|
|
1398
|
+
return evalRegistry.map((entry) => entry.define(ctx));
|
|
1399
|
+
}
|
|
1400
|
+
/** Drop every registered eval — test isolation helper. */
|
|
1401
|
+
function clearRegisteredEvals() {
|
|
1402
|
+
evalRegistry.length = 0;
|
|
1403
|
+
}
|
|
1404
|
+
function safeRegistryId(define) {
|
|
1405
|
+
try {
|
|
1406
|
+
const stub = define({
|
|
1407
|
+
provider: STUB_PROVIDER,
|
|
1408
|
+
judge: STUB_PROVIDER
|
|
1409
|
+
});
|
|
1410
|
+
return `${stub.suite ?? "eval"}/${stub.id}`;
|
|
1411
|
+
} catch {
|
|
1412
|
+
return `eval/${evalRegistry.length}`;
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
//#endregion
|
|
1416
|
+
//#region src/eval.ts
|
|
1417
|
+
async function runEvalCase(options) {
|
|
1418
|
+
const { id, suite, variant, tags = [], artifactDir, workspace, scorers = [], onEvent, ...headless } = options;
|
|
1419
|
+
const { metrics: declaredMetrics, sourceFile, ...headlessRest } = headless;
|
|
1420
|
+
const events = [];
|
|
1421
|
+
const caseArtifactDir = artifactDir ? join(artifactDir, ...variant ? [safeSegment(variant)] : [], safeSegment(suite ?? "eval"), safeSegment(id)) : void 0;
|
|
1422
|
+
const workspaceState = await prepareWorkspace(headlessRest.execution, workspace);
|
|
1423
|
+
const caseMetricIds = new Set(Object.keys(declaredMetrics ?? {}));
|
|
1424
|
+
const hasCaseMetrics = caseMetricIds.size > 0;
|
|
1425
|
+
const metricSpecs = {
|
|
1426
|
+
...EFFICIENCY_METRICS,
|
|
1427
|
+
...declaredMetrics
|
|
1428
|
+
};
|
|
1429
|
+
const rawMetrics = /* @__PURE__ */ new Map();
|
|
1430
|
+
const emitMetric = (metricId, raw) => {
|
|
1431
|
+
if (!(metricId in metricSpecs)) throw new EvalMetricError(`Eval ${id}: metric "${metricId}" emitted but not declared in defineMetrics`);
|
|
1432
|
+
if (!Number.isFinite(raw)) throw new EvalMetricError(`Eval ${id}: metric "${metricId}" emitted a non-finite value (${raw}); scores require finite numbers`);
|
|
1433
|
+
rawMetrics.set(metricId, raw);
|
|
1434
|
+
};
|
|
1435
|
+
try {
|
|
1436
|
+
const result = await runHeadless({
|
|
1437
|
+
...headlessRest,
|
|
1438
|
+
...workspaceState.execution ? { execution: workspaceState.execution } : {},
|
|
1439
|
+
onEvent(event) {
|
|
1440
|
+
events.push(event);
|
|
1441
|
+
onEvent?.(event);
|
|
1442
|
+
}
|
|
1443
|
+
});
|
|
1444
|
+
emitEfficiencyMetrics(emitMetric, result);
|
|
1445
|
+
const workspaceSnapshot = workspaceState.snapshot();
|
|
1446
|
+
const workspaceError = workspaceState.error();
|
|
1447
|
+
const scores = await runScorers({
|
|
1448
|
+
id,
|
|
1449
|
+
...suite ? { suite } : {},
|
|
1450
|
+
tags,
|
|
1451
|
+
result,
|
|
1452
|
+
events,
|
|
1453
|
+
metric: emitMetric,
|
|
1454
|
+
...workspaceSnapshot ? { workspace: workspaceSnapshot } : {},
|
|
1455
|
+
...caseArtifactDir ? { artifactDir: caseArtifactDir } : {}
|
|
1456
|
+
}, scorers);
|
|
1457
|
+
const metrics = finalizeEvalMetrics(metricSpecs, rawMetrics);
|
|
1458
|
+
const tagScores = computeEvalTagScores(metrics);
|
|
1459
|
+
const caseMetrics = metrics.filter((metric) => caseMetricIds.has(metric.id));
|
|
1460
|
+
const score = hasCaseMetrics && caseMetrics.length > 0 ? meanNormalized(caseMetrics) : aggregateScore(scores);
|
|
1461
|
+
const evalResult = {
|
|
1462
|
+
id,
|
|
1463
|
+
...suite ? { suite } : {},
|
|
1464
|
+
...variant ? { variant } : {},
|
|
1465
|
+
tags,
|
|
1466
|
+
result,
|
|
1467
|
+
score,
|
|
1468
|
+
passed: result.status === "completed" && scores.every((score) => score.passed),
|
|
1469
|
+
scores,
|
|
1470
|
+
metrics,
|
|
1471
|
+
tagScores,
|
|
1472
|
+
trajectory: buildTrajectory(result.transcript),
|
|
1473
|
+
events,
|
|
1474
|
+
...sourceFile ? { sourceFile } : {},
|
|
1475
|
+
...workspaceSnapshot ? { workspace: workspaceSnapshot } : {},
|
|
1476
|
+
...workspaceError ? { workspaceError } : {}
|
|
1477
|
+
};
|
|
1478
|
+
const artifacts = caseArtifactDir ? await writeEvalArtifacts(caseArtifactDir, evalResult) : void 0;
|
|
1479
|
+
return {
|
|
1480
|
+
...evalResult,
|
|
1481
|
+
...artifacts ? { artifacts } : {}
|
|
1482
|
+
};
|
|
1483
|
+
} finally {
|
|
1484
|
+
await workspaceState.cleanup();
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
/**
|
|
1488
|
+
* Register one test per eval case against a test runner, funnel every result
|
|
1489
|
+
* into a shared reporter, and print the aggregated summary once after all cases.
|
|
1490
|
+
*
|
|
1491
|
+
* A failing test means the agent run itself broke (provider/tool/timeout). Low
|
|
1492
|
+
* scores are reported, not asserted — see {@link RegisterEvalTestsOptions.failOnIncomplete}.
|
|
1493
|
+
*/
|
|
1494
|
+
function registerEvalTests(options) {
|
|
1495
|
+
const reporter = options.reporter ?? createEvalRunReporter();
|
|
1496
|
+
const failOnIncomplete = options.failOnIncomplete ?? true;
|
|
1497
|
+
const printSummary = options.printSummary ?? true;
|
|
1498
|
+
const repeat = Math.max(1, options.repeat ?? 1);
|
|
1499
|
+
const concurrency = Math.max(1, options.concurrency ?? 1);
|
|
1500
|
+
options.runner.afterAll(async () => {
|
|
1501
|
+
await reporter.flush();
|
|
1502
|
+
if (printSummary) console.log(`\n${reporter.format()}\n`);
|
|
1503
|
+
if (options.dispose) await options.dispose();
|
|
1504
|
+
});
|
|
1505
|
+
const work = [];
|
|
1506
|
+
for (const evalCase of options.cases) {
|
|
1507
|
+
const caseLabel = `${evalCase.suite ? `${evalCase.suite}/` : ""}${evalCase.id}`;
|
|
1508
|
+
const baseLabel = evalCase.variant ? `[${evalCase.variant}] ${caseLabel}` : caseLabel;
|
|
1509
|
+
for (let run = 1; run <= repeat; run++) {
|
|
1510
|
+
const label = repeat > 1 ? `${baseLabel} #${run}` : baseLabel;
|
|
1511
|
+
const caseId = repeat > 1 ? `${evalCase.id}-repeat-${run}` : evalCase.id;
|
|
1512
|
+
work.push({
|
|
1513
|
+
label,
|
|
1514
|
+
run: () => runEvalCase({
|
|
1515
|
+
...evalCase,
|
|
1516
|
+
id: caseId,
|
|
1517
|
+
...options.artifactDir ? { artifactDir: options.artifactDir } : {}
|
|
1518
|
+
})
|
|
1519
|
+
});
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
const schedule = createLazyPool(concurrency);
|
|
1523
|
+
work.forEach((w) => {
|
|
1524
|
+
options.runner.it(w.label, async () => {
|
|
1525
|
+
const result = await schedule(w.run);
|
|
1526
|
+
await reporter.record(result);
|
|
1527
|
+
if (failOnIncomplete && result.result.status !== "completed") throw new Error(`Eval ${w.label} did not complete: ${JSON.stringify(result.result.error ?? { status: result.result.status }, null, 2)}`);
|
|
1528
|
+
});
|
|
1529
|
+
});
|
|
1530
|
+
return reporter;
|
|
1531
|
+
}
|
|
1532
|
+
/**
|
|
1533
|
+
* Lazily run tasks with a bounded concurrency `limit`. Nothing starts until a
|
|
1534
|
+
* runner invokes the corresponding test callback, so filtered/skipped tests do
|
|
1535
|
+
* not leak model or Docker work in the background.
|
|
1536
|
+
*/
|
|
1537
|
+
function createLazyPool(limit) {
|
|
1538
|
+
const queue = [];
|
|
1539
|
+
let active = 0;
|
|
1540
|
+
const drain = () => {
|
|
1541
|
+
const available = Math.min(limit - active, queue.length);
|
|
1542
|
+
for (let i = 0; i < available; i++) queue.shift()?.();
|
|
1543
|
+
};
|
|
1544
|
+
return (task) => new Promise((resolve, reject) => {
|
|
1545
|
+
const run = async () => {
|
|
1546
|
+
active++;
|
|
1547
|
+
try {
|
|
1548
|
+
resolve(await task());
|
|
1549
|
+
} catch (err) {
|
|
1550
|
+
reject(err);
|
|
1551
|
+
} finally {
|
|
1552
|
+
active--;
|
|
1553
|
+
drain();
|
|
1554
|
+
}
|
|
1555
|
+
};
|
|
1556
|
+
queue.push(() => void run());
|
|
1557
|
+
drain();
|
|
1558
|
+
});
|
|
1559
|
+
}
|
|
1560
|
+
function createEvalRunReporter(options = {}) {
|
|
1561
|
+
const results = [];
|
|
1562
|
+
return {
|
|
1563
|
+
get results() {
|
|
1564
|
+
return results;
|
|
1565
|
+
},
|
|
1566
|
+
async record(result) {
|
|
1567
|
+
results.push(result);
|
|
1568
|
+
if (options.outputDir) {
|
|
1569
|
+
const segments = [
|
|
1570
|
+
...result.variant ? [safeSegment(result.variant)] : [],
|
|
1571
|
+
safeSegment(result.suite ?? "eval"),
|
|
1572
|
+
safeSegment(result.id)
|
|
1573
|
+
];
|
|
1574
|
+
const casePath = join(options.outputDir, "cases", `${segments.join("--")}.json`);
|
|
1575
|
+
await mkdir(join(options.outputDir, "cases"), { recursive: true });
|
|
1576
|
+
await writeFile(casePath, `${JSON.stringify(result, null, 2)}\n`);
|
|
1577
|
+
}
|
|
1578
|
+
},
|
|
1579
|
+
async flush() {
|
|
1580
|
+
const summary = buildEvalRunSummary(results);
|
|
1581
|
+
if (options.outputDir) {
|
|
1582
|
+
await mkdir(options.outputDir, { recursive: true });
|
|
1583
|
+
await writeFile(join(options.outputDir, "run-summary.json"), `${JSON.stringify(summary, null, 2)}\n`);
|
|
1584
|
+
}
|
|
1585
|
+
return summary;
|
|
1586
|
+
},
|
|
1587
|
+
format() {
|
|
1588
|
+
return formatEvalRunSummaryWithOptions(results, { color: options.color });
|
|
1589
|
+
}
|
|
1590
|
+
};
|
|
1591
|
+
}
|
|
1592
|
+
//#endregion
|
|
1593
|
+
export { relativeArtifactPath as A, computeEvalTagScores as C, finalizeEvalMetrics as D, emitEfficiencyMetrics as E, normalizeMetric as O, EvalMetricError as S, efficiencyMetricValues as T, statusCompleted as _, clearRegisteredEvals as a, buildEvalRunSummary as b, createEvalAgent as c, formatTrajectoryLine as d, fileContains as f, functionalityMetric as g, fileExistsOneOf as h, buildRegisteredEvals as i, artifactPath as k, createReusableExecutionContext as l, fileExists as m, registerEvalTests as n, defineEval as o, fileContentQuality as p, runEvalCase as r, llmJudge as s, createEvalRunReporter as t, buildTrajectory as u, formatEvalCaseSummary as v, defineMetrics as w, EFFICIENCY_METRICS as x, formatEvalRunSummary as y };
|
|
1594
|
+
|
|
1595
|
+
//# sourceMappingURL=eval-BNcvle_J.js.map
|