stageflow 0.40.0 → 0.42.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 +3 -1
- package/dist/agents/authorChat.d.ts +4 -0
- package/dist/agents/authorChat.js +113 -12
- package/dist/agents/authorSessionStore.d.ts +35 -0
- package/dist/agents/authorSessionStore.js +152 -0
- package/dist/agents/chatTurn.d.ts +2 -0
- package/dist/agents/chatTurn.js +17 -1
- package/dist/agents/errors.d.ts +1 -1
- package/dist/agents/errors.js +1 -0
- package/dist/agents/evalGate.d.ts +61 -0
- package/dist/agents/evalGate.js +103 -0
- package/dist/agents/evalSession.d.ts +26 -0
- package/dist/agents/evalSession.js +29 -0
- package/dist/agents/hostContext.d.ts +3 -0
- package/dist/agents/hostContext.js +4 -1
- package/dist/agents/runtime/piRuntime.d.ts +1 -1
- package/dist/agents/runtime/piRuntime.js +4 -4
- package/dist/agents/runtime/types.d.ts +3 -1
- package/dist/agents/service.d.ts +27 -2
- package/dist/agents/service.js +88 -13
- package/dist/agents/sessions.d.ts +8 -0
- package/dist/agents/sessions.js +50 -10
- package/dist/agents/store.d.ts +14 -3
- package/dist/agents/store.js +45 -7
- package/dist/agents/toolRisk.d.ts +21 -0
- package/dist/agents/toolRisk.js +50 -0
- package/dist/cli/doctorCommand.js +26 -0
- package/dist/cli/evalCommand.d.ts +20 -0
- package/dist/cli/evalCommand.js +720 -0
- package/dist/cli/exportAllCommand.js +1 -1
- package/dist/cli/hostClient.d.ts +60 -0
- package/dist/cli/hostClient.js +71 -0
- package/dist/cli/validateOutput.d.ts +2 -2
- package/dist/cli/validateOutput.js +2 -2
- package/dist/cli.js +10 -0
- package/dist/config/atomicWrite.d.ts +15 -0
- package/dist/config/atomicWrite.js +43 -0
- package/dist/config/browseCatalog.d.ts +2 -0
- package/dist/config/browseCatalog.js +4 -2
- package/dist/config/csv.d.ts +9 -0
- package/dist/config/csv.js +81 -0
- package/dist/config/datasetAgentCheck.d.ts +18 -0
- package/dist/config/datasetAgentCheck.js +109 -0
- package/dist/config/datasetCandidates.d.ts +22 -0
- package/dist/config/datasetCandidates.js +86 -0
- package/dist/config/datasetFromSessions.d.ts +34 -0
- package/dist/config/datasetFromSessions.js +105 -0
- package/dist/config/datasetHash.d.ts +1 -0
- package/dist/config/datasetHash.js +4 -0
- package/dist/config/datasetImport.d.ts +21 -0
- package/dist/config/datasetImport.js +120 -0
- package/dist/config/datasetStageCheck.d.ts +9 -0
- package/dist/config/datasetStageCheck.js +17 -0
- package/dist/config/datasetTypes.d.ts +27 -0
- package/dist/config/datasetTypes.js +1 -0
- package/dist/config/ensureCatalogScanRoot.d.ts +2 -1
- package/dist/config/ensureCatalogScanRoot.js +2 -6
- package/dist/config/evalCatalog.d.ts +22 -0
- package/dist/config/evalCatalog.js +67 -0
- package/dist/config/evalWriteRoots.d.ts +17 -0
- package/dist/config/evalWriteRoots.js +109 -0
- package/dist/config/hostConfig.js +1 -0
- package/dist/config/installEvalSample.d.ts +34 -0
- package/dist/config/installEvalSample.js +182 -0
- package/dist/config/loadDataset.d.ts +16 -0
- package/dist/config/loadDataset.js +318 -0
- package/dist/config/loadEval.d.ts +32 -0
- package/dist/config/loadEval.js +1085 -0
- package/dist/config/loadOutcome.d.ts +5 -0
- package/dist/config/loadStageflowManifest.js +64 -0
- package/dist/config/resolveAgentTarget.d.ts +32 -0
- package/dist/config/resolveAgentTarget.js +118 -0
- package/dist/config/resolveEvalTarget.d.ts +21 -0
- package/dist/config/resolveEvalTarget.js +89 -0
- package/dist/config/scanCatalogPaths.d.ts +1 -1
- package/dist/config/scanCatalogPaths.js +17 -5
- package/dist/config/validateCatalog.d.ts +1 -1
- package/dist/config/validateCatalog.js +9 -1
- package/dist/config/validateEval.d.ts +36 -0
- package/dist/config/validateEval.js +405 -0
- package/dist/config/writeDataset.d.ts +54 -0
- package/dist/config/writeDataset.js +186 -0
- package/dist/config/writeEval.d.ts +25 -0
- package/dist/config/writeEval.js +398 -0
- package/dist/diagnostics/checks.js +1 -1
- package/dist/evals/agentDiff.d.ts +13 -0
- package/dist/evals/agentDiff.js +11 -0
- package/dist/evals/canonical.d.ts +3 -0
- package/dist/evals/canonical.js +24 -0
- package/dist/evals/caseTask.d.ts +3 -0
- package/dist/evals/caseTask.js +9 -0
- package/dist/evals/compare.d.ts +94 -0
- package/dist/evals/compare.js +201 -0
- package/dist/evals/constants.d.ts +60 -0
- package/dist/evals/constants.js +95 -0
- package/dist/evals/engine/FakeEngine.d.ts +33 -0
- package/dist/evals/engine/FakeEngine.js +176 -0
- package/dist/evals/engine/index.d.ts +9 -0
- package/dist/evals/engine/index.js +19 -0
- package/dist/evals/engine/promptfoo/PromptfooEngine.d.ts +52 -0
- package/dist/evals/engine/promptfoo/PromptfooEngine.js +311 -0
- package/dist/evals/engine/promptfoo/errors.d.ts +5 -0
- package/dist/evals/engine/promptfoo/errors.js +8 -0
- package/dist/evals/engine/promptfoo/generate.d.ts +30 -0
- package/dist/evals/engine/promptfoo/generate.js +342 -0
- package/dist/evals/engine/promptfoo/guard.d.ts +10 -0
- package/dist/evals/engine/promptfoo/guard.js +116 -0
- package/dist/evals/engine/promptfoo/install.d.ts +17 -0
- package/dist/evals/engine/promptfoo/install.js +73 -0
- package/dist/evals/engine/promptfoo/judgeProviders.d.ts +2 -0
- package/dist/evals/engine/promptfoo/judgeProviders.js +9 -0
- package/dist/evals/engine/promptfoo/judgeServices.d.ts +6 -0
- package/dist/evals/engine/promptfoo/judgeServices.js +41 -0
- package/dist/evals/engine/promptfoo/locate.d.ts +21 -0
- package/dist/evals/engine/promptfoo/locate.js +68 -0
- package/dist/evals/engine/promptfoo/nodeVersion.d.ts +7 -0
- package/dist/evals/engine/promptfoo/nodeVersion.js +14 -0
- package/dist/evals/engine/promptfoo/normalize.d.ts +66 -0
- package/dist/evals/engine/promptfoo/normalize.js +261 -0
- package/dist/evals/engine/promptfoo/pinned.d.ts +5 -0
- package/dist/evals/engine/promptfoo/pinned.js +5 -0
- package/dist/evals/engine/promptfoo/rubricPrompt.d.ts +14 -0
- package/dist/evals/engine/promptfoo/rubricPrompt.js +103 -0
- package/dist/evals/engine/promptfoo/runTokens.d.ts +18 -0
- package/dist/evals/engine/promptfoo/runTokens.js +31 -0
- package/dist/evals/engine/promptfoo/status.d.ts +13 -0
- package/dist/evals/engine/promptfoo/status.js +16 -0
- package/dist/evals/estimate.d.ts +56 -0
- package/dist/evals/estimate.js +66 -0
- package/dist/evals/graders/codeGrader.d.ts +6 -0
- package/dist/evals/graders/codeGrader.js +85 -0
- package/dist/evals/graders/conversationCheckGrader.d.ts +6 -0
- package/dist/evals/graders/conversationCheckGrader.js +62 -0
- package/dist/evals/graders/fieldGrader.d.ts +11 -0
- package/dist/evals/graders/fieldGrader.js +117 -0
- package/dist/evals/graders/goal.d.ts +3 -0
- package/dist/evals/graders/goal.js +14 -0
- package/dist/evals/graders/index.d.ts +18 -0
- package/dist/evals/graders/index.js +34 -0
- package/dist/evals/graders/notApplicable.d.ts +5 -0
- package/dist/evals/graders/notApplicable.js +9 -0
- package/dist/evals/graders/path.d.ts +26 -0
- package/dist/evals/graders/path.js +75 -0
- package/dist/evals/graders/referenceGrader.d.ts +5 -0
- package/dist/evals/graders/referenceGrader.js +59 -0
- package/dist/evals/graders/schemaGrader.d.ts +5 -0
- package/dist/evals/graders/schemaGrader.js +29 -0
- package/dist/evals/graders/subject.d.ts +6 -0
- package/dist/evals/graders/subject.js +33 -0
- package/dist/evals/graders/toolCallsGrader.d.ts +6 -0
- package/dist/evals/graders/toolCallsGrader.js +100 -0
- package/dist/evals/graders/transcript.d.ts +12 -0
- package/dist/evals/graders/transcript.js +57 -0
- package/dist/evals/graders/types.d.ts +20 -0
- package/dist/evals/graders/types.js +1 -0
- package/dist/evals/judgeProviders.d.ts +16 -0
- package/dist/evals/judgeProviders.js +27 -0
- package/dist/evals/lineDiff.d.ts +5 -0
- package/dist/evals/lineDiff.js +34 -0
- package/dist/evals/listStatus.d.ts +31 -0
- package/dist/evals/listStatus.js +48 -0
- package/dist/evals/manager.d.ts +174 -0
- package/dist/evals/manager.js +787 -0
- package/dist/evals/plan.d.ts +58 -0
- package/dist/evals/plan.js +602 -0
- package/dist/evals/providerKeys.d.ts +1 -0
- package/dist/evals/providerKeys.js +21 -0
- package/dist/evals/read/agentTargets.d.ts +37 -0
- package/dist/evals/read/agentTargets.js +101 -0
- package/dist/evals/read/api.d.ts +196 -0
- package/dist/evals/read/api.js +358 -0
- package/dist/evals/read/compare.d.ts +4 -0
- package/dist/evals/read/compare.js +39 -0
- package/dist/evals/read/datasets.d.ts +105 -0
- package/dist/evals/read/datasets.js +318 -0
- package/dist/evals/read/list.d.ts +66 -0
- package/dist/evals/read/list.js +173 -0
- package/dist/evals/read/results.d.ts +46 -0
- package/dist/evals/read/results.js +107 -0
- package/dist/evals/read/runs.d.ts +149 -0
- package/dist/evals/read/runs.js +289 -0
- package/dist/evals/read/suggestions.d.ts +43 -0
- package/dist/evals/read/suggestions.js +73 -0
- package/dist/evals/runner/FakeAgentBackend.d.ts +40 -0
- package/dist/evals/runner/FakeAgentBackend.js +82 -0
- package/dist/evals/runner/agentCaseRunner.d.ts +24 -0
- package/dist/evals/runner/agentCaseRunner.js +388 -0
- package/dist/evals/runner/caseRunner.d.ts +7 -0
- package/dist/evals/runner/caseRunner.js +17 -0
- package/dist/evals/runner/conversationSubject.d.ts +18 -0
- package/dist/evals/runner/conversationSubject.js +123 -0
- package/dist/evals/runner/simulatorModel.d.ts +51 -0
- package/dist/evals/runner/simulatorModel.js +58 -0
- package/dist/evals/runner/simulatorPrompt.d.ts +29 -0
- package/dist/evals/runner/simulatorPrompt.js +59 -0
- package/dist/evals/runner/stageCaseRunner.d.ts +30 -0
- package/dist/evals/runner/stageCaseRunner.js +279 -0
- package/dist/evals/runner/userDriver.d.ts +71 -0
- package/dist/evals/runner/userDriver.js +104 -0
- package/dist/evals/scoring.d.ts +34 -0
- package/dist/evals/scoring.js +67 -0
- package/dist/evals/snapshot.d.ts +183 -0
- package/dist/evals/snapshot.js +65 -0
- package/dist/evals/stageDiff.d.ts +8 -0
- package/dist/evals/stageDiff.js +48 -0
- package/dist/evals/stats.d.ts +95 -0
- package/dist/evals/stats.js +220 -0
- package/dist/evals/statsAdapter.d.ts +4 -0
- package/dist/evals/statsAdapter.js +30 -0
- package/dist/evals/statsTypes.d.ts +51 -0
- package/dist/evals/statsTypes.js +18 -0
- package/dist/evals/storeError.d.ts +5 -0
- package/dist/evals/storeError.js +8 -0
- package/dist/evals/storeTypes.d.ts +207 -0
- package/dist/evals/storeTypes.js +1 -0
- package/dist/evals/tTable.d.ts +3 -0
- package/dist/evals/tTable.js +15 -0
- package/dist/evals/triage/constants.d.ts +26 -0
- package/dist/evals/triage/constants.js +33 -0
- package/dist/evals/triage/credentials.d.ts +13 -0
- package/dist/evals/triage/credentials.js +20 -0
- package/dist/evals/triage/jevClassifier.d.ts +14 -0
- package/dist/evals/triage/jevClassifier.js +124 -0
- package/dist/evals/triage/judgeClassifier.d.ts +10 -0
- package/dist/evals/triage/judgeClassifier.js +76 -0
- package/dist/evals/triage/prompts.d.ts +16 -0
- package/dist/evals/triage/prompts.js +64 -0
- package/dist/evals/triage/service.d.ts +79 -0
- package/dist/evals/triage/service.js +217 -0
- package/dist/evals/triage/stateBuilder.d.ts +19 -0
- package/dist/evals/triage/stateBuilder.js +125 -0
- package/dist/evals/triage/types.d.ts +60 -0
- package/dist/evals/triage/types.js +8 -0
- package/dist/evals/types.d.ts +347 -0
- package/dist/evals/types.js +1 -0
- package/dist/mcp/deps.d.ts +7 -0
- package/dist/mcp/evalTools.d.ts +3 -0
- package/dist/mcp/evalTools.js +280 -0
- package/dist/mcp/server.js +1 -0
- package/dist/mcp/tools.js +2 -0
- package/dist/models/response.d.ts +7 -2
- package/dist/models/response.js +6 -1
- package/dist/operatorAgent/agentAuthorContext.d.ts +1 -1
- package/dist/operatorAgent/agentAuthorContext.js +1 -1
- package/dist/operatorAgent/piModel.js +17 -4
- package/dist/operatorAgent/profiles/agentAuthor.d.ts +3 -1
- package/dist/operatorAgent/profiles/agentAuthor.js +145 -20
- package/dist/operatorAgent/profiles/agentAuthorPlaybook.d.ts +1 -1
- package/dist/operatorAgent/profiles/agentAuthorPlaybook.js +48 -15
- package/dist/operatorAgent/profiles/authorReadable.d.ts +2 -0
- package/dist/operatorAgent/profiles/authorReadable.js +26 -0
- package/dist/package-meta.d.ts +1 -1
- package/dist/package-meta.js +1 -1
- package/dist/runstore/port.d.ts +43 -0
- package/dist/runstore/sqlite/SqliteRunStore.d.ts +28 -0
- package/dist/runstore/sqlite/SqliteRunStore.js +99 -5
- package/dist/runstore/sqlite/evalStore.d.ts +20 -0
- package/dist/runstore/sqlite/evalStore.js +541 -0
- package/dist/runstore/sqlite/migrations/015-evals.d.ts +12 -0
- package/dist/runstore/sqlite/migrations/015-evals.js +157 -0
- package/dist/runstore/sqlite/migrations/016-eval-subject-ref.d.ts +12 -0
- package/dist/runstore/sqlite/migrations/016-eval-subject-ref.js +20 -0
- package/dist/runstore/sqlite/migrations/017-eval-result-suggestions.d.ts +11 -0
- package/dist/runstore/sqlite/migrations/017-eval-result-suggestions.js +36 -0
- package/dist/runstore/sqlite/migrations/index.js +6 -0
- package/dist/runstore/sqlite/suggestionStore.d.ts +14 -0
- package/dist/runstore/sqlite/suggestionStore.js +132 -0
- package/dist/runtime/pipelineRunner.d.ts +2 -1
- package/dist/runtime/pipelineRunner.js +2 -0
- package/dist/runtime/runManager.d.ts +2 -1
- package/dist/runtime/runManager.js +8 -6
- package/dist/runtime/runRetentionSweep.js +2 -2
- package/dist/server/agentAuthorRoutes.js +50 -1
- package/dist/server/bootstrap.d.ts +8 -0
- package/dist/server/bootstrap.js +56 -0
- package/dist/server/controlToken.js +3 -0
- package/dist/server/evalInternalRoutes.d.ts +14 -0
- package/dist/server/evalInternalRoutes.js +87 -0
- package/dist/server/evalRouteContext.d.ts +46 -0
- package/dist/server/evalRouteContext.js +43 -0
- package/dist/server/evalRoutes.d.ts +5 -0
- package/dist/server/evalRoutes.js +1427 -0
- package/dist/server/evalTriageRoutes.d.ts +2 -0
- package/dist/server/evalTriageRoutes.js +95 -0
- package/dist/server/http.d.ts +5 -0
- package/dist/server/http.js +32 -2
- package/dist/server/mcpHost.js +3 -0
- package/dist/toolkit/meta.d.ts +1 -1
- package/dist/toolkit/meta.js +15 -0
- package/dist/toolkit/profiles.js +1 -1
- package/dist/types/stageflowManifest.d.ts +6 -0
- package/dist/ui/assets/AssistantRuntimeProvider-qIX1xNoH.js +9 -0
- package/dist/ui/assets/{MarkdownText-D7tYaIFO.js → MarkdownText-BTrOqu5J.js} +1 -1
- package/dist/ui/assets/chat-Df_wCpak.js +5 -0
- package/dist/ui/assets/chat-P7KQznHm.css +2 -0
- package/dist/ui/assets/{main-BFo79krC.css → main-CHlmTNJe.css} +1 -1
- package/dist/ui/assets/main-CvVfxmMd.js +271 -0
- package/dist/ui/chat.html +3 -3
- package/dist/ui/index.html +3 -3
- package/examples/README.md +2 -0
- package/examples/document-policy-review/README.md +35 -0
- package/examples/document-policy-review/agents/compliance.agent.yaml +42 -0
- package/examples/document-policy-review/documents/acme-vendor-sow.md +15 -0
- package/examples/document-policy-review/documents/support-runbook.md +13 -0
- package/examples/document-policy-review/policies/acceptable-use.md +13 -0
- package/examples/document-policy-review/policies/data-handling.md +17 -0
- package/examples/document-policy-review/policies/vendor-security.md +17 -0
- package/examples/evals/README.md +123 -0
- package/examples/evals/critique-sample.dataset.jsonl +8 -0
- package/examples/evals/critique-sample.eval.yaml +34 -0
- package/examples/evals/critique-sample.task.yaml +4 -0
- package/examples/evals/eval-sample.pipeline.yaml +35 -0
- package/examples/evals/support-sample.agent.yaml +20 -0
- package/examples/evals/support-sample.dataset.jsonl +6 -0
- package/examples/evals/support-sample.eval.yaml +38 -0
- package/examples/stageflow.yaml +6 -0
- package/package.json +1 -1
- package/skills/stageflow/SKILL.md +5 -2
- package/skills/stageflow/references/control-surface.md +4 -2
- package/skills/stageflow/references/evals.md +68 -0
- package/skills/stageflow-author/SKILL.md +4 -0
- package/skills/stageflow-run/SKILL.md +4 -0
- package/dist/ui/assets/AssistantRuntimeProvider-CsiPFhCT.js +0 -9
- package/dist/ui/assets/chat-BbLukq9W.js +0 -5
- package/dist/ui/assets/chat-zIZU_bP1.css +0 -2
- package/dist/ui/assets/main-CijYMeM0.js +0 -259
|
@@ -0,0 +1,1085 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { parse as parseYaml } from "yaml";
|
|
4
|
+
import { AGENT_ID_PATTERN, isReservedAgentId } from "../agents/agentId.js";
|
|
5
|
+
import { canonicalJson, hash6, sha256Hex } from "../evals/canonical.js";
|
|
6
|
+
import { loadFailure, loadSuccess } from "./loadOutcome.js";
|
|
7
|
+
import { yamlParsePosition } from "./yamlParsePosition.js";
|
|
8
|
+
export const EVAL_DEFAULT_REPEATS = 3;
|
|
9
|
+
export const EVAL_DEFAULT_PASS_THRESHOLD = 0.8;
|
|
10
|
+
export const EVAL_DEFAULT_MAX_COST_USD = 2;
|
|
11
|
+
export const EVAL_DEFAULT_CONCURRENCY = 4;
|
|
12
|
+
export const EVAL_AGENT_DEFAULT_CONCURRENCY = 2;
|
|
13
|
+
export const EVAL_AGENT_DEFAULT_MAX_COST_USD = 1;
|
|
14
|
+
export const EVAL_CONVERSATION_DEFAULTS = {
|
|
15
|
+
mode: "script",
|
|
16
|
+
maxTurns: 8,
|
|
17
|
+
turnTimeoutMs: 120_000,
|
|
18
|
+
tools: "read_only",
|
|
19
|
+
approvals: "record_only",
|
|
20
|
+
readOnlyTools: [],
|
|
21
|
+
allowRealTools: false,
|
|
22
|
+
};
|
|
23
|
+
export function resolveConversation(conversation) {
|
|
24
|
+
const d = EVAL_CONVERSATION_DEFAULTS;
|
|
25
|
+
return {
|
|
26
|
+
mode: conversation?.mode ?? d.mode,
|
|
27
|
+
maxTurns: conversation?.max_turns ?? d.maxTurns,
|
|
28
|
+
turnTimeoutMs: conversation?.turn_timeout_ms ?? d.turnTimeoutMs,
|
|
29
|
+
tools: conversation?.tools ?? d.tools,
|
|
30
|
+
approvals: conversation?.approvals ?? d.approvals,
|
|
31
|
+
readOnlyTools: [...(conversation?.read_only_tools ?? d.readOnlyTools)],
|
|
32
|
+
allowRealTools: conversation?.allow_real_tools ?? d.allowRealTools,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,62}$/;
|
|
36
|
+
const GRADER_TYPES = [
|
|
37
|
+
"schema",
|
|
38
|
+
"field",
|
|
39
|
+
"reference",
|
|
40
|
+
"rubric",
|
|
41
|
+
"code",
|
|
42
|
+
"tool_calls",
|
|
43
|
+
"conversation_check",
|
|
44
|
+
"goal_reached",
|
|
45
|
+
];
|
|
46
|
+
const FIELD_OPS = [
|
|
47
|
+
"exists",
|
|
48
|
+
"not_empty",
|
|
49
|
+
"equals",
|
|
50
|
+
"not_equals",
|
|
51
|
+
"contains",
|
|
52
|
+
"matches",
|
|
53
|
+
"gte",
|
|
54
|
+
"lte",
|
|
55
|
+
"between",
|
|
56
|
+
"in",
|
|
57
|
+
"length_gte",
|
|
58
|
+
"length_lte",
|
|
59
|
+
];
|
|
60
|
+
const FIELD_PATH_PATTERN = /^[^.[\]\s]+(?:\.[^.[\]\s]+|\[(?:\d+|\*)\])*$/;
|
|
61
|
+
const TOP_KEYS = [
|
|
62
|
+
"version",
|
|
63
|
+
"id",
|
|
64
|
+
"name",
|
|
65
|
+
"description",
|
|
66
|
+
"type",
|
|
67
|
+
"target",
|
|
68
|
+
"dataset",
|
|
69
|
+
"task",
|
|
70
|
+
"defaults",
|
|
71
|
+
"conversation",
|
|
72
|
+
"graders",
|
|
73
|
+
"budget",
|
|
74
|
+
];
|
|
75
|
+
const TARGET_KEYS = ["kind", "pipeline", "stage", "file"];
|
|
76
|
+
const AGENT_TARGET_KEYS = ["kind", "agent"];
|
|
77
|
+
const CONVERSATION_KEYS = [
|
|
78
|
+
"mode",
|
|
79
|
+
"max_turns",
|
|
80
|
+
"turn_timeout_ms",
|
|
81
|
+
"tools",
|
|
82
|
+
"approvals",
|
|
83
|
+
"read_only_tools",
|
|
84
|
+
"allow_real_tools",
|
|
85
|
+
];
|
|
86
|
+
const READ_ONLY_TOOL_PATTERN = /^[A-Za-z0-9_.*:-]{1,128}$/;
|
|
87
|
+
const TASK_KEYS = ["goal", "context", "constraints"];
|
|
88
|
+
const DEFAULTS_KEYS = [
|
|
89
|
+
"repeats",
|
|
90
|
+
"judge",
|
|
91
|
+
"pass_threshold",
|
|
92
|
+
"max_cost_usd",
|
|
93
|
+
"concurrency",
|
|
94
|
+
"model",
|
|
95
|
+
"user_model",
|
|
96
|
+
];
|
|
97
|
+
const BUDGET_KEYS = ["latency_ms", "cost_usd"];
|
|
98
|
+
const GRADER_COMMON_KEYS = ["id", "type", "description", "weight", "must_pass"];
|
|
99
|
+
const GRADER_TYPE_KEYS = {
|
|
100
|
+
schema: [],
|
|
101
|
+
field: ["checks"],
|
|
102
|
+
reference: ["mode", "path", "expected", "pass_at"],
|
|
103
|
+
rubric: ["rubric", "scale", "pass_at", "judge", "examples"],
|
|
104
|
+
code: ["code"],
|
|
105
|
+
tool_calls: ["must_call", "must_not_call", "must_ask_first", "max_calls", "args"],
|
|
106
|
+
conversation_check: ["max_turns", "max_agent_questions_per_turn", "final_reply"],
|
|
107
|
+
goal_reached: ["judge", "guidance"],
|
|
108
|
+
};
|
|
109
|
+
const TOOL_ARG_KEYS = ["tool", "path", "op", "value"];
|
|
110
|
+
const FINAL_REPLY_KEYS = ["op", "value"];
|
|
111
|
+
const CHECK_KEYS = ["path", "op", "value"];
|
|
112
|
+
function isPlainObject(value) {
|
|
113
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
114
|
+
}
|
|
115
|
+
function evalIssue(code, message) {
|
|
116
|
+
return { code, message, category: "eval" };
|
|
117
|
+
}
|
|
118
|
+
export function evalFileStem(filePath) {
|
|
119
|
+
const base = path.basename(filePath);
|
|
120
|
+
const match = /^(.*)\.eval\.ya?ml$/.exec(base);
|
|
121
|
+
if (match)
|
|
122
|
+
return match[1] ?? base;
|
|
123
|
+
return base.slice(0, base.length - path.extname(base).length);
|
|
124
|
+
}
|
|
125
|
+
function isInside(root, candidate) {
|
|
126
|
+
const rel = path.relative(root, candidate);
|
|
127
|
+
return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
|
|
128
|
+
}
|
|
129
|
+
/** Paths in an eval file resolve against the eval file's directory (P6). */
|
|
130
|
+
export function resolveEvalPaths(file, evalAbsPath) {
|
|
131
|
+
const dir = path.dirname(evalAbsPath);
|
|
132
|
+
const stage = file.target.kind === "stage" ? file.target : undefined;
|
|
133
|
+
return {
|
|
134
|
+
...(stage?.pipeline !== undefined ? { pipelinePath: path.resolve(dir, stage.pipeline) } : {}),
|
|
135
|
+
...(stage?.file !== undefined ? { stageFile: path.resolve(dir, stage.file) } : {}),
|
|
136
|
+
datasetPath: path.resolve(dir, file.dataset),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
class Collector {
|
|
140
|
+
issues = [];
|
|
141
|
+
add(code, message) {
|
|
142
|
+
this.issues.push(evalIssue(code, message));
|
|
143
|
+
}
|
|
144
|
+
unknownKeys(obj, allowed, where) {
|
|
145
|
+
for (const key of Object.keys(obj)) {
|
|
146
|
+
if (!allowed.includes(key)) {
|
|
147
|
+
this.add("eval.unknown_key", `Unknown key "${key}" in ${where}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function optionalString(c, obj, key, where, limits = {}) {
|
|
153
|
+
const value = obj[key];
|
|
154
|
+
if (value === undefined)
|
|
155
|
+
return undefined;
|
|
156
|
+
if (typeof value !== "string") {
|
|
157
|
+
c.add("eval.invalid_field", `${where} "${key}" must be a string`);
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
const min = limits.min ?? 0;
|
|
161
|
+
if (value.length < min || (limits.max !== undefined && value.length > limits.max)) {
|
|
162
|
+
const range = limits.max !== undefined ? `${min}-${limits.max} characters` : `at least ${min} character(s)`;
|
|
163
|
+
c.add("eval.invalid_field", `${where} "${key}" must be ${range}`);
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
return value;
|
|
167
|
+
}
|
|
168
|
+
function optionalNumber(c, obj, key, where, rule, ok, code = "eval.invalid_field") {
|
|
169
|
+
const value = obj[key];
|
|
170
|
+
if (value === undefined)
|
|
171
|
+
return undefined;
|
|
172
|
+
if (typeof value !== "number" || !Number.isFinite(value) || !ok(value)) {
|
|
173
|
+
c.add(code, `${where} "${key}" must be ${rule}`);
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
return value;
|
|
177
|
+
}
|
|
178
|
+
function isIntegerIn(n, min, max) {
|
|
179
|
+
return Number.isInteger(n) && n >= min && n <= max;
|
|
180
|
+
}
|
|
181
|
+
function parseTarget(c, raw) {
|
|
182
|
+
if (raw === undefined) {
|
|
183
|
+
c.add("eval.invalid_field", `Missing required key "target"`);
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
if (!isPlainObject(raw)) {
|
|
187
|
+
c.add("eval.invalid_field", `"target" must be a mapping`);
|
|
188
|
+
return undefined;
|
|
189
|
+
}
|
|
190
|
+
if (raw.kind === undefined) {
|
|
191
|
+
c.add("eval.invalid_field", `target "kind" is required ("stage" or "agent")`);
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
if (raw.kind === "agent")
|
|
195
|
+
return parseAgentTarget(c, raw);
|
|
196
|
+
if (raw.kind !== "stage") {
|
|
197
|
+
c.add("eval.target_kind_unsupported", `target kind ${JSON.stringify(raw.kind)} is not supported (only "stage" and "agent")`);
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
200
|
+
if (raw.agent !== undefined) {
|
|
201
|
+
c.add("eval.target_invalid", `target "agent" is only valid with kind: agent`);
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
c.unknownKeys(raw, TARGET_KEYS, "target");
|
|
205
|
+
const pipeline = raw.pipeline;
|
|
206
|
+
const stage = raw.stage;
|
|
207
|
+
const file = raw.file;
|
|
208
|
+
for (const [key, value] of [
|
|
209
|
+
["pipeline", pipeline],
|
|
210
|
+
["stage", stage],
|
|
211
|
+
["file", file],
|
|
212
|
+
]) {
|
|
213
|
+
if (value !== undefined && (typeof value !== "string" || value.trim() === "")) {
|
|
214
|
+
c.add("eval.invalid_field", `target "${key}" must be a non-empty string`);
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const pipelineForm = pipeline !== undefined || stage !== undefined;
|
|
219
|
+
const fileForm = file !== undefined;
|
|
220
|
+
if (pipelineForm === fileForm || (pipelineForm && (pipeline === undefined || stage === undefined))) {
|
|
221
|
+
c.add("eval.target_invalid", "target must be exactly one of: { kind: stage, pipeline, stage } or { kind: stage, file }");
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
kind: "stage",
|
|
226
|
+
...(pipeline !== undefined ? { pipeline: pipeline } : {}),
|
|
227
|
+
...(stage !== undefined ? { stage: stage } : {}),
|
|
228
|
+
...(file !== undefined ? { file: file } : {}),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
function parseAgentTarget(c, raw) {
|
|
232
|
+
if (raw.revision !== undefined) {
|
|
233
|
+
c.add("eval.target_invalid", `target "revision" is recorded by the run, not authored`);
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
for (const key of ["pipeline", "stage", "file"]) {
|
|
237
|
+
if (raw[key] !== undefined) {
|
|
238
|
+
c.add("eval.target_invalid", `target "${key}" is only valid with kind: stage`);
|
|
239
|
+
return undefined;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
c.unknownKeys(raw, AGENT_TARGET_KEYS, "target");
|
|
243
|
+
const agent = raw.agent;
|
|
244
|
+
if (typeof agent !== "string" || !AGENT_ID_PATTERN.test(agent)) {
|
|
245
|
+
c.add("eval.target_invalid", `target "agent" must be an agent id matching ${AGENT_ID_PATTERN.source}`);
|
|
246
|
+
return undefined;
|
|
247
|
+
}
|
|
248
|
+
if (isReservedAgentId(agent)) {
|
|
249
|
+
c.add("eval.target_invalid", `target agent id "${agent}" is reserved`);
|
|
250
|
+
return undefined;
|
|
251
|
+
}
|
|
252
|
+
return { kind: "agent", agent };
|
|
253
|
+
}
|
|
254
|
+
function parseTask(c, raw) {
|
|
255
|
+
if (raw === undefined)
|
|
256
|
+
return undefined;
|
|
257
|
+
if (!isPlainObject(raw)) {
|
|
258
|
+
c.add("eval.invalid_field", `"task" must be a mapping`);
|
|
259
|
+
return undefined;
|
|
260
|
+
}
|
|
261
|
+
c.unknownKeys(raw, TASK_KEYS, "task");
|
|
262
|
+
const out = {};
|
|
263
|
+
for (const key of TASK_KEYS) {
|
|
264
|
+
const value = optionalString(c, raw, key, "task");
|
|
265
|
+
if (value !== undefined)
|
|
266
|
+
out[key] = value;
|
|
267
|
+
}
|
|
268
|
+
return out;
|
|
269
|
+
}
|
|
270
|
+
function parseDefaults(c, raw, kind) {
|
|
271
|
+
if (raw === undefined)
|
|
272
|
+
return undefined;
|
|
273
|
+
if (!isPlainObject(raw)) {
|
|
274
|
+
c.add("eval.invalid_field", `"defaults" must be a mapping`);
|
|
275
|
+
return undefined;
|
|
276
|
+
}
|
|
277
|
+
c.unknownKeys(raw, DEFAULTS_KEYS, "defaults");
|
|
278
|
+
const out = {};
|
|
279
|
+
const repeats = optionalNumber(c, raw, "repeats", "defaults", "an integer from 1 to 10", (n) => isIntegerIn(n, 1, 10));
|
|
280
|
+
if (repeats !== undefined)
|
|
281
|
+
out.repeats = repeats;
|
|
282
|
+
const judge = optionalString(c, raw, "judge", "defaults", { min: 1 });
|
|
283
|
+
if (judge !== undefined)
|
|
284
|
+
out.judge = judge;
|
|
285
|
+
const passThreshold = optionalNumber(c, raw, "pass_threshold", "defaults", "a number greater than 0 and at most 1", (n) => n > 0 && n <= 1);
|
|
286
|
+
if (passThreshold !== undefined)
|
|
287
|
+
out.pass_threshold = passThreshold;
|
|
288
|
+
const maxCost = optionalNumber(c, raw, "max_cost_usd", "defaults", "a number greater than 0", (n) => n > 0);
|
|
289
|
+
if (maxCost !== undefined)
|
|
290
|
+
out.max_cost_usd = maxCost;
|
|
291
|
+
const concurrency = optionalNumber(c, raw, "concurrency", "defaults", "an integer from 1 to 8", (n) => isIntegerIn(n, 1, 8));
|
|
292
|
+
if (concurrency !== undefined)
|
|
293
|
+
out.concurrency = concurrency;
|
|
294
|
+
const model = optionalString(c, raw, "model", "defaults", { min: 1 });
|
|
295
|
+
if (model !== undefined)
|
|
296
|
+
out.model = model;
|
|
297
|
+
if (raw.user_model !== undefined) {
|
|
298
|
+
if (kind === "stage") {
|
|
299
|
+
c.add("eval.conversation_not_applicable", `defaults "user_model" is only valid with an agent target`);
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
const userModel = optionalString(c, raw, "user_model", "defaults", { min: 1 });
|
|
303
|
+
if (userModel !== undefined)
|
|
304
|
+
out.user_model = userModel;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return out;
|
|
308
|
+
}
|
|
309
|
+
function parseConversation(c, raw, kind) {
|
|
310
|
+
if (raw === undefined)
|
|
311
|
+
return undefined;
|
|
312
|
+
if (kind === "stage") {
|
|
313
|
+
c.add("eval.conversation_not_applicable", `"conversation" is only valid with an agent target`);
|
|
314
|
+
return undefined;
|
|
315
|
+
}
|
|
316
|
+
const bad = (key, rule) => c.add("eval.conversation_invalid", `conversation "${key}" must be ${rule}`);
|
|
317
|
+
if (!isPlainObject(raw)) {
|
|
318
|
+
c.add("eval.conversation_invalid", `"conversation" must be a mapping`);
|
|
319
|
+
return undefined;
|
|
320
|
+
}
|
|
321
|
+
c.unknownKeys(raw, CONVERSATION_KEYS, "conversation");
|
|
322
|
+
const out = {};
|
|
323
|
+
const oneOf = (key, allowed) => {
|
|
324
|
+
const value = raw[key];
|
|
325
|
+
if (value === undefined)
|
|
326
|
+
return undefined;
|
|
327
|
+
if (typeof value === "string" && allowed.includes(value))
|
|
328
|
+
return value;
|
|
329
|
+
bad(key, `one of: ${allowed.join(", ")}`);
|
|
330
|
+
return undefined;
|
|
331
|
+
};
|
|
332
|
+
const mode = oneOf("mode", ["script", "simulated"]);
|
|
333
|
+
if (mode !== undefined)
|
|
334
|
+
out.mode = mode;
|
|
335
|
+
const maxTurns = optionalNumber(c, raw, "max_turns", "conversation", "an integer from 1 to 30", (n) => isIntegerIn(n, 1, 30), "eval.conversation_invalid");
|
|
336
|
+
if (maxTurns !== undefined)
|
|
337
|
+
out.max_turns = maxTurns;
|
|
338
|
+
const timeout = optionalNumber(c, raw, "turn_timeout_ms", "conversation", "an integer from 5000 to 600000", (n) => isIntegerIn(n, 5000, 600_000), "eval.conversation_invalid");
|
|
339
|
+
if (timeout !== undefined)
|
|
340
|
+
out.turn_timeout_ms = timeout;
|
|
341
|
+
const tools = oneOf("tools", ["real", "read_only", "mocked"]);
|
|
342
|
+
if (tools !== undefined)
|
|
343
|
+
out.tools = tools;
|
|
344
|
+
const approvals = oneOf("approvals", ["deny", "auto_approve_read", "record_only"]);
|
|
345
|
+
if (approvals !== undefined)
|
|
346
|
+
out.approvals = approvals;
|
|
347
|
+
if (raw.read_only_tools !== undefined) {
|
|
348
|
+
const list = raw.read_only_tools;
|
|
349
|
+
if (Array.isArray(list) &&
|
|
350
|
+
list.length <= 50 &&
|
|
351
|
+
list.every((item) => typeof item === "string" && READ_ONLY_TOOL_PATTERN.test(item))) {
|
|
352
|
+
out.read_only_tools = list;
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
bad("read_only_tools", "an array of at most 50 tool grant ids");
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (raw.allow_real_tools !== undefined) {
|
|
359
|
+
if (typeof raw.allow_real_tools !== "boolean") {
|
|
360
|
+
bad("allow_real_tools", "a boolean");
|
|
361
|
+
}
|
|
362
|
+
else if (raw.allow_real_tools && (tools ?? "read_only") !== "real") {
|
|
363
|
+
c.add("eval.conversation_invalid", `conversation "allow_real_tools" is only valid with tools: real`);
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
out.allow_real_tools = raw.allow_real_tools;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return out;
|
|
370
|
+
}
|
|
371
|
+
function parseBudget(c, raw) {
|
|
372
|
+
if (raw === undefined)
|
|
373
|
+
return undefined;
|
|
374
|
+
if (!isPlainObject(raw)) {
|
|
375
|
+
c.add("eval.invalid_field", `"budget" must be a mapping`);
|
|
376
|
+
return undefined;
|
|
377
|
+
}
|
|
378
|
+
c.unknownKeys(raw, BUDGET_KEYS, "budget");
|
|
379
|
+
const out = {};
|
|
380
|
+
const latency = optionalNumber(c, raw, "latency_ms", "budget", "a positive number", (n) => n > 0);
|
|
381
|
+
if (latency !== undefined)
|
|
382
|
+
out.latency_ms = latency;
|
|
383
|
+
const cost = optionalNumber(c, raw, "cost_usd", "budget", "a positive number", (n) => n > 0);
|
|
384
|
+
if (cost !== undefined)
|
|
385
|
+
out.cost_usd = cost;
|
|
386
|
+
return out;
|
|
387
|
+
}
|
|
388
|
+
function checkValueProblem(op, value, present) {
|
|
389
|
+
if (op === "exists" || op === "not_empty") {
|
|
390
|
+
return present ? `op "${op}" takes no value` : undefined;
|
|
391
|
+
}
|
|
392
|
+
if (!present)
|
|
393
|
+
return `op "${op}" requires a value`;
|
|
394
|
+
switch (op) {
|
|
395
|
+
case "equals":
|
|
396
|
+
case "not_equals":
|
|
397
|
+
case "contains":
|
|
398
|
+
return undefined;
|
|
399
|
+
case "matches": {
|
|
400
|
+
if (typeof value !== "string")
|
|
401
|
+
return `op "matches" value must be a regular expression string`;
|
|
402
|
+
try {
|
|
403
|
+
new RegExp(value);
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
return `op "matches" value is not a valid regular expression`;
|
|
407
|
+
}
|
|
408
|
+
return undefined;
|
|
409
|
+
}
|
|
410
|
+
case "gte":
|
|
411
|
+
case "lte":
|
|
412
|
+
return typeof value === "number" && Number.isFinite(value)
|
|
413
|
+
? undefined
|
|
414
|
+
: `op "${op}" value must be a number`;
|
|
415
|
+
case "between":
|
|
416
|
+
return Array.isArray(value) &&
|
|
417
|
+
value.length === 2 &&
|
|
418
|
+
value.every((v) => typeof v === "number" && Number.isFinite(v)) &&
|
|
419
|
+
value[0] <= value[1]
|
|
420
|
+
? undefined
|
|
421
|
+
: `op "between" value must be [min, max] with min <= max`;
|
|
422
|
+
case "in":
|
|
423
|
+
return Array.isArray(value) ? undefined : `op "in" value must be an array`;
|
|
424
|
+
case "length_gte":
|
|
425
|
+
case "length_lte":
|
|
426
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0
|
|
427
|
+
? undefined
|
|
428
|
+
: `op "${op}" value must be a non-negative integer`;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
function parseChecks(c, raw, where) {
|
|
432
|
+
if (!Array.isArray(raw) || raw.length === 0) {
|
|
433
|
+
c.add("eval.grader_invalid", `${where} "checks" must be a non-empty array`);
|
|
434
|
+
return undefined;
|
|
435
|
+
}
|
|
436
|
+
const checks = [];
|
|
437
|
+
let ok = true;
|
|
438
|
+
raw.forEach((item, index) => {
|
|
439
|
+
const at = `${where} checks[${index}]`;
|
|
440
|
+
if (!isPlainObject(item)) {
|
|
441
|
+
c.add("eval.grader_invalid", `${at} must be a mapping`);
|
|
442
|
+
ok = false;
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
c.unknownKeys(item, CHECK_KEYS, at);
|
|
446
|
+
if (typeof item.path !== "string" || !FIELD_PATH_PATTERN.test(item.path)) {
|
|
447
|
+
c.add("eval.grader_invalid", `${at} "path" must be dot-separated keys with optional [n] or [*] (for example payload.findings[*].line)`);
|
|
448
|
+
ok = false;
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
if (typeof item.op !== "string" || !FIELD_OPS.includes(item.op)) {
|
|
452
|
+
c.add("eval.grader_invalid", `${at} "op" must be one of: ${FIELD_OPS.join(", ")}`);
|
|
453
|
+
ok = false;
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
const op = item.op;
|
|
457
|
+
const problem = checkValueProblem(op, item.value, "value" in item);
|
|
458
|
+
if (problem) {
|
|
459
|
+
c.add("eval.grader_invalid", `${at} ${problem}`);
|
|
460
|
+
ok = false;
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
checks.push({ path: item.path, op, ...("value" in item ? { value: item.value } : {}) });
|
|
464
|
+
});
|
|
465
|
+
return ok ? checks : undefined;
|
|
466
|
+
}
|
|
467
|
+
function parseExamples(c, raw, scale, where) {
|
|
468
|
+
if (!Array.isArray(raw) || raw.length > 6) {
|
|
469
|
+
c.add("eval.grader_invalid", `${where} "examples" must be an array of at most 6 items`);
|
|
470
|
+
return undefined;
|
|
471
|
+
}
|
|
472
|
+
const out = [];
|
|
473
|
+
let ok = true;
|
|
474
|
+
raw.forEach((item, index) => {
|
|
475
|
+
const at = `${where} examples[${index}]`;
|
|
476
|
+
if (!isPlainObject(item)) {
|
|
477
|
+
c.add("eval.grader_invalid", `${at} must be a mapping`);
|
|
478
|
+
ok = false;
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
c.unknownKeys(item, [scale === "pass_fail" ? "verdict" : "score", "output", "reason"], at);
|
|
482
|
+
if (typeof item.output !== "string") {
|
|
483
|
+
c.add("eval.grader_invalid", `${at} "output" must be a string`);
|
|
484
|
+
ok = false;
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
if (item.reason !== undefined && typeof item.reason !== "string") {
|
|
488
|
+
c.add("eval.grader_invalid", `${at} "reason" must be a string`);
|
|
489
|
+
ok = false;
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
const reason = item.reason;
|
|
493
|
+
if (scale === "pass_fail") {
|
|
494
|
+
if (item.verdict !== "pass" && item.verdict !== "fail") {
|
|
495
|
+
c.add("eval.grader_invalid", `${at} "verdict" must be "pass" or "fail"`);
|
|
496
|
+
ok = false;
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
out.push({ verdict: item.verdict, output: item.output, ...(reason !== undefined ? { reason } : {}) });
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
if (typeof item.score !== "number" || !isIntegerIn(item.score, 1, 5)) {
|
|
503
|
+
c.add("eval.grader_invalid", `${at} "score" must be an integer from 1 to 5`);
|
|
504
|
+
ok = false;
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
out.push({ score: item.score, output: item.output, ...(reason !== undefined ? { reason } : {}) });
|
|
508
|
+
});
|
|
509
|
+
return ok ? out : undefined;
|
|
510
|
+
}
|
|
511
|
+
function optionalToolList(c, raw, key, where) {
|
|
512
|
+
const value = raw[key];
|
|
513
|
+
if (value === undefined)
|
|
514
|
+
return undefined;
|
|
515
|
+
if (!Array.isArray(value) ||
|
|
516
|
+
value.length > 50 ||
|
|
517
|
+
!value.every((item) => typeof item === "string" && item.length >= 1 && item.length <= 128)) {
|
|
518
|
+
c.add("eval.grader_invalid", `${where} "${key}" must be an array of at most 50 tool names`);
|
|
519
|
+
return undefined;
|
|
520
|
+
}
|
|
521
|
+
return value;
|
|
522
|
+
}
|
|
523
|
+
function parseToolArgs(c, raw, where) {
|
|
524
|
+
if (!Array.isArray(raw) || raw.length > 20) {
|
|
525
|
+
c.add("eval.grader_invalid", `${where} "args" must be an array of at most 20 checks`);
|
|
526
|
+
return undefined;
|
|
527
|
+
}
|
|
528
|
+
const out = [];
|
|
529
|
+
let ok = true;
|
|
530
|
+
raw.forEach((item, index) => {
|
|
531
|
+
const at = `${where} args[${index}]`;
|
|
532
|
+
if (!isPlainObject(item)) {
|
|
533
|
+
c.add("eval.grader_invalid", `${at} must be a mapping`);
|
|
534
|
+
ok = false;
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
c.unknownKeys(item, TOOL_ARG_KEYS, at);
|
|
538
|
+
if (typeof item.tool !== "string" || item.tool.length < 1 || item.tool.length > 128) {
|
|
539
|
+
c.add("eval.grader_invalid", `${at} "tool" must be a tool name`);
|
|
540
|
+
ok = false;
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
if (typeof item.path !== "string" || !FIELD_PATH_PATTERN.test(item.path)) {
|
|
544
|
+
c.add("eval.grader_invalid", `${at} "path" must be dot-separated keys with optional [n] or [*]`);
|
|
545
|
+
ok = false;
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
if (typeof item.op !== "string" || !FIELD_OPS.includes(item.op)) {
|
|
549
|
+
c.add("eval.grader_invalid", `${at} "op" must be one of: ${FIELD_OPS.join(", ")}`);
|
|
550
|
+
ok = false;
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
const op = item.op;
|
|
554
|
+
const problem = checkValueProblem(op, item.value, "value" in item);
|
|
555
|
+
if (problem) {
|
|
556
|
+
c.add("eval.grader_invalid", `${at} ${problem}`);
|
|
557
|
+
ok = false;
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
out.push({ tool: item.tool, path: item.path, op, ...("value" in item ? { value: item.value } : {}) });
|
|
561
|
+
});
|
|
562
|
+
return ok ? out : undefined;
|
|
563
|
+
}
|
|
564
|
+
function parseFinalReply(c, raw, where) {
|
|
565
|
+
if (!Array.isArray(raw) || raw.length === 0 || raw.length > 10) {
|
|
566
|
+
c.add("eval.grader_invalid", `${where} "final_reply" must be an array of 1-10 checks`);
|
|
567
|
+
return undefined;
|
|
568
|
+
}
|
|
569
|
+
const out = [];
|
|
570
|
+
let ok = true;
|
|
571
|
+
raw.forEach((item, index) => {
|
|
572
|
+
const at = `${where} final_reply[${index}]`;
|
|
573
|
+
if (!isPlainObject(item)) {
|
|
574
|
+
c.add("eval.grader_invalid", `${at} must be a mapping`);
|
|
575
|
+
ok = false;
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
c.unknownKeys(item, FINAL_REPLY_KEYS, at);
|
|
579
|
+
if (typeof item.op !== "string" || !FIELD_OPS.includes(item.op)) {
|
|
580
|
+
c.add("eval.grader_invalid", `${at} "op" must be one of: ${FIELD_OPS.join(", ")}`);
|
|
581
|
+
ok = false;
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
const op = item.op;
|
|
585
|
+
const problem = checkValueProblem(op, item.value, "value" in item);
|
|
586
|
+
if (problem) {
|
|
587
|
+
c.add("eval.grader_invalid", `${at} ${problem}`);
|
|
588
|
+
ok = false;
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
out.push({ op, ...("value" in item ? { value: item.value } : {}) });
|
|
592
|
+
});
|
|
593
|
+
return ok ? out : undefined;
|
|
594
|
+
}
|
|
595
|
+
function parseGrader(c, raw, index, defaultJudge, kind) {
|
|
596
|
+
if (!isPlainObject(raw)) {
|
|
597
|
+
c.add("eval.grader_invalid", `graders[${index}] must be a mapping`);
|
|
598
|
+
return undefined;
|
|
599
|
+
}
|
|
600
|
+
const label = typeof raw.id === "string" ? `grader "${raw.id}"` : `graders[${index}]`;
|
|
601
|
+
const before = c.issues.length;
|
|
602
|
+
if (typeof raw.id !== "string" || !ID_PATTERN.test(raw.id)) {
|
|
603
|
+
c.add("eval.invalid_field", `graders[${index}] "id" must match ${ID_PATTERN.source}`);
|
|
604
|
+
}
|
|
605
|
+
const type = raw.type;
|
|
606
|
+
if (type === undefined) {
|
|
607
|
+
c.add("eval.grader_invalid", `${label} "type" is required`);
|
|
608
|
+
return undefined;
|
|
609
|
+
}
|
|
610
|
+
if (type === "command") {
|
|
611
|
+
c.add("eval.grader_type_unsupported", `${label} type "command" is not available in release 1 (supported: ${GRADER_TYPES.join(", ")})`);
|
|
612
|
+
return undefined;
|
|
613
|
+
}
|
|
614
|
+
if (typeof type !== "string" || !GRADER_TYPES.includes(type)) {
|
|
615
|
+
c.add("eval.grader_type_unsupported", `${label} type ${JSON.stringify(type)} is not supported (supported: ${GRADER_TYPES.join(", ")})`);
|
|
616
|
+
return undefined;
|
|
617
|
+
}
|
|
618
|
+
const graderType = type;
|
|
619
|
+
const scaleOnGoal = graderType === "goal_reached" && raw.scale !== undefined;
|
|
620
|
+
c.unknownKeys(scaleOnGoal ? Object.fromEntries(Object.entries(raw).filter(([key]) => key !== "scale")) : raw, [...GRADER_COMMON_KEYS, ...GRADER_TYPE_KEYS[graderType]], label);
|
|
621
|
+
if (scaleOnGoal) {
|
|
622
|
+
c.add("eval.grader_invalid", `${label} "scale" is not accepted: goal_reached is always pass_fail`);
|
|
623
|
+
}
|
|
624
|
+
const description = optionalString(c, raw, "description", label, { max: 200 });
|
|
625
|
+
const weight = optionalNumber(c, raw, "weight", label, "a number greater than 0", (n) => n > 0);
|
|
626
|
+
let mustPass = false;
|
|
627
|
+
if (raw.must_pass !== undefined) {
|
|
628
|
+
if (typeof raw.must_pass === "boolean")
|
|
629
|
+
mustPass = raw.must_pass;
|
|
630
|
+
else
|
|
631
|
+
c.add("eval.invalid_field", `${label} "must_pass" must be a boolean`);
|
|
632
|
+
}
|
|
633
|
+
const common = {
|
|
634
|
+
id: String(raw.id),
|
|
635
|
+
...(description !== undefined ? { description } : {}),
|
|
636
|
+
...(weight !== undefined ? { weight } : {}),
|
|
637
|
+
...(raw.must_pass !== undefined && typeof raw.must_pass === "boolean"
|
|
638
|
+
? { must_pass: raw.must_pass }
|
|
639
|
+
: {}),
|
|
640
|
+
};
|
|
641
|
+
const resolvedCommon = {
|
|
642
|
+
id: String(raw.id),
|
|
643
|
+
weight: weight ?? 1,
|
|
644
|
+
mustPass,
|
|
645
|
+
...(description !== undefined ? { description } : {}),
|
|
646
|
+
};
|
|
647
|
+
let result;
|
|
648
|
+
switch (graderType) {
|
|
649
|
+
case "schema":
|
|
650
|
+
result = {
|
|
651
|
+
config: { ...common, type: "schema" },
|
|
652
|
+
resolved: { ...resolvedCommon, type: "schema" },
|
|
653
|
+
};
|
|
654
|
+
break;
|
|
655
|
+
case "field": {
|
|
656
|
+
const checks = parseChecks(c, raw.checks, label);
|
|
657
|
+
if (checks) {
|
|
658
|
+
result = {
|
|
659
|
+
config: { ...common, type: "field", checks },
|
|
660
|
+
resolved: { ...resolvedCommon, type: "field", checks },
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
break;
|
|
664
|
+
}
|
|
665
|
+
case "reference": {
|
|
666
|
+
const mode = raw.mode;
|
|
667
|
+
let ok = true;
|
|
668
|
+
if (mode !== "each_mentioned" && mode !== "equals") {
|
|
669
|
+
c.add("eval.grader_invalid", `${label} "mode" must be "each_mentioned" or "equals"`);
|
|
670
|
+
ok = false;
|
|
671
|
+
}
|
|
672
|
+
let pathValue = kind === "agent" ? "final_reply" : "payload";
|
|
673
|
+
if (raw.path !== undefined) {
|
|
674
|
+
if (typeof raw.path === "string" && FIELD_PATH_PATTERN.test(raw.path))
|
|
675
|
+
pathValue = raw.path;
|
|
676
|
+
else {
|
|
677
|
+
c.add("eval.grader_invalid", `${label} "path" must be a valid envelope path`);
|
|
678
|
+
ok = false;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
if (typeof raw.expected !== "string" || !FIELD_PATH_PATTERN.test(raw.expected)) {
|
|
682
|
+
c.add("eval.grader_invalid", `${label} "expected" must be a valid path into the case (for example expected.findings)`);
|
|
683
|
+
ok = false;
|
|
684
|
+
}
|
|
685
|
+
const passAt = optionalNumber(c, raw, "pass_at", label, "a number from 0 to 1", (n) => n >= 0 && n <= 1, "eval.grader_invalid");
|
|
686
|
+
if (raw.pass_at !== undefined && passAt === undefined)
|
|
687
|
+
ok = false;
|
|
688
|
+
if (ok) {
|
|
689
|
+
result = {
|
|
690
|
+
config: {
|
|
691
|
+
...common,
|
|
692
|
+
type: "reference",
|
|
693
|
+
mode: mode,
|
|
694
|
+
...(raw.path !== undefined ? { path: pathValue } : {}),
|
|
695
|
+
expected: raw.expected,
|
|
696
|
+
...(passAt !== undefined ? { pass_at: passAt } : {}),
|
|
697
|
+
},
|
|
698
|
+
resolved: {
|
|
699
|
+
...resolvedCommon,
|
|
700
|
+
type: "reference",
|
|
701
|
+
mode: mode,
|
|
702
|
+
path: pathValue,
|
|
703
|
+
expected: raw.expected,
|
|
704
|
+
passAt: passAt ?? 1,
|
|
705
|
+
},
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
break;
|
|
709
|
+
}
|
|
710
|
+
case "rubric": {
|
|
711
|
+
let ok = true;
|
|
712
|
+
if (typeof raw.rubric !== "string" || raw.rubric.length < 1 || raw.rubric.length > 4000) {
|
|
713
|
+
c.add("eval.grader_invalid", `${label} "rubric" must be a string of 1-4000 characters`);
|
|
714
|
+
ok = false;
|
|
715
|
+
}
|
|
716
|
+
let scale = "pass_fail";
|
|
717
|
+
if (raw.scale !== undefined) {
|
|
718
|
+
if (raw.scale === "pass_fail" || raw.scale === "1_to_5")
|
|
719
|
+
scale = raw.scale;
|
|
720
|
+
else {
|
|
721
|
+
c.add("eval.grader_invalid", `${label} "scale" must be "pass_fail" or "1_to_5"`);
|
|
722
|
+
ok = false;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
let passAt;
|
|
726
|
+
if (raw.pass_at !== undefined) {
|
|
727
|
+
if (scale === "pass_fail") {
|
|
728
|
+
c.add("eval.grader_invalid", `${label} "pass_at" is only allowed with scale "1_to_5"`);
|
|
729
|
+
ok = false;
|
|
730
|
+
}
|
|
731
|
+
else if (typeof raw.pass_at === "number" && isIntegerIn(raw.pass_at, 1, 5)) {
|
|
732
|
+
passAt = raw.pass_at;
|
|
733
|
+
}
|
|
734
|
+
else {
|
|
735
|
+
c.add("eval.grader_invalid", `${label} "pass_at" must be an integer from 1 to 5`);
|
|
736
|
+
ok = false;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
let judge;
|
|
740
|
+
if (raw.judge !== undefined) {
|
|
741
|
+
if (typeof raw.judge === "string" && raw.judge.length > 0)
|
|
742
|
+
judge = raw.judge;
|
|
743
|
+
else {
|
|
744
|
+
c.add("eval.grader_invalid", `${label} "judge" must be a model id string`);
|
|
745
|
+
ok = false;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
let examples;
|
|
749
|
+
if (raw.examples !== undefined) {
|
|
750
|
+
examples = parseExamples(c, raw.examples, scale, label);
|
|
751
|
+
if (examples === undefined)
|
|
752
|
+
ok = false;
|
|
753
|
+
}
|
|
754
|
+
if (ok) {
|
|
755
|
+
result = {
|
|
756
|
+
config: {
|
|
757
|
+
...common,
|
|
758
|
+
type: "rubric",
|
|
759
|
+
rubric: raw.rubric,
|
|
760
|
+
...(raw.scale !== undefined ? { scale } : {}),
|
|
761
|
+
...(passAt !== undefined ? { pass_at: passAt } : {}),
|
|
762
|
+
...(judge !== undefined ? { judge } : {}),
|
|
763
|
+
...(examples !== undefined ? { examples } : {}),
|
|
764
|
+
},
|
|
765
|
+
resolved: {
|
|
766
|
+
...resolvedCommon,
|
|
767
|
+
type: "rubric",
|
|
768
|
+
rubric: raw.rubric,
|
|
769
|
+
scale,
|
|
770
|
+
passAt: scale === "1_to_5" ? (passAt ?? 4) : 1,
|
|
771
|
+
judge: judge ?? defaultJudge ?? "",
|
|
772
|
+
examples: examples ?? [],
|
|
773
|
+
},
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
break;
|
|
777
|
+
}
|
|
778
|
+
case "code": {
|
|
779
|
+
if (typeof raw.code !== "string" || raw.code.trim() === "") {
|
|
780
|
+
c.add("eval.grader_invalid", `${label} "code" must be a non-empty string`);
|
|
781
|
+
}
|
|
782
|
+
else {
|
|
783
|
+
result = {
|
|
784
|
+
config: { ...common, type: "code", code: raw.code },
|
|
785
|
+
resolved: { ...resolvedCommon, type: "code", code: raw.code },
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
break;
|
|
789
|
+
}
|
|
790
|
+
case "tool_calls": {
|
|
791
|
+
const mustCall = optionalToolList(c, raw, "must_call", label);
|
|
792
|
+
const mustNotCall = optionalToolList(c, raw, "must_not_call", label);
|
|
793
|
+
const mustAskFirst = optionalToolList(c, raw, "must_ask_first", label);
|
|
794
|
+
const maxCalls = optionalNumber(c, raw, "max_calls", label, "an integer from 0 to 200", (n) => isIntegerIn(n, 0, 200), "eval.grader_invalid");
|
|
795
|
+
const args = raw.args === undefined ? [] : parseToolArgs(c, raw.args, label);
|
|
796
|
+
if (args !== undefined) {
|
|
797
|
+
result = {
|
|
798
|
+
config: {
|
|
799
|
+
...common,
|
|
800
|
+
type: "tool_calls",
|
|
801
|
+
...(mustCall !== undefined ? { must_call: mustCall } : {}),
|
|
802
|
+
...(mustNotCall !== undefined ? { must_not_call: mustNotCall } : {}),
|
|
803
|
+
...(mustAskFirst !== undefined ? { must_ask_first: mustAskFirst } : {}),
|
|
804
|
+
...(maxCalls !== undefined ? { max_calls: maxCalls } : {}),
|
|
805
|
+
...(raw.args !== undefined ? { args } : {}),
|
|
806
|
+
},
|
|
807
|
+
resolved: {
|
|
808
|
+
...resolvedCommon,
|
|
809
|
+
type: "tool_calls",
|
|
810
|
+
...(mustCall !== undefined ? { mustCall } : {}),
|
|
811
|
+
...(mustNotCall !== undefined ? { mustNotCall } : {}),
|
|
812
|
+
...(mustAskFirst !== undefined ? { mustAskFirst } : {}),
|
|
813
|
+
...(maxCalls !== undefined ? { maxCalls } : {}),
|
|
814
|
+
args,
|
|
815
|
+
},
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
break;
|
|
819
|
+
}
|
|
820
|
+
case "conversation_check": {
|
|
821
|
+
const maxTurns = optionalNumber(c, raw, "max_turns", label, "an integer from 1 to 100", (n) => isIntegerIn(n, 1, 100), "eval.grader_invalid");
|
|
822
|
+
const maxQuestions = optionalNumber(c, raw, "max_agent_questions_per_turn", label, "an integer from 0 to 10", (n) => isIntegerIn(n, 0, 10), "eval.grader_invalid");
|
|
823
|
+
const finalReply = raw.final_reply === undefined ? [] : parseFinalReply(c, raw.final_reply, label);
|
|
824
|
+
if (raw.max_turns === undefined && raw.max_agent_questions_per_turn === undefined && raw.final_reply === undefined) {
|
|
825
|
+
c.add("eval.grader_invalid", `${label} needs at least one of: max_turns, max_agent_questions_per_turn, final_reply`);
|
|
826
|
+
}
|
|
827
|
+
if (finalReply !== undefined) {
|
|
828
|
+
result = {
|
|
829
|
+
config: {
|
|
830
|
+
...common,
|
|
831
|
+
type: "conversation_check",
|
|
832
|
+
...(maxTurns !== undefined ? { max_turns: maxTurns } : {}),
|
|
833
|
+
...(maxQuestions !== undefined ? { max_agent_questions_per_turn: maxQuestions } : {}),
|
|
834
|
+
...(raw.final_reply !== undefined ? { final_reply: finalReply } : {}),
|
|
835
|
+
},
|
|
836
|
+
resolved: {
|
|
837
|
+
...resolvedCommon,
|
|
838
|
+
type: "conversation_check",
|
|
839
|
+
...(maxTurns !== undefined ? { maxTurns } : {}),
|
|
840
|
+
...(maxQuestions !== undefined ? { maxAgentQuestionsPerTurn: maxQuestions } : {}),
|
|
841
|
+
finalReply,
|
|
842
|
+
},
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
break;
|
|
846
|
+
}
|
|
847
|
+
case "goal_reached": {
|
|
848
|
+
let ok = true;
|
|
849
|
+
let judge;
|
|
850
|
+
if (raw.judge !== undefined) {
|
|
851
|
+
if (typeof raw.judge === "string" && raw.judge.length > 0)
|
|
852
|
+
judge = raw.judge;
|
|
853
|
+
else {
|
|
854
|
+
c.add("eval.grader_invalid", `${label} "judge" must be a model id string`);
|
|
855
|
+
ok = false;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
let guidance;
|
|
859
|
+
if (raw.guidance !== undefined) {
|
|
860
|
+
if (typeof raw.guidance === "string" && raw.guidance.length <= 2000)
|
|
861
|
+
guidance = raw.guidance;
|
|
862
|
+
else {
|
|
863
|
+
c.add("eval.grader_invalid", `${label} "guidance" must be a string of at most 2000 characters`);
|
|
864
|
+
ok = false;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
if (ok) {
|
|
868
|
+
result = {
|
|
869
|
+
config: {
|
|
870
|
+
...common,
|
|
871
|
+
type: "goal_reached",
|
|
872
|
+
...(judge !== undefined ? { judge } : {}),
|
|
873
|
+
...(guidance !== undefined ? { guidance } : {}),
|
|
874
|
+
},
|
|
875
|
+
resolved: {
|
|
876
|
+
...resolvedCommon,
|
|
877
|
+
type: "goal_reached",
|
|
878
|
+
judge: judge ?? defaultJudge ?? "",
|
|
879
|
+
...(guidance !== undefined ? { guidance } : {}),
|
|
880
|
+
},
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
break;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
return c.issues.length > before ? undefined : result;
|
|
887
|
+
}
|
|
888
|
+
/** Grader hash object: the grader's file keys with defaults filled in; a rubric carries its effective judge. */
|
|
889
|
+
function graderHashObject(grader) {
|
|
890
|
+
const base = {
|
|
891
|
+
id: grader.id,
|
|
892
|
+
type: grader.type,
|
|
893
|
+
description: grader.description,
|
|
894
|
+
weight: grader.weight,
|
|
895
|
+
must_pass: grader.mustPass,
|
|
896
|
+
};
|
|
897
|
+
switch (grader.type) {
|
|
898
|
+
case "schema":
|
|
899
|
+
return base;
|
|
900
|
+
case "field":
|
|
901
|
+
return { ...base, checks: grader.checks };
|
|
902
|
+
case "reference":
|
|
903
|
+
return {
|
|
904
|
+
...base,
|
|
905
|
+
mode: grader.mode,
|
|
906
|
+
path: grader.path,
|
|
907
|
+
expected: grader.expected,
|
|
908
|
+
pass_at: grader.passAt,
|
|
909
|
+
};
|
|
910
|
+
case "rubric":
|
|
911
|
+
return {
|
|
912
|
+
...base,
|
|
913
|
+
rubric: grader.rubric,
|
|
914
|
+
scale: grader.scale,
|
|
915
|
+
...(grader.scale === "1_to_5" ? { pass_at: grader.passAt } : {}),
|
|
916
|
+
judge: grader.judge,
|
|
917
|
+
examples: grader.examples,
|
|
918
|
+
};
|
|
919
|
+
case "code":
|
|
920
|
+
return { ...base, code: grader.code };
|
|
921
|
+
case "tool_calls":
|
|
922
|
+
return {
|
|
923
|
+
...base,
|
|
924
|
+
must_call: grader.mustCall,
|
|
925
|
+
must_not_call: grader.mustNotCall,
|
|
926
|
+
must_ask_first: grader.mustAskFirst,
|
|
927
|
+
max_calls: grader.maxCalls,
|
|
928
|
+
args: grader.args,
|
|
929
|
+
};
|
|
930
|
+
case "conversation_check":
|
|
931
|
+
return {
|
|
932
|
+
...base,
|
|
933
|
+
max_turns: grader.maxTurns,
|
|
934
|
+
max_agent_questions_per_turn: grader.maxAgentQuestionsPerTurn,
|
|
935
|
+
final_reply: grader.finalReply,
|
|
936
|
+
};
|
|
937
|
+
case "goal_reached":
|
|
938
|
+
return { ...base, judge: grader.judge, guidance: grader.guidance };
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
export function graderHash(grader) {
|
|
942
|
+
return sha256Hex(canonicalJson(graderHashObject(grader)));
|
|
943
|
+
}
|
|
944
|
+
export function graderHashShort(grader) {
|
|
945
|
+
return hash6(grader.hash);
|
|
946
|
+
}
|
|
947
|
+
export function parseEvalOutcome(yamlText, filePath, projectRoot) {
|
|
948
|
+
let raw;
|
|
949
|
+
try {
|
|
950
|
+
raw = parseYaml(yamlText);
|
|
951
|
+
}
|
|
952
|
+
catch (err) {
|
|
953
|
+
return loadFailure([
|
|
954
|
+
{
|
|
955
|
+
...evalIssue("eval.load_error", err instanceof Error ? err.message : String(err)),
|
|
956
|
+
...yamlParsePosition(err),
|
|
957
|
+
},
|
|
958
|
+
]);
|
|
959
|
+
}
|
|
960
|
+
if (!isPlainObject(raw)) {
|
|
961
|
+
return loadFailure([evalIssue("eval.not_object", "Eval file must be a YAML mapping")]);
|
|
962
|
+
}
|
|
963
|
+
const c = new Collector();
|
|
964
|
+
c.unknownKeys(raw, TOP_KEYS, "the eval file");
|
|
965
|
+
if (raw.version !== 1) {
|
|
966
|
+
c.add("eval.invalid_version", `"version" must be 1 (found ${JSON.stringify(raw.version) ?? "none"})`);
|
|
967
|
+
}
|
|
968
|
+
const absPath = path.resolve(filePath);
|
|
969
|
+
const stem = evalFileStem(absPath);
|
|
970
|
+
const id = raw.id;
|
|
971
|
+
if (typeof id !== "string" || !ID_PATTERN.test(id)) {
|
|
972
|
+
c.add("eval.invalid_id", `"id" must match ${ID_PATTERN.source}`);
|
|
973
|
+
}
|
|
974
|
+
else if (id !== stem) {
|
|
975
|
+
c.add("eval.id_filename_mismatch", `Eval id "${id}" does not match filename stem "${stem}"`);
|
|
976
|
+
}
|
|
977
|
+
const name = optionalString(c, raw, "name", "eval", { min: 1, max: 80 });
|
|
978
|
+
const description = optionalString(c, raw, "description", "eval", { max: 500 });
|
|
979
|
+
let type;
|
|
980
|
+
if (raw.type !== undefined) {
|
|
981
|
+
if (raw.type === "capability" || raw.type === "regression")
|
|
982
|
+
type = raw.type;
|
|
983
|
+
else
|
|
984
|
+
c.add("eval.invalid_field", `"type" must be "capability" or "regression"`);
|
|
985
|
+
}
|
|
986
|
+
const target = parseTarget(c, raw.target);
|
|
987
|
+
let dataset;
|
|
988
|
+
if (raw.dataset === undefined) {
|
|
989
|
+
c.add("eval.invalid_field", `Missing required key "dataset"`);
|
|
990
|
+
}
|
|
991
|
+
else if (typeof raw.dataset !== "string" || raw.dataset.trim() === "") {
|
|
992
|
+
c.add("eval.invalid_field", `"dataset" must be a non-empty path string`);
|
|
993
|
+
}
|
|
994
|
+
else {
|
|
995
|
+
dataset = raw.dataset;
|
|
996
|
+
}
|
|
997
|
+
const root = path.resolve(projectRoot);
|
|
998
|
+
const dir = path.dirname(absPath);
|
|
999
|
+
if (dataset !== undefined && !isInside(root, path.resolve(dir, dataset))) {
|
|
1000
|
+
c.add("eval.dataset_unresolved", `Dataset path "${dataset}" resolves outside the project root`);
|
|
1001
|
+
}
|
|
1002
|
+
if (target?.kind === "stage") {
|
|
1003
|
+
for (const [key, value] of [
|
|
1004
|
+
["pipeline", target.pipeline],
|
|
1005
|
+
["file", target.file],
|
|
1006
|
+
]) {
|
|
1007
|
+
if (value !== undefined && !isInside(root, path.resolve(dir, value))) {
|
|
1008
|
+
c.add("eval.target_unresolved", `Target ${key} "${value}" resolves outside the project root`);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
let task;
|
|
1013
|
+
if (target?.kind === "agent" && raw.task !== undefined) {
|
|
1014
|
+
c.add("eval.task_not_applicable", `"task" is not accepted for an agent target (the scenario goal is input.goal)`);
|
|
1015
|
+
}
|
|
1016
|
+
else {
|
|
1017
|
+
task = parseTask(c, raw.task);
|
|
1018
|
+
}
|
|
1019
|
+
const defaults = parseDefaults(c, raw.defaults, target?.kind);
|
|
1020
|
+
const conversation = parseConversation(c, raw.conversation, target?.kind);
|
|
1021
|
+
const budget = parseBudget(c, raw.budget);
|
|
1022
|
+
const parsedGraders = [];
|
|
1023
|
+
if (raw.graders === undefined) {
|
|
1024
|
+
c.add("eval.invalid_field", `Missing required key "graders"`);
|
|
1025
|
+
}
|
|
1026
|
+
else if (!Array.isArray(raw.graders) || raw.graders.length < 1 || raw.graders.length > 20) {
|
|
1027
|
+
c.add("eval.invalid_field", `"graders" must be an array of 1-20 graders`);
|
|
1028
|
+
}
|
|
1029
|
+
else {
|
|
1030
|
+
const seen = new Set();
|
|
1031
|
+
raw.graders.forEach((rawGrader, index) => {
|
|
1032
|
+
const parsed = parseGrader(c, rawGrader, index, defaults?.judge, target?.kind);
|
|
1033
|
+
if (isPlainObject(rawGrader) && typeof rawGrader.id === "string") {
|
|
1034
|
+
if (seen.has(rawGrader.id)) {
|
|
1035
|
+
c.add("eval.grader_duplicate_id", `Duplicate grader id "${rawGrader.id}"`);
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
seen.add(rawGrader.id);
|
|
1039
|
+
}
|
|
1040
|
+
if (parsed)
|
|
1041
|
+
parsedGraders.push(parsed);
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
if (c.issues.length > 0 || !target || dataset === undefined || typeof id !== "string") {
|
|
1045
|
+
return loadFailure(c.issues);
|
|
1046
|
+
}
|
|
1047
|
+
const file = {
|
|
1048
|
+
version: 1,
|
|
1049
|
+
id,
|
|
1050
|
+
...(name !== undefined ? { name } : {}),
|
|
1051
|
+
...(description !== undefined ? { description } : {}),
|
|
1052
|
+
...(type !== undefined ? { type } : {}),
|
|
1053
|
+
target,
|
|
1054
|
+
dataset,
|
|
1055
|
+
...(task !== undefined ? { task } : {}),
|
|
1056
|
+
...(defaults !== undefined ? { defaults } : {}),
|
|
1057
|
+
...(conversation !== undefined ? { conversation } : {}),
|
|
1058
|
+
graders: parsedGraders.map((g) => g.config),
|
|
1059
|
+
...(budget !== undefined ? { budget } : {}),
|
|
1060
|
+
};
|
|
1061
|
+
const graders = parsedGraders.map((g) => ({ ...g.resolved, hash: graderHash(g.resolved) }));
|
|
1062
|
+
return loadSuccess({
|
|
1063
|
+
path: absPath,
|
|
1064
|
+
relPath: path.relative(root, absPath).split(path.sep).join("/"),
|
|
1065
|
+
id,
|
|
1066
|
+
fileHash: sha256Hex(Buffer.from(yamlText, "utf8")),
|
|
1067
|
+
file,
|
|
1068
|
+
graders,
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
export async function loadEvalOutcome(absPath, projectRoot) {
|
|
1072
|
+
let bytes;
|
|
1073
|
+
try {
|
|
1074
|
+
bytes = await readFile(absPath);
|
|
1075
|
+
}
|
|
1076
|
+
catch (err) {
|
|
1077
|
+
return loadFailure([
|
|
1078
|
+
evalIssue("eval.load_error", err instanceof Error ? err.message : String(err)),
|
|
1079
|
+
]);
|
|
1080
|
+
}
|
|
1081
|
+
const outcome = parseEvalOutcome(bytes.toString("utf8"), absPath, projectRoot);
|
|
1082
|
+
if (!outcome.ok)
|
|
1083
|
+
return outcome;
|
|
1084
|
+
return loadSuccess({ ...outcome.value, fileHash: sha256Hex(bytes) });
|
|
1085
|
+
}
|