@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.
Files changed (28) hide show
  1. package/CHANGELOG.md +14 -1
  2. package/README.md +22 -6
  3. package/dist/src/agents/adapters/claudeAdapter.js +47 -4
  4. package/dist/src/agents/adapters/codexAdapter.js +64 -5
  5. package/dist/src/cli/help.js +22 -1
  6. package/dist/src/commands/runExperimentDescribeCommand.js +6 -1
  7. package/dist/src/commands/runExperimentRunCommand.js +87 -5
  8. package/dist/src/commands/runWarmIndexCampaignPresentation.js +64 -0
  9. package/dist/src/core/runMeasuredCommand.js +49 -10
  10. package/dist/src/experiments/plugins/warmIndexReuse/agentEvaluation.js +233 -0
  11. package/dist/src/experiments/plugins/warmIndexReuse/campaignPresets.js +83 -0
  12. package/dist/src/experiments/plugins/warmIndexReuse/config.js +51 -0
  13. package/dist/src/experiments/plugins/warmIndexReuse/fakeAgentEvaluation.js +6 -118
  14. package/dist/src/experiments/plugins/warmIndexReuse/index.js +3 -1
  15. package/dist/src/experiments/plugins/warmIndexReuse/metrics.js +35 -11
  16. package/dist/src/experiments/plugins/warmIndexReuse/plugin.js +50 -14
  17. package/dist/src/experiments/plugins/warmIndexReuse/realAgentPrompt.js +52 -0
  18. package/dist/src/experiments/plugins/warmIndexReuse/selection.js +35 -0
  19. package/dist/src/gallery/index.js +1 -0
  20. package/dist/src/gallery/writeGalleryManifest.js +5 -1
  21. package/dist/src/gallery/writeWarmIndexCampaignGallery.js +116 -0
  22. package/dist/src/plots/buildWarmIndexPlotData.js +21 -2
  23. package/dist/src/report/experiments/buildPluginExperimentReport.js +35 -8
  24. package/dist/src/report/experiments/buildWarmIndexReuseReport.js +138 -12
  25. package/dist/src/report/experiments/renderPluginExperimentReportText.js +35 -12
  26. package/dist/src/report/experiments/renderWarmIndexReuseHtml.js +45 -14
  27. package/docs/METRICS.md +21 -2
  28. 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.timeoutMs !== undefined) {
93
- timeout = setTimeout(() => {
94
- timedOut = true;
95
- spawnError = `Command timed out after ${options.timeoutMs}ms.`;
96
- forceTerminateProcess(child.pid);
97
- }, options.timeoutMs);
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
- child.on("close", async (exitCode) => {
100
- if (timeout) {
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
- import path from "node:path";
2
- import { runAgentPrompt } from "../../../agents/index.js";
3
- import { resolveWithinRoot } from "../../../core/pathSafety.js";
4
- import { classifyAgentRunOutcome } from "../../../evaluation/classifyAgentRunOutcome.js";
5
- import { parseAgentAnswer } from "../../../evaluation/parseAgentAnswer.js";
6
- import { scoreCorrectness } from "../../../evaluation/scoreCorrectness.js";
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";