@dailephd/my-dev-kit-lab 0.5.1 → 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 +14 -1
- package/README.md +22 -6
- 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/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 +21 -2
- package/package.json +1 -1
|
@@ -41,13 +41,15 @@ export async function runMeasuredCommand(options) {
|
|
|
41
41
|
let spawnError;
|
|
42
42
|
let timedOut = false;
|
|
43
43
|
let timeout;
|
|
44
|
+
let killGraceTimeout;
|
|
45
|
+
let settled = false;
|
|
44
46
|
let child;
|
|
45
47
|
try {
|
|
46
48
|
child = spawn(executable, args, {
|
|
47
49
|
cwd: options.cwd,
|
|
48
50
|
env: { ...process.env, ...options.env },
|
|
49
51
|
shell: false,
|
|
50
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
52
|
+
stdio: options.stdinText !== undefined ? ["pipe", "pipe", "pipe"] : ["ignore", "pipe", "pipe"]
|
|
51
53
|
});
|
|
52
54
|
}
|
|
53
55
|
catch (error) {
|
|
@@ -80,6 +82,7 @@ export async function runMeasuredCommand(options) {
|
|
|
80
82
|
]).then(() => resolve(measured));
|
|
81
83
|
return;
|
|
82
84
|
}
|
|
85
|
+
// stdout/stderr are always piped in both stdio branches above.
|
|
83
86
|
child.stdout.on("data", (chunk) => {
|
|
84
87
|
stdout += String(chunk);
|
|
85
88
|
});
|
|
@@ -89,17 +92,40 @@ export async function runMeasuredCommand(options) {
|
|
|
89
92
|
child.on("error", (error) => {
|
|
90
93
|
spawnError = error.message;
|
|
91
94
|
});
|
|
92
|
-
if (options.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
spawnError = `
|
|
96
|
-
|
|
97
|
-
|
|
95
|
+
if (options.stdinText !== undefined) {
|
|
96
|
+
// A child that exits or closes stdin before we write must not crash the process (EPIPE).
|
|
97
|
+
child.stdin?.on("error", (error) => {
|
|
98
|
+
spawnError = spawnError ?? `Failed to write to command stdin: ${error.message}`;
|
|
99
|
+
});
|
|
100
|
+
child.stdin?.write(options.stdinText, "utf8");
|
|
101
|
+
child.stdin?.end();
|
|
98
102
|
}
|
|
99
|
-
|
|
100
|
-
|
|
103
|
+
// forceTerminateProcess is fire-and-forget (it never confirms the target process tree actually
|
|
104
|
+
// exited: on Windows it spawns a detached `taskkill /T /F` and does not await or check its
|
|
105
|
+
// result). That is normally sufficient, but a grandchild process spawned through an
|
|
106
|
+
// intermediate shim (for example a Windows .cmd launcher) can occasionally outlive an
|
|
107
|
+
// already-killed intermediate process. Without a bounded fallback, a caller whose kill silently
|
|
108
|
+
// failed would await this promise forever instead of receiving the already-known timeout error.
|
|
109
|
+
const KILL_GRACE_MS = 3000;
|
|
110
|
+
const settle = async (exitCode) => {
|
|
111
|
+
if (settled)
|
|
112
|
+
return;
|
|
113
|
+
settled = true;
|
|
114
|
+
if (timeout)
|
|
101
115
|
clearTimeout(timeout);
|
|
102
|
-
|
|
116
|
+
if (killGraceTimeout)
|
|
117
|
+
clearTimeout(killGraceTimeout);
|
|
118
|
+
// The caller's `scripts/cli.ts` entry point sets `process.exitCode` rather than calling
|
|
119
|
+
// `process.exit()`, so the process only exits once Node's event loop actually drains. A
|
|
120
|
+
// still-open child stdio pipe -- the same situation the kill-grace fallback above exists for
|
|
121
|
+
// -- would otherwise keep this process alive forever even though this promise has already
|
|
122
|
+
// resolved. Detaching is safe to do unconditionally: once "close" has fired the streams are
|
|
123
|
+
// already ended, so this is a harmless no-op on the normal (non-fallback) path.
|
|
124
|
+
child.stdout?.destroy();
|
|
125
|
+
child.stderr?.destroy();
|
|
126
|
+
child.stdin?.destroy();
|
|
127
|
+
child.removeAllListeners();
|
|
128
|
+
child.unref();
|
|
103
129
|
const endedAt = new Date().toISOString();
|
|
104
130
|
const durationMs = Date.now() - started;
|
|
105
131
|
await writeArtifact(stdoutPath, stdout);
|
|
@@ -125,6 +151,19 @@ export async function runMeasuredCommand(options) {
|
|
|
125
151
|
};
|
|
126
152
|
await writeArtifact(telemetryPath, JSON.stringify(measured, null, 2));
|
|
127
153
|
resolve(measured);
|
|
154
|
+
};
|
|
155
|
+
if (options.timeoutMs !== undefined) {
|
|
156
|
+
timeout = setTimeout(() => {
|
|
157
|
+
timedOut = true;
|
|
158
|
+
spawnError = `Command timed out after ${options.timeoutMs}ms.`;
|
|
159
|
+
forceTerminateProcess(child.pid);
|
|
160
|
+
killGraceTimeout = setTimeout(() => {
|
|
161
|
+
void settle(null);
|
|
162
|
+
}, KILL_GRACE_MS);
|
|
163
|
+
}, options.timeoutMs);
|
|
164
|
+
}
|
|
165
|
+
child.on("close", (exitCode) => {
|
|
166
|
+
void settle(exitCode);
|
|
128
167
|
});
|
|
129
168
|
});
|
|
130
169
|
return result;
|
|
@@ -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
|
}
|
|
@@ -1,118 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
import { generatePromptVariants } from "../../../prompts/index.js";
|
|
8
|
-
import { taskOutputSegment } from "./selection.js";
|
|
9
|
-
// Classified statuses for which the scorer evaluated an actual agent answer.
|
|
10
|
-
const SCOREABLE_STATUSES = new Set(["completed", "invalid-output"]);
|
|
11
|
-
const SIDE_STRATEGIES = {
|
|
12
|
-
"raw-full-file": "raw-full-file",
|
|
13
|
-
"warm-index-reuse": "my-dev-kit-guided",
|
|
14
|
-
};
|
|
15
|
-
export async function evaluateWarmIndexFakeAgents(options) {
|
|
16
|
-
const casesById = new Map(options.cases.map((evaluationCase) => [evaluationCase.id, evaluationCase]));
|
|
17
|
-
const result = [];
|
|
18
|
-
for (const project of options.projects) {
|
|
19
|
-
const tasks = [];
|
|
20
|
-
for (const task of project.tasks) {
|
|
21
|
-
const evaluationCase = casesById.get(task.caseId);
|
|
22
|
-
const run = (variantId) => evaluateSide({
|
|
23
|
-
evaluationCase,
|
|
24
|
-
caseId: task.caseId,
|
|
25
|
-
variantId,
|
|
26
|
-
projectProfiles: options.projectProfiles,
|
|
27
|
-
outDir: resolveWithinRoot(options.outputRoot, path.join("agents", project.projectSegment, taskOutputSegment(task.caseId), variantId)),
|
|
28
|
-
cwd: options.cwd,
|
|
29
|
-
env: options.env,
|
|
30
|
-
});
|
|
31
|
-
tasks.push({
|
|
32
|
-
caseId: task.caseId,
|
|
33
|
-
raw: task.rawBaseline ? await run("raw-full-file") : null,
|
|
34
|
-
warm: task.warmRetrieval ? await run("warm-index-reuse") : null,
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
result.push({ benchmarkProject: project.benchmarkProject, tasks });
|
|
38
|
-
}
|
|
39
|
-
return result;
|
|
40
|
-
}
|
|
41
|
-
async function evaluateSide(args) {
|
|
42
|
-
const promptStrategy = SIDE_STRATEGIES[args.variantId];
|
|
43
|
-
const base = { variantId: args.variantId, agentId: "fake-agent", promptStrategy };
|
|
44
|
-
try {
|
|
45
|
-
if (!args.evaluationCase) {
|
|
46
|
-
throw new Error(`Evaluation case ${args.caseId} is not available for fake-agent evaluation.`);
|
|
47
|
-
}
|
|
48
|
-
const [promptVariant] = generatePromptVariants({
|
|
49
|
-
cases: [args.evaluationCase],
|
|
50
|
-
projectProfiles: [...args.projectProfiles],
|
|
51
|
-
strategies: [promptStrategy],
|
|
52
|
-
complexityLevels: ["short"],
|
|
53
|
-
});
|
|
54
|
-
if (!promptVariant) {
|
|
55
|
-
throw new Error(`Failed to generate a fake-agent prompt for case ${args.caseId}.`);
|
|
56
|
-
}
|
|
57
|
-
const agentRunResult = await runAgentPrompt({
|
|
58
|
-
runId: `${args.caseId}.fake-agent.${args.variantId}`,
|
|
59
|
-
agentId: "fake-agent",
|
|
60
|
-
promptVariant,
|
|
61
|
-
promptText: promptVariant.promptText,
|
|
62
|
-
cwd: args.cwd,
|
|
63
|
-
outDir: args.outDir,
|
|
64
|
-
env: args.env,
|
|
65
|
-
});
|
|
66
|
-
const parsedAnswer = parseAgentAnswer({
|
|
67
|
-
text: agentRunResult.finalAnswerText,
|
|
68
|
-
answerKey: promptVariant.expectedAnswerKey,
|
|
69
|
-
tokenUsage: agentRunResult.tokenUsage,
|
|
70
|
-
});
|
|
71
|
-
const classification = classifyAgentRunOutcome({ agentRunResult, parsedAnswer });
|
|
72
|
-
const correctness = scoreCorrectness({
|
|
73
|
-
caseId: args.caseId,
|
|
74
|
-
answerKey: promptVariant.expectedAnswerKey,
|
|
75
|
-
parsedAnswer,
|
|
76
|
-
status: classification.status,
|
|
77
|
-
});
|
|
78
|
-
const scoreable = SCOREABLE_STATUSES.has(classification.status) && Number.isFinite(correctness.correctnessScore);
|
|
79
|
-
const totalTokens = agentRunResult.tokenUsage.totalTokens;
|
|
80
|
-
return {
|
|
81
|
-
...base,
|
|
82
|
-
status: classification.status,
|
|
83
|
-
correctness: {
|
|
84
|
-
available: scoreable,
|
|
85
|
-
score: scoreable ? correctness.correctnessScore : null,
|
|
86
|
-
passed: scoreable ? correctness.passed : null,
|
|
87
|
-
failureReasons: [...correctness.failureReasons],
|
|
88
|
-
},
|
|
89
|
-
tokenUsage: {
|
|
90
|
-
totalTokens: typeof totalTokens === "number" && Number.isFinite(totalTokens) ? totalTokens : null,
|
|
91
|
-
source: agentRunResult.tokenUsageSource,
|
|
92
|
-
reliability: agentRunResult.tokenUsageReliability,
|
|
93
|
-
},
|
|
94
|
-
durationMs: agentRunResult.durationMs,
|
|
95
|
-
warnings: [...classification.warnings],
|
|
96
|
-
errors: [...classification.errors],
|
|
97
|
-
artifactPaths: {
|
|
98
|
-
promptPath: path.join(args.outDir, "prompt.txt"),
|
|
99
|
-
agentRunResultPath: path.join(args.outDir, "agent-run-result.json"),
|
|
100
|
-
stdoutPath: agentRunResult.stdoutPath,
|
|
101
|
-
stderrPath: agentRunResult.stderrPath,
|
|
102
|
-
telemetryPath: agentRunResult.telemetryPath,
|
|
103
|
-
},
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
catch (error) {
|
|
107
|
-
return {
|
|
108
|
-
...base,
|
|
109
|
-
status: "failed",
|
|
110
|
-
correctness: { available: false, score: null, passed: null, failureReasons: [] },
|
|
111
|
-
tokenUsage: { totalTokens: null, source: "unavailable", reliability: "unavailable" },
|
|
112
|
-
durationMs: null,
|
|
113
|
-
warnings: [],
|
|
114
|
-
errors: [error instanceof Error ? error.message : String(error)],
|
|
115
|
-
artifactPaths: {},
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
}
|
|
1
|
+
// v0.5.2 Batch 3 -- thin compatibility re-export. The shared bounded evidence types,
|
|
2
|
+
// scoring/classification pipeline, and both the fake-agent and real-agent campaign evaluators now
|
|
3
|
+
// live in one owner: ./agentEvaluation.ts. This file exists only so existing direct imports (for
|
|
4
|
+
// example src/experiments/plugins/warmIndexReuse/metrics.ts and
|
|
5
|
+
// src/report/experiments/buildWarmIndexReuseReport.ts) and existing tests keep working unchanged.
|
|
6
|
+
export { evaluateWarmIndexFakeAgents, } from "./agentEvaluation.js";
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
export * from "./agentEvaluation.js";
|
|
2
|
+
export * from "./campaignPresets.js";
|
|
1
3
|
export * from "./config.js";
|
|
2
4
|
export * from "./execution.js";
|
|
3
5
|
export * from "./executionArtifact.js";
|
|
4
|
-
export * from "./fakeAgentEvaluation.js";
|
|
5
6
|
export * from "./metrics.js";
|
|
6
7
|
export * from "./plugin.js";
|
|
8
|
+
export * from "./realAgentPrompt.js";
|
|
7
9
|
export * from "./selection.js";
|
|
8
10
|
export * from "./warmIndexSession.js";
|