Skip to content

Commit 3a22490

Browse files
authored
fix(cache): honor configured response cache paths
Bead: av-vwa.3
1 parent 58fda3e commit 3a22490

15 files changed

Lines changed: 309 additions & 20 deletions

File tree

apps/cli/src/commands/eval/commands/run.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,14 @@ export const evalRunCommand = command({
108108
long: 'cache',
109109
description: 'Enable provider response cache (persisted to disk)',
110110
}),
111+
cachePath: option({
112+
type: optional(string),
113+
long: 'cache-path',
114+
description: 'Enable provider response cache at the given directory',
115+
}),
111116
noCache: flag({
112117
long: 'no-cache',
113-
description: 'Disable caching (overrides YAML execution.cache)',
118+
description: 'Disable response caching (overrides --cache, --cache-path, config, and YAML)',
114119
}),
115120
verbose: flag({
116121
long: 'verbose',
@@ -262,6 +267,7 @@ export const evalRunCommand = command({
262267
agentTimeout: args.agentTimeout,
263268
maxRetries: args.maxRetries,
264269
cache: args.cache,
270+
cachePath: args.cachePath,
265271
noCache: args.noCache,
266272
verbose: args.verbose,
267273
workspaceMode: args.workspaceMode,

apps/cli/src/commands/eval/run-eval.ts

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,10 @@ interface NormalizedOptions {
9696
readonly agentTimeoutSeconds?: number;
9797
readonly maxRetries: number;
9898
readonly cache: boolean;
99+
readonly cachePath?: string;
99100
readonly noCache: boolean;
101+
readonly tsConfigCache?: boolean;
102+
readonly tsConfigCachePath?: string;
100103
readonly verbose: boolean;
101104
readonly otelFile?: string;
102105
readonly exportOtel: boolean;
@@ -313,13 +316,12 @@ function normalizeOptions(
313316
const cliMaxRetries = normalizeOptionalNumber(rawOptions.maxRetries);
314317
const configMaxRetries = config?.execution?.maxRetries;
315318

316-
// Cache: CLI flags take priority, then config file, then default (true via shouldEnableCache)
317-
const cliCache = normalizeBoolean(rawOptions.cache);
319+
// Response cache: CLI request/path, then eval YAML, then TypeScript config, then default off.
320+
const cliCachePath = normalizeString(rawOptions.cachePath);
321+
const cliCache = normalizeBoolean(rawOptions.cache) || cliCachePath !== undefined;
318322
const cliNoCache = normalizeBoolean(rawOptions.noCache);
319323
const configCacheEnabled = config?.cache?.enabled;
320-
// If neither --cache nor --no-cache was passed, use config value
321-
const resolvedCache = cliCache || (!cliNoCache && configCacheEnabled === true);
322-
const resolvedNoCache = cliNoCache;
324+
const configCachePath = normalizeString(config?.cache?.path);
323325

324326
// Output dir: CLI --out > config output.dir > auto-generated
325327
const cliOut = normalizeString(rawOptions.out);
@@ -352,8 +354,11 @@ function normalizeOptions(
352354
dryRunDelayMax: normalizeNumber(rawOptions.dryRunDelayMax, 0),
353355
agentTimeoutSeconds: cliAgentTimeout ?? configAgentTimeoutSeconds,
354356
maxRetries: cliMaxRetries ?? configMaxRetries ?? 2,
355-
cache: resolvedCache,
356-
noCache: resolvedNoCache,
357+
cache: cliCache,
358+
cachePath: cliCachePath,
359+
noCache: cliNoCache,
360+
tsConfigCache: configCacheEnabled,
361+
tsConfigCachePath: configCachePath,
357362
// Boolean OR: config `true` cannot be overridden to `false` from CLI.
358363
// Intentional — there are no --no-verbose / --no-keep-workspaces flags.
359364
// Precedence: CLI > YAML config > TS config
@@ -1311,13 +1316,15 @@ export async function runEvalCommand(
13111316
cliCache: options.cache,
13121317
cliNoCache: options.noCache,
13131318
yamlCache: yamlCacheEnabled,
1319+
tsConfigCache: options.tsConfigCache,
13141320
});
1321+
const activeCachePath = options.cachePath ?? yamlCachePath ?? options.tsConfigCachePath;
13151322
const cache = cacheEnabled
1316-
? new ResponseCache(yamlCachePath ? path.resolve(yamlCachePath) : undefined)
1323+
? new ResponseCache(activeCachePath ? path.resolve(activeCachePath) : undefined)
13171324
: undefined;
13181325

1319-
if (cacheEnabled) {
1320-
console.log(`Response cache: enabled${yamlCachePath ? ` (${yamlCachePath})` : ''}`);
1326+
if (cache) {
1327+
console.log(`Response cache: enabled (${cache.cachePath})`);
13211328
}
13221329

13231330
// Resolve suite-level threshold: CLI --threshold takes precedence over YAML execution.threshold.

apps/cli/test/eval.integration.test.ts

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,23 @@ async function readDiagnostics(fixture: EvalFixture): Promise<Record<string, unk
199199
throw new Error(`Missing diagnostics file: ${fixture.diagnosticsPath}`);
200200
}
201201

202+
async function writeTsCacheConfig(fixture: EvalFixture, cachePath: string): Promise<void> {
203+
await writeFile(
204+
path.join(fixture.suiteDir, 'agentv.config.ts'),
205+
`export default { cache: { enabled: true, path: ${JSON.stringify(cachePath)} } };\n`,
206+
'utf8',
207+
);
208+
}
209+
210+
async function prependYamlCacheConfig(fixture: EvalFixture, cachePath: string): Promise<void> {
211+
const original = await readFile(fixture.testFilePath, 'utf8');
212+
await writeFile(
213+
fixture.testFilePath,
214+
`execution:\n cache: true\n cache_path: ${cachePath}\n\n${original}`,
215+
'utf8',
216+
);
217+
}
218+
202219
describe('agentv eval CLI', () => {
203220
it('writes results, summary, and prompt dumps using default directories', async () => {
204221
const fixture = await createFixture();
@@ -285,4 +302,117 @@ describe('agentv eval CLI', () => {
285302
await rm(fixture.baseDir, { recursive: true, force: true });
286303
}
287304
}, 30_000);
305+
306+
it('honors agentv.config.ts cache.path when response cache is enabled there', async () => {
307+
const fixture = await createFixture();
308+
try {
309+
const cachePath = '.agentv/ts-response-cache';
310+
await writeTsCacheConfig(fixture, cachePath);
311+
312+
const { stdout } = await runCli(fixture, ['eval', fixture.testFilePath]);
313+
314+
const resolvedCachePath = path.resolve(fixture.suiteDir, cachePath);
315+
expect(stdout).toContain(`Response cache: enabled (${resolvedCachePath})`);
316+
const diagnostics = await readDiagnostics(fixture);
317+
expect(diagnostics).toMatchObject({
318+
hasCache: true,
319+
cachePath: resolvedCachePath,
320+
useCache: true,
321+
});
322+
} finally {
323+
await rm(fixture.baseDir, { recursive: true, force: true });
324+
}
325+
}, 30_000);
326+
327+
it('honors eval YAML execution.cache_path', async () => {
328+
const fixture = await createFixture();
329+
try {
330+
const cachePath = '.agentv/yaml-response-cache';
331+
await prependYamlCacheConfig(fixture, cachePath);
332+
333+
const { stdout } = await runCli(fixture, ['eval', fixture.testFilePath]);
334+
335+
const resolvedCachePath = path.resolve(fixture.suiteDir, cachePath);
336+
expect(stdout).toContain(`Response cache: enabled (${resolvedCachePath})`);
337+
const diagnostics = await readDiagnostics(fixture);
338+
expect(diagnostics).toMatchObject({
339+
hasCache: true,
340+
cachePath: resolvedCachePath,
341+
useCache: true,
342+
});
343+
} finally {
344+
await rm(fixture.baseDir, { recursive: true, force: true });
345+
}
346+
}, 30_000);
347+
348+
it('honors CLI --cache-path as an explicit response cache opt-in', async () => {
349+
const fixture = await createFixture();
350+
try {
351+
const cachePath = '.agentv/cli-response-cache';
352+
353+
const { stdout } = await runCli(fixture, [
354+
'eval',
355+
fixture.testFilePath,
356+
'--cache-path',
357+
cachePath,
358+
]);
359+
360+
const resolvedCachePath = path.resolve(fixture.suiteDir, cachePath);
361+
expect(stdout).toContain(`Response cache: enabled (${resolvedCachePath})`);
362+
const diagnostics = await readDiagnostics(fixture);
363+
expect(diagnostics).toMatchObject({
364+
hasCache: true,
365+
cachePath: resolvedCachePath,
366+
useCache: true,
367+
});
368+
} finally {
369+
await rm(fixture.baseDir, { recursive: true, force: true });
370+
}
371+
}, 30_000);
372+
373+
it('lets --no-cache override config-driven response cache settings', async () => {
374+
const fixture = await createFixture();
375+
try {
376+
await writeTsCacheConfig(fixture, '.agentv/disabled-response-cache');
377+
378+
const { stdout } = await runCli(fixture, ['eval', fixture.testFilePath, '--no-cache']);
379+
380+
expect(stdout).not.toContain('Response cache: enabled');
381+
const diagnostics = await readDiagnostics(fixture);
382+
expect(diagnostics).toMatchObject({
383+
hasCache: false,
384+
cachePath: null,
385+
useCache: false,
386+
});
387+
} finally {
388+
await rm(fixture.baseDir, { recursive: true, force: true });
389+
}
390+
}, 30_000);
391+
392+
it('keeps response cache help separate from transcript replay terminology', async () => {
393+
const result = await execa('bun', ['--no-env-file', CLI_ENTRY, 'eval', 'run', '--help'], {
394+
cwd: projectRoot,
395+
env: { ...process.env, CI: 'true' },
396+
reject: false,
397+
});
398+
const helpText = `${result.stdout}\n${result.stderr}`;
399+
expect(helpText).toContain('--cache');
400+
expect(helpText).toContain('--cache-path');
401+
expect(helpText).toContain('--transcript');
402+
403+
const cacheHelp = helpText
404+
.split(/\r?\n/)
405+
.filter((line) => line.includes('--cache'))
406+
.join('\n')
407+
.toLowerCase();
408+
expect(cacheHelp).toContain('response cache');
409+
expect(cacheHelp).not.toContain('replay');
410+
411+
const transcriptHelp = helpText
412+
.split(/\r?\n/)
413+
.filter((line) => line.includes('--transcript'))
414+
.join('\n')
415+
.toLowerCase();
416+
expect(transcriptHelp).not.toContain('cache');
417+
}, 30_000);
288418
});

apps/cli/test/fixtures/mock-run-evaluation.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ interface RunEvaluationOptionsLike {
2525
readonly onResult?: (result: EvaluationResultLike) => Promise<void> | void;
2626
}
2727

28+
function getCachePath(cache: unknown): string | null {
29+
if (!cache || typeof cache !== 'object') {
30+
return null;
31+
}
32+
const maybeCachePath = (cache as { readonly cachePath?: unknown }).cachePath;
33+
return typeof maybeCachePath === 'string' ? maybeCachePath : null;
34+
}
35+
2836
interface EvaluationResultLike {
2937
readonly testId: string;
3038
readonly score: number;
@@ -82,6 +90,8 @@ async function maybeWriteDiagnostics(
8290
agentTimeoutMs: options.agentTimeoutMs ?? null,
8391
promptDumpDir: options.promptDumpDir,
8492
filter: options.filter ?? null,
93+
hasCache: options.cache !== undefined,
94+
cachePath: getCachePath(options.cache),
8595
useCache: options.useCache ?? false,
8696
envSample: process.env.CLI_ENV_SAMPLE ?? null,
8797
envRootOnly: process.env.CLI_ENV_ROOT_ONLY ?? null,

apps/web/src/content/docs/docs/evaluation/running-evals.mdx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,40 @@ The `{timestamp}` placeholder is replaced with an ISO-like timestamp (e.g., `202
427427

428428
**Precedence:** CLI flags > project-local `.agentv/config.yaml` > home/global `$AGENTV_HOME/config.yaml` (or `~/.agentv/config.yaml`) > `agentv.config.ts` > built-in defaults.
429429

430+
## Response Cache
431+
432+
AgentV's response cache stores exact provider responses on disk to reduce repeated live LLM calls while iterating on the same eval. It is disabled by default. Enable it from the CLI with `--cache`, or enable it with a custom directory using `--cache-path`:
433+
434+
```bash
435+
agentv eval evals/dataset.eval.yaml --cache
436+
agentv eval evals/dataset.eval.yaml --cache-path .agentv/response-cache
437+
```
438+
439+
Eval YAML can enable the same cache per suite:
440+
441+
```yaml
442+
execution:
443+
cache: true
444+
cache_path: .agentv/response-cache
445+
```
446+
447+
Project TypeScript config can set the project default:
448+
449+
```typescript
450+
import { defineConfig } from '@agentv/core';
451+
452+
export default defineConfig({
453+
cache: {
454+
enabled: true,
455+
path: '.agentv/response-cache',
456+
},
457+
});
458+
```
459+
460+
`--no-cache` disables response caching regardless of CLI, eval YAML, or TypeScript config. Cache path precedence is `--cache-path` > eval YAML `execution.cache_path` > TypeScript config `cache.path` > `.agentv/cache`.
461+
462+
Response cache and replay are separate concepts. The response cache is an iteration aid for repeated live provider calls. Transcript or fixture replay is target substitution from curated artifacts, and graders still run fresh against the replayed output.
463+
430464
## Environment Variables
431465

432466
### AGENTV_HOME

packages/core/src/evaluation/cache/response-cache.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const DEFAULT_CACHE_PATH = '.agentv/cache';
1313
* Directory structure: <cache_path>/<first-2-chars>/<full-hash>.json
1414
*/
1515
export class ResponseCache implements EvaluationCache {
16-
private readonly cachePath: string;
16+
readonly cachePath: string;
1717

1818
constructor(cachePath?: string) {
1919
this.cachePath = cachePath ?? DEFAULT_CACHE_PATH;
@@ -47,16 +47,21 @@ export class ResponseCache implements EvaluationCache {
4747
*
4848
* Precedence:
4949
* 1. --no-cache CLI flag → always disabled
50-
* 2. --cache CLI flag OR execution.cache YAML → enabled
51-
* 3. Default → disabled (safe for variability testing)
50+
* 2. --cache CLI flag → enabled
51+
* 3. execution.cache YAML → enabled/disabled for that eval file
52+
* 4. agentv.config.ts cache.enabled → project default
53+
* 5. Default → disabled (safe for variability testing)
5254
*/
5355
export function shouldEnableCache(params: {
5456
cliCache: boolean;
5557
cliNoCache: boolean;
5658
yamlCache?: boolean;
59+
tsConfigCache?: boolean;
5760
}): boolean {
5861
if (params.cliNoCache) return false;
59-
return params.cliCache || params.yamlCache === true;
62+
if (params.cliCache) return true;
63+
if (params.yamlCache !== undefined) return params.yamlCache;
64+
return params.tsConfigCache === true;
6065
}
6166

6267
/**

packages/core/src/evaluation/config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ const AgentVConfigSchema = z.object({
6464
.object({
6565
/** Enable response caching */
6666
enabled: z.boolean().optional(),
67-
/** Cache file path */
67+
/** Response cache directory */
6868
path: z.string().optional(),
6969
})
7070
.optional(),

0 commit comments

Comments
 (0)