@dailephd/my-dev-kit-lab 0.5.0 → 0.5.2
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/CHANGELOG.md +26 -0
- package/README.md +26 -10
- package/benchmarks/contracts/benchmark-project-profiles.json +14 -14
- package/benchmarks/contracts/warm-index-benchmark-cases.json +764 -0
- package/dist/src/agents/adapters/claudeAdapter.js +47 -4
- package/dist/src/agents/adapters/codexAdapter.js +64 -5
- package/dist/src/cli/help.js +22 -1
- package/dist/src/commands/runExperimentDescribeCommand.js +6 -1
- package/dist/src/commands/runExperimentRunCommand.js +87 -5
- package/dist/src/commands/runWarmIndexCampaignPresentation.js +64 -0
- package/dist/src/core/runMeasuredCommand.js +49 -10
- package/dist/src/evaluation/benchmarkMetadata.js +191 -1
- package/dist/src/evaluation/projectComplexity.js +1 -1
- package/dist/src/evaluation/readEvaluationCases.js +7 -1
- package/dist/src/evaluation/types.js +1 -1
- package/dist/src/experiments/plugins/warmIndexReuse/agentEvaluation.js +233 -0
- package/dist/src/experiments/plugins/warmIndexReuse/campaignPresets.js +83 -0
- package/dist/src/experiments/plugins/warmIndexReuse/config.js +51 -0
- package/dist/src/experiments/plugins/warmIndexReuse/fakeAgentEvaluation.js +6 -118
- package/dist/src/experiments/plugins/warmIndexReuse/index.js +3 -1
- package/dist/src/experiments/plugins/warmIndexReuse/metrics.js +35 -11
- package/dist/src/experiments/plugins/warmIndexReuse/plugin.js +50 -14
- package/dist/src/experiments/plugins/warmIndexReuse/realAgentPrompt.js +52 -0
- package/dist/src/experiments/plugins/warmIndexReuse/selection.js +35 -0
- package/dist/src/gallery/index.js +1 -0
- package/dist/src/gallery/writeGalleryManifest.js +5 -1
- package/dist/src/gallery/writeWarmIndexCampaignGallery.js +116 -0
- package/dist/src/plots/buildWarmIndexPlotData.js +21 -2
- package/dist/src/report/experiments/buildPluginExperimentReport.js +35 -8
- package/dist/src/report/experiments/buildWarmIndexReuseReport.js +138 -12
- package/dist/src/report/experiments/renderPluginExperimentReportText.js +35 -12
- package/dist/src/report/experiments/renderWarmIndexReuseHtml.js +45 -14
- package/docs/METRICS.md +33 -3
- package/package.json +1 -1
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { PROJECT_COMPLEXITY_FORMULA, calculateProjectComplexityScore } from "./projectComplexity.js";
|
|
4
|
+
import { PROJECT_COMPLEXITY_FORMULA, calculateProjectComplexityScore, roundToTwo } from "./projectComplexity.js";
|
|
5
|
+
import { TASK_LOCALITIES } from "./types.js";
|
|
5
6
|
export const REQUIRED_BENCHMARK_PROJECT_IDS = [
|
|
6
7
|
"todo-ts",
|
|
7
8
|
"todo-python",
|
|
@@ -11,6 +12,8 @@ export const REQUIRED_BENCHMARK_PROJECT_IDS = [
|
|
|
11
12
|
"task-analytics-large-mixed"
|
|
12
13
|
];
|
|
13
14
|
export const VALID_COMPLEXITY_LEVELS = new Set(["small", "medium", "large", "mixed-language"]);
|
|
15
|
+
export const REQUIRED_WARM_INDEX_BENCHMARK_PROJECT_IDS = ["task-workflow-medium-ts", "task-analytics-large-mixed"];
|
|
16
|
+
export const MIN_WARM_INDEX_TASKS_PER_PROJECT = 5;
|
|
14
17
|
export async function readBenchmarkProjectProfiles(profilesPath, repoRoot = process.cwd()) {
|
|
15
18
|
let parsed;
|
|
16
19
|
try {
|
|
@@ -95,6 +98,193 @@ export function validateAnswerKey(answerKey, label) {
|
|
|
95
98
|
}
|
|
96
99
|
return errors;
|
|
97
100
|
}
|
|
101
|
+
export function validateTaskLocality(value, label) {
|
|
102
|
+
if (typeof value === "string" && TASK_LOCALITIES.includes(value)) {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
return [`${label}: taskLocality must be one of ${TASK_LOCALITIES.join(", ")} (received ${JSON.stringify(value)}).`];
|
|
106
|
+
}
|
|
107
|
+
export function deriveWarmIndexTaskStats(cases, projectId) {
|
|
108
|
+
const projectCases = cases.filter((benchmarkCase) => benchmarkCase?.benchmarkProject === projectId);
|
|
109
|
+
if (projectCases.length === 0) {
|
|
110
|
+
return { taskCount: 0, expectedRelevantFilesAverage: 0, expectedRelevantSymbolsAverage: 0 };
|
|
111
|
+
}
|
|
112
|
+
const fileCounts = projectCases.map((benchmarkCase) => (Array.isArray(benchmarkCase.expectedFiles) ? benchmarkCase.expectedFiles.length : 0));
|
|
113
|
+
const symbolCounts = projectCases.map((benchmarkCase) => Array.isArray(benchmarkCase.expectedSymbols) ? benchmarkCase.expectedSymbols.length : 0);
|
|
114
|
+
return {
|
|
115
|
+
taskCount: projectCases.length,
|
|
116
|
+
expectedRelevantFilesAverage: roundToTwo(fileCounts.reduce((total, count) => total + count, 0) / projectCases.length),
|
|
117
|
+
expectedRelevantSymbolsAverage: roundToTwo(symbolCounts.reduce((total, count) => total + count, 0) / projectCases.length)
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
export function validateWarmIndexBenchmarkCases(cases, profiles, repoRoot = process.cwd()) {
|
|
121
|
+
const errors = [];
|
|
122
|
+
const profilesById = new Map(profiles.map((profile) => [profile.projectId, profile]));
|
|
123
|
+
const ids = new Set();
|
|
124
|
+
const sourceRootsByProject = new Map();
|
|
125
|
+
cases.forEach((benchmarkCase, index) => {
|
|
126
|
+
if (!benchmarkCase || typeof benchmarkCase !== "object") {
|
|
127
|
+
errors.push(`warm-index case at index ${index}: must be an object.`);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const id = typeof benchmarkCase.id === "string" && benchmarkCase.id.length > 0 ? benchmarkCase.id : undefined;
|
|
131
|
+
const label = id === undefined ? `warm-index case at index ${index}` : `warm-index case ${id}`;
|
|
132
|
+
if (id === undefined) {
|
|
133
|
+
errors.push(`${label}: id must be a nonempty string.`);
|
|
134
|
+
}
|
|
135
|
+
else if (ids.has(id)) {
|
|
136
|
+
errors.push(`${label}: duplicate case id.`);
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
ids.add(id);
|
|
140
|
+
}
|
|
141
|
+
const projectId = typeof benchmarkCase.benchmarkProject === "string" && benchmarkCase.benchmarkProject.length > 0
|
|
142
|
+
? benchmarkCase.benchmarkProject
|
|
143
|
+
: undefined;
|
|
144
|
+
if (projectId === undefined) {
|
|
145
|
+
errors.push(`${label}: benchmarkProject must be a nonempty string.`);
|
|
146
|
+
}
|
|
147
|
+
if (typeof benchmarkCase.projectProfileRef !== "string" || benchmarkCase.projectProfileRef.length === 0) {
|
|
148
|
+
errors.push(`${label}: missing projectProfileRef.`);
|
|
149
|
+
}
|
|
150
|
+
else if (benchmarkCase.projectProfileRef !== projectId) {
|
|
151
|
+
errors.push(`${label}: projectProfileRef ${benchmarkCase.projectProfileRef} must equal benchmarkProject ${projectId ?? "<missing>"}.`);
|
|
152
|
+
}
|
|
153
|
+
const profile = projectId === undefined ? undefined : profilesById.get(projectId);
|
|
154
|
+
if (projectId !== undefined && profile === undefined) {
|
|
155
|
+
errors.push(`${label}: unknown benchmark project profile ${projectId}.`);
|
|
156
|
+
}
|
|
157
|
+
if (benchmarkCase.taskLocality === undefined) {
|
|
158
|
+
errors.push(`${label}: missing taskLocality; expected one of ${TASK_LOCALITIES.join(", ")}.`);
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
errors.push(...validateTaskLocality(benchmarkCase.taskLocality, label));
|
|
162
|
+
}
|
|
163
|
+
const targetRoot = typeof benchmarkCase.targetRoot === "string" && benchmarkCase.targetRoot.length > 0 ? benchmarkCase.targetRoot : undefined;
|
|
164
|
+
if (targetRoot === undefined) {
|
|
165
|
+
errors.push(`${label}: targetRoot must be a nonempty string.`);
|
|
166
|
+
}
|
|
167
|
+
else if (profile !== undefined && normalizeRelativeRoot(targetRoot) !== normalizeRelativeRoot(profile.rootPath ?? "")) {
|
|
168
|
+
errors.push(`${label}: targetRoot ${targetRoot} does not match profile ${profile.projectId} rootPath ${profile.rootPath}.`);
|
|
169
|
+
}
|
|
170
|
+
if (!Array.isArray(benchmarkCase.sourceRoots) || benchmarkCase.sourceRoots.length === 0) {
|
|
171
|
+
errors.push(`${label}: sourceRoots must be a nonempty array.`);
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
if (profile !== undefined) {
|
|
175
|
+
errors.push(...validateWarmIndexSourceRoots(benchmarkCase.sourceRoots, profile, label));
|
|
176
|
+
}
|
|
177
|
+
if (projectId !== undefined) {
|
|
178
|
+
const reference = sourceRootsByProject.get(projectId);
|
|
179
|
+
if (reference === undefined) {
|
|
180
|
+
sourceRootsByProject.set(projectId, { caseLabel: label, sourceRoots: benchmarkCase.sourceRoots });
|
|
181
|
+
}
|
|
182
|
+
else if (!sameOrderedValues(reference.sourceRoots, benchmarkCase.sourceRoots)) {
|
|
183
|
+
errors.push(`${label}: sourceRoots ${JSON.stringify(benchmarkCase.sourceRoots)} must match the ordered sourceRoots ${JSON.stringify(reference.sourceRoots)} of ${reference.caseLabel} for project ${projectId}.`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const expectedFiles = Array.isArray(benchmarkCase.expectedFiles) ? benchmarkCase.expectedFiles : undefined;
|
|
188
|
+
const expectedSymbols = Array.isArray(benchmarkCase.expectedSymbols) ? benchmarkCase.expectedSymbols : undefined;
|
|
189
|
+
if (expectedFiles === undefined || expectedFiles.length === 0) {
|
|
190
|
+
errors.push(`${label}: expectedFiles must be a nonempty array.`);
|
|
191
|
+
}
|
|
192
|
+
if (expectedSymbols === undefined || expectedSymbols.length === 0) {
|
|
193
|
+
errors.push(`${label}: expectedSymbols must be a nonempty array.`);
|
|
194
|
+
}
|
|
195
|
+
if (benchmarkCase.answerKey === undefined) {
|
|
196
|
+
errors.push(`${label}: missing answerKey.`);
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
errors.push(...validateAnswerKey(benchmarkCase.answerKey, label));
|
|
200
|
+
const answerKey = benchmarkCase.answerKey;
|
|
201
|
+
if (answerKey && typeof answerKey === "object") {
|
|
202
|
+
if (expectedFiles !== undefined && Array.isArray(answerKey.expectedFiles) && !sameOrderedValues(expectedFiles, answerKey.expectedFiles)) {
|
|
203
|
+
errors.push(`${label}: expectedFiles must exactly match answerKey.expectedFiles (same values in the same order).`);
|
|
204
|
+
}
|
|
205
|
+
if (expectedSymbols !== undefined &&
|
|
206
|
+
Array.isArray(answerKey.expectedSymbols) &&
|
|
207
|
+
!sameOrderedValues(expectedSymbols, answerKey.expectedSymbols)) {
|
|
208
|
+
errors.push(`${label}: expectedSymbols must exactly match answerKey.expectedSymbols (same values in the same order).`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (expectedFiles !== undefined && targetRoot !== undefined) {
|
|
213
|
+
for (const expectedFile of expectedFiles) {
|
|
214
|
+
if (typeof expectedFile !== "string" || expectedFile.length === 0 || path.isAbsolute(expectedFile) || expectedFile.includes("..")) {
|
|
215
|
+
errors.push(`${label}: expected file must be a safe relative path: ${String(expectedFile)}.`);
|
|
216
|
+
}
|
|
217
|
+
else if (!existsSync(path.resolve(repoRoot, targetRoot, expectedFile))) {
|
|
218
|
+
errors.push(`${label}: expected file does not exist in ${projectId ?? targetRoot}: ${expectedFile}.`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
const representedProjectIds = [
|
|
224
|
+
...new Set(cases
|
|
225
|
+
.map((benchmarkCase) => benchmarkCase?.benchmarkProject)
|
|
226
|
+
.filter((projectId) => typeof projectId === "string" && projectId.length > 0))
|
|
227
|
+
].sort();
|
|
228
|
+
for (const projectId of representedProjectIds) {
|
|
229
|
+
const profile = profilesById.get(projectId);
|
|
230
|
+
if (profile === undefined || !profile.complexityMetrics || typeof profile.complexityMetrics !== "object") {
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
const derived = deriveWarmIndexTaskStats(cases, projectId);
|
|
234
|
+
for (const field of ["taskCount", "expectedRelevantFilesAverage", "expectedRelevantSymbolsAverage"]) {
|
|
235
|
+
if (profile.complexityMetrics[field] !== derived[field]) {
|
|
236
|
+
errors.push(`profile ${projectId}: complexityMetrics.${field} ${profile.complexityMetrics[field]} does not match warm-index corpus value ${derived[field]}.`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return errors;
|
|
241
|
+
}
|
|
242
|
+
export function validateWarmIndexBenchmarkSuiteCoverage(cases) {
|
|
243
|
+
const errors = [];
|
|
244
|
+
for (const projectId of REQUIRED_WARM_INDEX_BENCHMARK_PROJECT_IDS) {
|
|
245
|
+
const projectCases = cases.filter((benchmarkCase) => benchmarkCase?.benchmarkProject === projectId);
|
|
246
|
+
if (projectCases.length === 0) {
|
|
247
|
+
errors.push(`warm-index suite: missing required benchmark project ${projectId}.`);
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
if (projectCases.length < MIN_WARM_INDEX_TASKS_PER_PROJECT) {
|
|
251
|
+
errors.push(`warm-index suite: project ${projectId} has ${projectCases.length} cases; at least ${MIN_WARM_INDEX_TASKS_PER_PROJECT} are required.`);
|
|
252
|
+
}
|
|
253
|
+
for (const locality of TASK_LOCALITIES) {
|
|
254
|
+
if (!projectCases.some((benchmarkCase) => benchmarkCase.taskLocality === locality)) {
|
|
255
|
+
errors.push(`warm-index suite: project ${projectId} has no ${locality} case.`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return errors;
|
|
260
|
+
}
|
|
261
|
+
function validateWarmIndexSourceRoots(sourceRoots, profile, label) {
|
|
262
|
+
const errors = [];
|
|
263
|
+
const allowedRoots = [...(profile.sourceRoots ?? []), ...(profile.testRoots ?? [])];
|
|
264
|
+
const seen = new Set();
|
|
265
|
+
for (const sourceRoot of sourceRoots) {
|
|
266
|
+
if (seen.has(sourceRoot)) {
|
|
267
|
+
errors.push(`${label}: duplicate source root ${JSON.stringify(sourceRoot)}.`);
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
seen.add(sourceRoot);
|
|
271
|
+
if (typeof sourceRoot !== "string" || !allowedRoots.includes(sourceRoot)) {
|
|
272
|
+
errors.push(`${label}: unknown source root ${JSON.stringify(sourceRoot)} for profile ${profile.projectId}; allowed roots: ${allowedRoots.join(", ")}.`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
for (const allowedRoot of allowedRoots) {
|
|
276
|
+
if (!seen.has(allowedRoot)) {
|
|
277
|
+
errors.push(`${label}: missing profile source/test root ${allowedRoot} for profile ${profile.projectId}.`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return errors;
|
|
281
|
+
}
|
|
282
|
+
function normalizeRelativeRoot(value) {
|
|
283
|
+
return path.posix.normalize(value.replace(/\\/g, "/")).replace(/\/+$/, "");
|
|
284
|
+
}
|
|
285
|
+
function sameOrderedValues(left, right) {
|
|
286
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
287
|
+
}
|
|
98
288
|
export function validateBenchmarkProjectProfiles(profiles, repoRoot = process.cwd()) {
|
|
99
289
|
const errors = [];
|
|
100
290
|
const ids = new Set(profiles.map((profile) => profile.projectId));
|
|
@@ -73,7 +73,7 @@ export function computeProjectComplexityMetrics(projectRoot, fileTree, taskStats
|
|
|
73
73
|
function normalize(value, cap) {
|
|
74
74
|
return Math.min(value / cap, 1);
|
|
75
75
|
}
|
|
76
|
-
function roundToTwo(value) {
|
|
76
|
+
export function roundToTwo(value) {
|
|
77
77
|
return Math.round(value * 100) / 100;
|
|
78
78
|
}
|
|
79
79
|
function sum(values) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { resolveWithinRoot } from "../core/pathSafety.js";
|
|
4
|
-
import { validateAnswerKey } from "./benchmarkMetadata.js";
|
|
4
|
+
import { validateAnswerKey, validateTaskLocality } from "./benchmarkMetadata.js";
|
|
5
5
|
export async function readEvaluationCases(casesPath, repoRoot = process.cwd(), options = {}) {
|
|
6
6
|
let parsed;
|
|
7
7
|
try {
|
|
@@ -36,6 +36,12 @@ export async function readEvaluationCases(casesPath, repoRoot = process.cwd(), o
|
|
|
36
36
|
throw new Error(`Duplicate evaluation case id: ${input.id}`);
|
|
37
37
|
}
|
|
38
38
|
ids.add(input.id);
|
|
39
|
+
if (candidate.taskLocality !== undefined) {
|
|
40
|
+
const localityErrors = validateTaskLocality(candidate.taskLocality, `evaluation case ${input.id}`);
|
|
41
|
+
if (localityErrors.length > 0) {
|
|
42
|
+
throw new Error(localityErrors.join("\n"));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
39
45
|
if (candidate.answerKey !== undefined) {
|
|
40
46
|
const answerKeyErrors = validateAnswerKey(candidate.answerKey, `evaluation case ${input.id}`);
|
|
41
47
|
if (answerKeyErrors.length > 0) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export
|
|
1
|
+
export const TASK_LOCALITIES = ["localized", "cross-module", "broad-change"];
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { runAgentPrompt } from "../../../agents/index.js";
|
|
5
|
+
import { resolveWithinRoot } from "../../../core/pathSafety.js";
|
|
6
|
+
import { classifyAgentRunOutcome } from "../../../evaluation/classifyAgentRunOutcome.js";
|
|
7
|
+
import { parseAgentAnswer } from "../../../evaluation/parseAgentAnswer.js";
|
|
8
|
+
import { scoreCorrectness } from "../../../evaluation/scoreCorrectness.js";
|
|
9
|
+
import { generatePromptVariants } from "../../../prompts/index.js";
|
|
10
|
+
import { buildWarmIndexRealAgentPrompt } from "./realAgentPrompt.js";
|
|
11
|
+
import { taskOutputSegment } from "./selection.js";
|
|
12
|
+
// Classified statuses for which the scorer evaluated an actual agent answer.
|
|
13
|
+
const SCOREABLE_STATUSES = new Set(["completed", "invalid-output"]);
|
|
14
|
+
const SIDE_STRATEGIES = {
|
|
15
|
+
"raw-full-file": "raw-full-file",
|
|
16
|
+
"warm-index-reuse": "my-dev-kit-guided",
|
|
17
|
+
};
|
|
18
|
+
export async function evaluateWarmIndexFakeAgents(options) {
|
|
19
|
+
const casesById = new Map(options.cases.map((evaluationCase) => [evaluationCase.id, evaluationCase]));
|
|
20
|
+
const result = [];
|
|
21
|
+
for (const project of options.projects) {
|
|
22
|
+
const tasks = [];
|
|
23
|
+
for (const task of project.tasks) {
|
|
24
|
+
const evaluationCase = casesById.get(task.caseId);
|
|
25
|
+
const run = (variantId) => evaluateFakeSide({
|
|
26
|
+
evaluationCase,
|
|
27
|
+
caseId: task.caseId,
|
|
28
|
+
variantId,
|
|
29
|
+
projectProfiles: options.projectProfiles,
|
|
30
|
+
outDir: resolveWithinRoot(options.outputRoot, path.join("agents", project.projectSegment, taskOutputSegment(task.caseId), variantId)),
|
|
31
|
+
cwd: options.cwd,
|
|
32
|
+
env: options.env,
|
|
33
|
+
});
|
|
34
|
+
tasks.push({
|
|
35
|
+
caseId: task.caseId,
|
|
36
|
+
raw: task.rawBaseline ? await run("raw-full-file") : null,
|
|
37
|
+
warm: task.warmRetrieval ? await run("warm-index-reuse") : null,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
result.push({ benchmarkProject: project.benchmarkProject, tasks });
|
|
41
|
+
}
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
async function evaluateFakeSide(args) {
|
|
45
|
+
const promptStrategy = SIDE_STRATEGIES[args.variantId];
|
|
46
|
+
const base = { variantId: args.variantId, agentId: "fake-agent", promptStrategy };
|
|
47
|
+
try {
|
|
48
|
+
if (!args.evaluationCase) {
|
|
49
|
+
throw new Error(`Evaluation case ${args.caseId} is not available for fake-agent evaluation.`);
|
|
50
|
+
}
|
|
51
|
+
const [promptVariant] = generatePromptVariants({
|
|
52
|
+
cases: [args.evaluationCase],
|
|
53
|
+
projectProfiles: [...args.projectProfiles],
|
|
54
|
+
strategies: [promptStrategy],
|
|
55
|
+
complexityLevels: ["short"],
|
|
56
|
+
});
|
|
57
|
+
if (!promptVariant) {
|
|
58
|
+
throw new Error(`Failed to generate a fake-agent prompt for case ${args.caseId}.`);
|
|
59
|
+
}
|
|
60
|
+
const agentRunResult = await runAgentPrompt({
|
|
61
|
+
runId: `${args.caseId}.fake-agent.${args.variantId}`,
|
|
62
|
+
agentId: "fake-agent",
|
|
63
|
+
promptVariant,
|
|
64
|
+
promptText: promptVariant.promptText,
|
|
65
|
+
cwd: args.cwd,
|
|
66
|
+
outDir: args.outDir,
|
|
67
|
+
env: args.env,
|
|
68
|
+
});
|
|
69
|
+
return buildSideEvidenceFromRun({ base, caseId: args.caseId, outDir: args.outDir, agentRunResult, expectedAnswerKey: promptVariant.expectedAnswerKey });
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
return buildFailedSideEvidence(base, error);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Real-agent campaign evaluator (v0.5.2 Batch 3): consumes the exact context text the warm-index
|
|
77
|
+
* execution stage already produced for one side. Never calls my-dev-kit, never independently
|
|
78
|
+
* inspects the repository, and never runs a side with no usable context.
|
|
79
|
+
*/
|
|
80
|
+
export async function evaluateWarmIndexRealAgentCampaign(options) {
|
|
81
|
+
const casesById = new Map(options.cases.map((evaluationCase) => [evaluationCase.id, evaluationCase]));
|
|
82
|
+
const result = [];
|
|
83
|
+
for (const project of options.projects) {
|
|
84
|
+
const tasks = [];
|
|
85
|
+
for (const task of project.tasks) {
|
|
86
|
+
const evaluationCase = casesById.get(task.caseId);
|
|
87
|
+
const rawContext = task.rawBaseline?.contextText;
|
|
88
|
+
const rawEligible = typeof rawContext === "string" && rawContext.trim().length > 0;
|
|
89
|
+
const warmContext = task.warmRetrieval?.contextText;
|
|
90
|
+
const warmEligible = task.warmRetrieval !== undefined &&
|
|
91
|
+
task.warmRetrieval.skipped === false &&
|
|
92
|
+
typeof warmContext === "string" &&
|
|
93
|
+
warmContext.trim().length > 0;
|
|
94
|
+
const runSide = (variantId, contextText) => evaluateRealSide({
|
|
95
|
+
evaluationCase,
|
|
96
|
+
caseId: task.caseId,
|
|
97
|
+
variantId,
|
|
98
|
+
contextText,
|
|
99
|
+
projectProfiles: options.projectProfiles,
|
|
100
|
+
outDir: resolveWithinRoot(options.outputRoot, path.join("agents", project.projectSegment, taskOutputSegment(task.caseId), variantId)),
|
|
101
|
+
agentId: options.agentId,
|
|
102
|
+
timeoutMs: options.timeoutMs,
|
|
103
|
+
env: options.env,
|
|
104
|
+
});
|
|
105
|
+
tasks.push({
|
|
106
|
+
caseId: task.caseId,
|
|
107
|
+
raw: rawEligible ? await runSide("raw-full-file", rawContext) : null,
|
|
108
|
+
warm: warmEligible ? await runSide("warm-index-reuse", warmContext) : null,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
result.push({ benchmarkProject: project.benchmarkProject, tasks });
|
|
112
|
+
}
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
async function evaluateRealSide(args) {
|
|
116
|
+
const promptStrategy = SIDE_STRATEGIES[args.variantId];
|
|
117
|
+
const base = { variantId: args.variantId, agentId: args.agentId, promptStrategy };
|
|
118
|
+
let neutralCwd;
|
|
119
|
+
try {
|
|
120
|
+
if (!args.evaluationCase) {
|
|
121
|
+
throw new Error(`Evaluation case ${args.caseId} is not available for real-agent evaluation.`);
|
|
122
|
+
}
|
|
123
|
+
// Generated only for internal scoring/metadata (expectedAnswerKey, promptComplexityLevel); its
|
|
124
|
+
// promptText is never sent to the provider.
|
|
125
|
+
const [promptVariant] = generatePromptVariants({
|
|
126
|
+
cases: [args.evaluationCase],
|
|
127
|
+
projectProfiles: [...args.projectProfiles],
|
|
128
|
+
strategies: [promptStrategy],
|
|
129
|
+
complexityLevels: ["short"],
|
|
130
|
+
});
|
|
131
|
+
if (!promptVariant) {
|
|
132
|
+
throw new Error(`Failed to generate an internal scoring prompt variant for case ${args.caseId}.`);
|
|
133
|
+
}
|
|
134
|
+
const realPromptText = buildWarmIndexRealAgentPrompt({
|
|
135
|
+
evaluationCase: args.evaluationCase,
|
|
136
|
+
variantId: args.variantId,
|
|
137
|
+
contextText: args.contextText,
|
|
138
|
+
});
|
|
139
|
+
// Neutral working directory: the provider must not run from the repository, the benchmark
|
|
140
|
+
// target, the installed package, or the agent artifact directory.
|
|
141
|
+
neutralCwd = await mkdtemp(path.join(os.tmpdir(), "my-dev-kit-lab-warm-agent-"));
|
|
142
|
+
const agentRunResult = await runAgentPrompt({
|
|
143
|
+
runId: `${args.caseId}.${args.agentId}.${args.variantId}`,
|
|
144
|
+
agentId: args.agentId,
|
|
145
|
+
promptVariant,
|
|
146
|
+
promptText: realPromptText,
|
|
147
|
+
promptTransport: "stdin",
|
|
148
|
+
timeoutMs: args.timeoutMs,
|
|
149
|
+
requireAvailable: false,
|
|
150
|
+
env: args.env,
|
|
151
|
+
cwd: neutralCwd,
|
|
152
|
+
outDir: args.outDir,
|
|
153
|
+
});
|
|
154
|
+
return buildSideEvidenceFromRun({
|
|
155
|
+
base,
|
|
156
|
+
caseId: args.caseId,
|
|
157
|
+
outDir: args.outDir,
|
|
158
|
+
agentRunResult,
|
|
159
|
+
expectedAnswerKey: promptVariant.expectedAnswerKey,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
return buildFailedSideEvidence(base, error);
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
if (neutralCwd) {
|
|
167
|
+
try {
|
|
168
|
+
await rm(neutralCwd, { recursive: true, force: true });
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
// Best-effort cleanup only: { force: true } suppresses a missing path, but not an
|
|
172
|
+
// EPERM/EBUSY from an OS still holding this directory open -- for example a timed-out
|
|
173
|
+
// provider process whose kill did not (yet) fully release it as its own cwd. A cleanup
|
|
174
|
+
// failure here must never replace or hide the side's already-classified evidence.
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// Shared scoring/classification/evidence assembly for both the fake-agent and real-agent paths.
|
|
180
|
+
function buildSideEvidenceFromRun(args) {
|
|
181
|
+
const { base, caseId, outDir, agentRunResult, expectedAnswerKey } = args;
|
|
182
|
+
const parsedAnswer = parseAgentAnswer({
|
|
183
|
+
text: agentRunResult.finalAnswerText,
|
|
184
|
+
answerKey: expectedAnswerKey,
|
|
185
|
+
tokenUsage: agentRunResult.tokenUsage,
|
|
186
|
+
});
|
|
187
|
+
const classification = classifyAgentRunOutcome({ agentRunResult, parsedAnswer });
|
|
188
|
+
const correctness = scoreCorrectness({
|
|
189
|
+
caseId,
|
|
190
|
+
answerKey: expectedAnswerKey,
|
|
191
|
+
parsedAnswer,
|
|
192
|
+
status: classification.status,
|
|
193
|
+
});
|
|
194
|
+
const scoreable = SCOREABLE_STATUSES.has(classification.status) && Number.isFinite(correctness.correctnessScore);
|
|
195
|
+
const totalTokens = agentRunResult.tokenUsage.totalTokens;
|
|
196
|
+
return {
|
|
197
|
+
...base,
|
|
198
|
+
status: classification.status,
|
|
199
|
+
correctness: {
|
|
200
|
+
available: scoreable,
|
|
201
|
+
score: scoreable ? correctness.correctnessScore : null,
|
|
202
|
+
passed: scoreable ? correctness.passed : null,
|
|
203
|
+
failureReasons: [...correctness.failureReasons],
|
|
204
|
+
},
|
|
205
|
+
tokenUsage: {
|
|
206
|
+
totalTokens: typeof totalTokens === "number" && Number.isFinite(totalTokens) ? totalTokens : null,
|
|
207
|
+
source: agentRunResult.tokenUsageSource,
|
|
208
|
+
reliability: agentRunResult.tokenUsageReliability,
|
|
209
|
+
},
|
|
210
|
+
durationMs: agentRunResult.durationMs,
|
|
211
|
+
warnings: [...classification.warnings],
|
|
212
|
+
errors: [...classification.errors],
|
|
213
|
+
artifactPaths: {
|
|
214
|
+
promptPath: path.join(outDir, "prompt.txt"),
|
|
215
|
+
agentRunResultPath: path.join(outDir, "agent-run-result.json"),
|
|
216
|
+
stdoutPath: agentRunResult.stdoutPath,
|
|
217
|
+
stderrPath: agentRunResult.stderrPath,
|
|
218
|
+
telemetryPath: agentRunResult.telemetryPath,
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
function buildFailedSideEvidence(base, error) {
|
|
223
|
+
return {
|
|
224
|
+
...base,
|
|
225
|
+
status: "failed",
|
|
226
|
+
correctness: { available: false, score: null, passed: null, failureReasons: [] },
|
|
227
|
+
tokenUsage: { totalTokens: null, source: "unavailable", reliability: "unavailable" },
|
|
228
|
+
durationMs: null,
|
|
229
|
+
warnings: [],
|
|
230
|
+
errors: [error instanceof Error ? error.message : String(error)],
|
|
231
|
+
artifactPaths: {},
|
|
232
|
+
};
|
|
233
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// v0.5.2 Batch 1 -- frozen warm-index real-agent campaign preset contract.
|
|
2
|
+
//
|
|
3
|
+
// This module owns preset identity, provider assignment, bundled resource paths, the frozen case
|
|
4
|
+
// list per preset, and the default per-agent timeout. It contains no CLI parsing, no report
|
|
5
|
+
// rendering, and no provider execution logic; those remain owned elsewhere.
|
|
6
|
+
export const WARM_INDEX_CAMPAIGN_PRESET_IDS = ["codex-full", "claude-full", "codex-timeout-isolation"];
|
|
7
|
+
const CASES_RESOURCE_PATH = "benchmarks/contracts/warm-index-benchmark-cases.json";
|
|
8
|
+
const PROJECT_PROFILES_RESOURCE_PATH = "benchmarks/contracts/benchmark-project-profiles.json";
|
|
9
|
+
const DEFAULT_TIMEOUT_MS = 240_000;
|
|
10
|
+
// Exact v0.5.1 12-case corpus, in corpus order. Frozen; do not reorder or extend in Batch 1.
|
|
11
|
+
const FULL_CAMPAIGN_CASE_IDS = Object.freeze([
|
|
12
|
+
"warm-medium-import-dedupe",
|
|
13
|
+
"warm-medium-create-project-task",
|
|
14
|
+
"warm-medium-complete-idempotent",
|
|
15
|
+
"warm-medium-composite-filter",
|
|
16
|
+
"warm-medium-project-summary",
|
|
17
|
+
"warm-medium-broad-workflow-map",
|
|
18
|
+
"warm-large-health-label",
|
|
19
|
+
"warm-large-ts-analytics-snapshot",
|
|
20
|
+
"warm-large-ts-leaderboard",
|
|
21
|
+
"warm-large-python-parser-metrics",
|
|
22
|
+
"warm-large-python-pipeline",
|
|
23
|
+
"warm-large-broad-analytics-comparison",
|
|
24
|
+
]);
|
|
25
|
+
// Exactly the three large/mixed cases covering cross-module, localized, and broad-change.
|
|
26
|
+
const TIMEOUT_ISOLATION_CASE_IDS = Object.freeze([
|
|
27
|
+
"warm-large-health-label",
|
|
28
|
+
"warm-large-ts-leaderboard",
|
|
29
|
+
"warm-large-broad-analytics-comparison",
|
|
30
|
+
]);
|
|
31
|
+
const WARM_INDEX_CAMPAIGN_PRESETS = Object.freeze({
|
|
32
|
+
"codex-full": Object.freeze({
|
|
33
|
+
id: "codex-full",
|
|
34
|
+
agentId: "codex",
|
|
35
|
+
casesResourcePath: CASES_RESOURCE_PATH,
|
|
36
|
+
projectProfilesResourcePath: PROJECT_PROFILES_RESOURCE_PATH,
|
|
37
|
+
caseIds: FULL_CAMPAIGN_CASE_IDS,
|
|
38
|
+
defaultTimeoutMs: DEFAULT_TIMEOUT_MS,
|
|
39
|
+
}),
|
|
40
|
+
"claude-full": Object.freeze({
|
|
41
|
+
id: "claude-full",
|
|
42
|
+
agentId: "claude",
|
|
43
|
+
casesResourcePath: CASES_RESOURCE_PATH,
|
|
44
|
+
projectProfilesResourcePath: PROJECT_PROFILES_RESOURCE_PATH,
|
|
45
|
+
caseIds: FULL_CAMPAIGN_CASE_IDS,
|
|
46
|
+
defaultTimeoutMs: DEFAULT_TIMEOUT_MS,
|
|
47
|
+
}),
|
|
48
|
+
"codex-timeout-isolation": Object.freeze({
|
|
49
|
+
id: "codex-timeout-isolation",
|
|
50
|
+
agentId: "codex",
|
|
51
|
+
casesResourcePath: CASES_RESOURCE_PATH,
|
|
52
|
+
projectProfilesResourcePath: PROJECT_PROFILES_RESOURCE_PATH,
|
|
53
|
+
caseIds: TIMEOUT_ISOLATION_CASE_IDS,
|
|
54
|
+
defaultTimeoutMs: DEFAULT_TIMEOUT_MS,
|
|
55
|
+
}),
|
|
56
|
+
});
|
|
57
|
+
/** Fails explicitly for any value outside the exact three frozen preset IDs. */
|
|
58
|
+
export function parseWarmIndexCampaignPresetId(value) {
|
|
59
|
+
if (WARM_INDEX_CAMPAIGN_PRESET_IDS.includes(value)) {
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
throw new Error(`Unknown warm-index campaign preset: ${value}. Supported presets: ${WARM_INDEX_CAMPAIGN_PRESET_IDS.join(", ")}.`);
|
|
63
|
+
}
|
|
64
|
+
export function getWarmIndexCampaignPreset(id) {
|
|
65
|
+
const preset = WARM_INDEX_CAMPAIGN_PRESETS[id];
|
|
66
|
+
if (!preset) {
|
|
67
|
+
throw new Error(`Unknown warm-index campaign preset: ${id}. Supported presets: ${WARM_INDEX_CAMPAIGN_PRESET_IDS.join(", ")}.`);
|
|
68
|
+
}
|
|
69
|
+
return preset;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Resolves the effective per-agent timeout for a campaign: the preset default when no override was
|
|
73
|
+
* requested, or an explicit finite positive integer override.
|
|
74
|
+
*/
|
|
75
|
+
export function resolveWarmIndexCampaignTimeoutMs(preset, requestedTimeoutMs) {
|
|
76
|
+
if (requestedTimeoutMs === undefined) {
|
|
77
|
+
return preset.defaultTimeoutMs;
|
|
78
|
+
}
|
|
79
|
+
if (!Number.isFinite(requestedTimeoutMs) || !Number.isInteger(requestedTimeoutMs) || requestedTimeoutMs <= 0) {
|
|
80
|
+
throw new Error(`Campaign timeoutMs must be a finite positive integer; received ${requestedTimeoutMs}.`);
|
|
81
|
+
}
|
|
82
|
+
return requestedTimeoutMs;
|
|
83
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { invalidExperimentConfig, isPlainObject, mergeConfig, validExperimentConfig } from "../../config.js";
|
|
2
|
+
import { WARM_INDEX_CAMPAIGN_PRESET_IDS } from "./campaignPresets.js";
|
|
2
3
|
export const defaultWarmIndexReuseConfig = {
|
|
3
4
|
casesPath: "examples/token-savings-cases.json",
|
|
4
5
|
projectProfilesPath: "benchmarks/contracts/benchmark-project-profiles.json",
|
|
@@ -12,6 +13,9 @@ const SUPPORTED_FIELDS = [
|
|
|
12
13
|
"kitCommand",
|
|
13
14
|
"caseIds",
|
|
14
15
|
"benchmarkProjects",
|
|
16
|
+
"campaignPreset",
|
|
17
|
+
"includeRealAgents",
|
|
18
|
+
"timeoutMs",
|
|
15
19
|
];
|
|
16
20
|
export const warmIndexReuseConfigDefinition = {
|
|
17
21
|
fields: [
|
|
@@ -21,6 +25,21 @@ export const warmIndexReuseConfigDefinition = {
|
|
|
21
25
|
{ name: "kitCommand", type: "string", description: "my-dev-kit command used for index and retrieval." },
|
|
22
26
|
{ name: "caseIds", type: "array", description: "Evaluation case IDs to include." },
|
|
23
27
|
{ name: "benchmarkProjects", type: "array", description: "Benchmark project IDs to include." },
|
|
28
|
+
{
|
|
29
|
+
name: "campaignPreset",
|
|
30
|
+
type: "string",
|
|
31
|
+
description: `Real-agent campaign preset id: ${WARM_INDEX_CAMPAIGN_PRESET_IDS.join(", ")}. Absent selects the legacy deterministic fake-agent path.`,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: "includeRealAgents",
|
|
35
|
+
type: "boolean",
|
|
36
|
+
description: "Required (must be true) together with campaignPreset; rejected without it.",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: "timeoutMs",
|
|
40
|
+
type: "number",
|
|
41
|
+
description: "Optional per-agent timeout override (ms) for a campaign; defaults to the preset's 240000 ms. Rejected without campaignPreset.",
|
|
42
|
+
},
|
|
24
43
|
],
|
|
25
44
|
};
|
|
26
45
|
/**
|
|
@@ -51,6 +70,38 @@ export function validateWarmIndexReuseConfig(config) {
|
|
|
51
70
|
errors.push(`${field} must be a non-empty array of non-empty strings when provided.`);
|
|
52
71
|
}
|
|
53
72
|
}
|
|
73
|
+
// Campaign fields are validated against the RAW supplied object, not the merged/normalized one,
|
|
74
|
+
// so legacy defaults (casesPath/projectProfilesPath) never look like an explicit override.
|
|
75
|
+
const raw = isPlainObject(config) ? config : {};
|
|
76
|
+
const rawCampaignPreset = raw.campaignPreset;
|
|
77
|
+
if (rawCampaignPreset !== undefined) {
|
|
78
|
+
if (typeof rawCampaignPreset !== "string" || !WARM_INDEX_CAMPAIGN_PRESET_IDS.includes(rawCampaignPreset)) {
|
|
79
|
+
errors.push(`campaignPreset must be one of: ${WARM_INDEX_CAMPAIGN_PRESET_IDS.join(", ")}.`);
|
|
80
|
+
}
|
|
81
|
+
if (raw.casesPath !== undefined) {
|
|
82
|
+
errors.push("campaignPreset cannot be combined with an explicit casesPath; the preset owns the production corpus.");
|
|
83
|
+
}
|
|
84
|
+
if (raw.projectProfilesPath !== undefined) {
|
|
85
|
+
errors.push("campaignPreset cannot be combined with an explicit projectProfilesPath; the preset owns the project profiles.");
|
|
86
|
+
}
|
|
87
|
+
if (raw.includeRealAgents !== true) {
|
|
88
|
+
errors.push("campaignPreset requires includeRealAgents to be exactly true.");
|
|
89
|
+
}
|
|
90
|
+
if (raw.timeoutMs !== undefined) {
|
|
91
|
+
const timeoutMs = raw.timeoutMs;
|
|
92
|
+
if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs) || !Number.isInteger(timeoutMs) || timeoutMs <= 0) {
|
|
93
|
+
errors.push("timeoutMs must be a finite positive integer when campaignPreset is set.");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
if (raw.includeRealAgents !== undefined) {
|
|
99
|
+
errors.push("includeRealAgents is only supported together with campaignPreset.");
|
|
100
|
+
}
|
|
101
|
+
if (raw.timeoutMs !== undefined) {
|
|
102
|
+
errors.push("timeoutMs is only supported together with campaignPreset.");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
54
105
|
if (errors.length > 0) {
|
|
55
106
|
return invalidExperimentConfig(errors);
|
|
56
107
|
}
|