Skip to content

Commit a708e59

Browse files
authored
feat(cli): migrate provider authoring surface (#1714)
* feat(cli): migrate provider authoring surface * fix(cli): preserve rerun bundle target artifacts
1 parent d22968f commit a708e59

123 files changed

Lines changed: 1016 additions & 713 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 29 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# AgentV
22

3-
Test AI targets on real repo tasks and measure what actually works.
3+
Test AI providers on real repo tasks and measure what actually works.
44

55
## Why?
66

@@ -10,18 +10,18 @@ Test AI targets on real repo tasks and measure what actually works.
1010
- **Version-controlled** — evals, judges, and results all live in Git
1111
- **Hybrid graders** — deterministic code checks + LLM-based subjective scoring
1212
- **CI/CD native** — exit codes, JSONL output, threshold flags for pipeline gating
13-
- **Any target** — run against agents, model providers, gateways, replay targets, CLI wrappers, transcript providers, and future app or service wrappers
13+
- **Any provider** — run against agents, model providers, gateways, replay providers, CLI wrappers, transcript providers, and future app or service wrappers
1414

1515
## Core Concepts
1616

1717
- **Eval suite / tests** are the task corpus: the prompts, cases, datasets, and reusable field-local files you want to evaluate.
1818
- **Category** is derived from where the eval lives, such as folder path and file name. Use paths to organize the corpus instead of repeating category labels in every eval.
1919
- **Environment / fixtures / graders** are task-owned context: host or Docker setup, repos, setup scripts, files, fixtures, deterministic checks, and LLM grading prompts.
20-
- **Target** is the system under test: an agent, provider, gateway, replay target, CLI wrapper, transcript provider, or future app/service wrapper. Each eval selects one `target` by configured target `id` or with an eval-local target object.
21-
- **Tags** are run/result grouping labels. `tags.experiment` is the default experiment namespace, such as `with-skills` or `without-skills`; keep suite/category and target/model names out of that tag.
20+
- **Provider** is the configured system under test: an agent, model provider, gateway, replay provider, CLI wrapper, transcript provider, or future app/service wrapper. Each provider entry uses `id` for the backend/spec and optional `label` for the stable AgentV selection and result identity.
21+
- **Tags** are run/result grouping labels. `tags.experiment` is the default experiment namespace, such as `with-skills` or `without-skills`; keep suite/category and provider/model names out of that tag.
2222
- **Evaluate options** configure eval run behavior such as `max_concurrency`, repeat policy, and budgets.
2323
- **Default test** configures inherited per-test defaults such as score `threshold`.
24-
- **Run** is one concrete execution of a tagged eval against a resolved target that writes portable artifacts for readers such as Dashboard, compare, and trend.
24+
- **Run** is one concrete execution of a tagged eval against a resolved provider that writes portable artifacts for readers such as Dashboard, compare, and trend.
2525

2626
## Quick start
2727

@@ -31,21 +31,21 @@ npm install -g agentv
3131
agentv init
3232
```
3333

34-
**2. Configure targets and graders** in `.agentv/config.yaml` — point to the system under test and the reusable grader. Provider settings live under `config`, and target `id` is the selection name used by evals and CLI flags:
34+
**2. Configure providers and graders** in `.agentv/providers.yaml` — point to the system under test and the reusable grader. Provider `id` names the backend/spec; `label` is the stable selection name used by evals and CLI flags:
3535

3636
```yaml
37-
targets:
38-
- id: local-openai
39-
provider: openai
37+
providers:
38+
- id: openai
39+
label: local-openai
4040
runtime: host
4141
config:
4242
api_format: chat
4343
base_url: "{{ env.LOCAL_OPENAI_PROXY_BASE_URL }}"
4444
api_key: "{{ env.LOCAL_OPENAI_PROXY_API_KEY }}"
4545
model: "{{ env.LOCAL_OPENAI_PROXY_MODEL }}"
4646

47-
- id: local-openai-grader
48-
provider: openai
47+
- id: openai
48+
label: local-openai-grader
4949
runtime: host
5050
config:
5151
api_format: chat
@@ -54,7 +54,7 @@ targets:
5454
model: "{{ env.LOCAL_OPENAI_PROXY_MODEL }}"
5555

5656
defaults:
57-
target: local-openai
57+
provider: local-openai
5858
grader: local-openai-grader
5959
```
6060
@@ -82,7 +82,8 @@ options:
8282
description: Code generation quality
8383
tags:
8484
experiment: with-skills
85-
target: local-openai
85+
providers:
86+
- local-openai
8687
evaluate_options:
8788
max_concurrency: 2
8889
@@ -112,25 +113,25 @@ tests:
112113
Plain assertion strings are short-form rubric criteria: AgentV groups them into
113114
`llm-rubric` and writes grader detail to `grading.json.component_results` for
114115
the Dashboard. Use explicit `type: llm-rubric` when you need weights, required
115-
flags, `score_ranges`, a custom grader prompt, a grader target, or output
116+
flags, `score_ranges`, a custom grader prompt, a grader provider, or output
116117
transforms; use string `value` for free-form rubric checks. Executable graders
117118
use `type: script`.
118119

119-
The target can be an eval-local object when this eval needs target settings of its own:
120+
The provider can be an eval-local object when this eval needs provider settings of its own:
120121

121122
```yaml
122-
description: Code generation quality with eval-local target settings
123+
description: Code generation quality with eval-local provider settings
123124
tags:
124125
experiment: with-skills
125-
target:
126-
id: local-mini
127-
provider: openai
128-
runtime: host
129-
config:
130-
api_format: chat
131-
base_url: "{{ env.LOCAL_OPENAI_PROXY_BASE_URL }}"
132-
api_key: "{{ env.LOCAL_OPENAI_PROXY_API_KEY }}"
133-
model: gpt-5.4-mini
126+
providers:
127+
- id: openai
128+
label: local-mini
129+
runtime: host
130+
config:
131+
api_format: chat
132+
base_url: "{{ env.LOCAL_OPENAI_PROXY_BASE_URL }}"
133+
api_key: "{{ env.LOCAL_OPENAI_PROXY_API_KEY }}"
134+
model: gpt-5.4-mini
134135
evaluate_options:
135136
repeat:
136137
count: 2
@@ -148,7 +149,7 @@ tests:
148149
input: Write FizzBuzz in Python
149150
```
150151

151-
`target: local-openai` resolves the configured target id from `.agentv/config.yaml` and uses its provider, model, hooks, and provider settings. The object form above defines a full eval-local target and must include enough provider configuration to run. AgentV records the resolved target information in run artifacts so results can be audited and replayed. The `tags.experiment` label stays `with-skills` because the condition is unchanged; the model/provider variation belongs to the resolved target metadata.
152+
`providers: [local-openai]` resolves the configured provider label from `.agentv/providers.yaml` and uses its backend, model, hooks, and provider settings. The object form above defines a full eval-local provider and must include enough provider configuration to run. AgentV records the resolved provider information in run artifacts so results can be audited and replayed. The `tags.experiment` label stays `with-skills` because the condition is unchanged; the model/provider variation belongs to the resolved provider metadata.
152153

153154
Use `default_test.threshold` for the inherited per-test pass cutoff. `default_test` can also point at a shared file:
154155

@@ -179,7 +180,7 @@ agentv results compare .agentv/results/<baseline-run-id>/.internal/index.jsonl .
179180

180181
## Results
181182

182-
Each run writes a portable bundle directly under `.agentv/results/<run_id>/`. In this example, `tags.experiment: with-skills` names the condition being measured and `target: local-openai` selects the system under test from `.agentv/config.yaml`; both are recorded as metadata, not path segments. The `.internal/index.jsonl` file is the portable row index used by scripts, CI, and `agentv results compare`; per-case sidecars include the resolved eval and target configuration used for the run.
183+
Each run writes a portable bundle directly under `.agentv/results/<run_id>/`. In this example, `tags.experiment: with-skills` names the condition being measured and `providers: [local-openai]` selects the system under test from `.agentv/providers.yaml`; both are recorded as metadata, not path segments. The `.internal/index.jsonl` file is the portable row index used by scripts, CI, and `agentv results compare`; per-case sidecars include the resolved eval and provider configuration used for the run.
183184

184185
```bash
185186
agentv eval evals/my-eval.eval.yaml
@@ -192,7 +193,7 @@ Run bundle layout:
192193
.agentv/results/
193194
├── 2026-06-30T08-30-00-000Z/ # <run_id> — one committed run bundle
194195
│ ├── summary.json # run rollup: metadata, pass rate, counts, cost
195-
│ ├── fizzbuzz--a1b2c3d4/ # <result_dir> for one test/target row
196+
│ ├── fizzbuzz--a1b2c3d4/ # <result_dir> for one test/provider row
196197
│ │ ├── summary.json # optional per-case rollup across samples
197198
│ │ ├── test/ # generated test bundle: frozen inputs for reproducibility
198199
│ │ │ ├── EVAL.yaml # resolved eval spec

apps/cli/src/commands/create/commands.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ export default defineAssertion(({ output }) => {
4242

4343
const EVAL_TEMPLATES: Record<string, (name: string) => string> = {
4444
default: (name: string) => `description: ${name} evaluation suite
45-
target: default
45+
providers:
46+
- default
4647
4748
tests:
4849
- id: sample-test
@@ -54,7 +55,8 @@ tests:
5455
value: "well"
5556
`,
5657
rubric: (name: string) => `description: ${name} evaluation suite
57-
target: default
58+
providers:
59+
- default
5860
5961
tests:
6062
- id: sample-test
@@ -78,11 +80,11 @@ const PROVIDER_TEMPLATE = `#!/usr/bin/env bun
7880
/**
7981
* Custom provider scaffold.
8082
*
81-
* AgentV providers are configured via .agentv/targets.yaml using the CLI provider:
83+
* AgentV providers are configured via .agentv/providers.yaml using the CLI provider:
8284
*
83-
* targets:
84-
* - name: my-target
85-
* provider: cli
85+
* providers:
86+
* - id: cli
87+
* label: my-provider
8688
* command: "bun run .agentv/providers/<name>.ts {PROMPT}"
8789
*
8890
* This script receives the prompt as a CLI argument and prints the response to stdout.
@@ -168,7 +170,7 @@ export const createProviderCommand = command({
168170
await writeFile(filePath, PROVIDER_TEMPLATE);
169171
console.log(`Created ${path.relative(process.cwd(), filePath)} (template: ${templateName})`);
170172
console.log(
171-
`\nConfigure in .agentv/targets.yaml:\n targets:\n - name: ${name}\n provider: cli\n command: "bun run .agentv/providers/${name}.ts {PROMPT}"`,
173+
`\nConfigure in .agentv/providers.yaml:\n providers:\n - id: cli\n label: ${name}\n command: "bun run .agentv/providers/${name}.ts {PROMPT}"`,
172174
);
173175
},
174176
});

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

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,9 @@ function ensureTargetGraph(
6767
.map((entry) => entry.name)
6868
.sort()
6969
.join(', ');
70-
const owner = requestedBy ? ` referenced by target '${requestedBy}'` : '';
70+
const owner = requestedBy ? ` referenced by provider '${requestedBy}'` : '';
7171
throw new Error(
72-
`Target '${name}'${owner} not found in ${targetsFilePath}. Available targets: ${available}`,
72+
`Provider '${name}'${owner} not found in ${targetsFilePath}. Available providers: ${available}`,
7373
);
7474
}
7575
seen.add(name);
@@ -117,7 +117,7 @@ function definitionsWithEvalTargetSpec(
117117
if (!base) {
118118
const available = definitions.map((definition) => definition.name).join(', ');
119119
throw new Error(
120-
`Target '${targetSpec.extends}' not found for eval-local target '${targetSpec.name}'. Available targets: ${available}`,
120+
`Provider '${targetSpec.extends}' not found for eval-local provider '${targetSpec.name}'. Available providers: ${available}`,
121121
);
122122
}
123123
const effective = {
@@ -161,15 +161,37 @@ export const evalBundleCommand = command({
161161
target: multioption({
162162
type: array(string),
163163
long: 'target',
164-
description: 'Target name to bundle (repeatable). Defaults to eval target(s) or default.',
164+
description: '[Removed: use --provider <label>] Former provider selector',
165165
}),
166166
targets: option({
167167
type: optional(string),
168168
long: 'targets',
169+
description: '[Removed: use --providers <path>] Former providers.yaml path',
170+
}),
171+
provider: multioption({
172+
type: array(string),
173+
long: 'provider',
174+
description:
175+
'Provider label to bundle (repeatable). Defaults to eval provider(s) or default.',
176+
}),
177+
providers: option({
178+
type: optional(string),
179+
long: 'providers',
169180
description: 'Path to providers.yaml (overrides discovery)',
170181
}),
171182
},
172183
handler: async (args) => {
184+
if (args.target.length > 0) {
185+
throw new Error(
186+
`--target was removed from agentv eval bundle. Use --provider ${args.target[0]} instead.`,
187+
);
188+
}
189+
if (args.targets !== undefined) {
190+
throw new Error(
191+
`--targets was removed from agentv eval bundle. Use --providers ${args.targets} instead.`,
192+
);
193+
}
194+
173195
const cwd = process.cwd();
174196
const repoRoot = await findRepoRoot(cwd);
175197
const resolvedPaths = await resolveEvalPaths([args.evalPath], cwd);
@@ -192,10 +214,10 @@ export const evalBundleCommand = command({
192214
let targetNames: readonly string[];
193215
if (suite.inlineTarget) {
194216
definitions = [suite.inlineTarget];
195-
targetNames = unique(args.target.length > 0 ? args.target : [suite.inlineTarget.name]);
217+
targetNames = unique(args.provider.length > 0 ? args.provider : [suite.inlineTarget.name]);
196218
} else {
197219
const targetsFilePath = await discoverTargetsFile({
198-
explicitPath: args.targets,
220+
explicitPath: args.providers,
199221
testFilePath: evalFilePath,
200222
repoRoot,
201223
cwd,
@@ -209,8 +231,8 @@ export const evalBundleCommand = command({
209231
);
210232
const suiteTarget = await readTestSuiteTarget(evalFilePath);
211233
targetNames = unique(
212-
args.target.length > 0
213-
? args.target
234+
args.provider.length > 0
235+
? args.provider
214236
: (suite.targets ?? [suite.targetSpec?.name ?? suiteTarget ?? 'default']),
215237
);
216238
for (const targetName of targetNames) {

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

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import path from 'node:path';
33
import { listTargetNames, readTargetDefinitions } from '@agentv/core';
44
import { checkbox, confirm, number, search, select } from '@inquirer/prompts';
55

6-
import { TARGET_FILE_CANDIDATES, fileExists } from '../../utils/targets.js';
6+
import { PROVIDER_FILE_CANDIDATES, fileExists } from '../../utils/targets.js';
77
import {
88
type DiscoveredEvalFile,
99
discoverEvalFiles,
@@ -98,7 +98,7 @@ async function promptMainMenu(
9898
choices.push({
9999
name: '⏯ Resume last run',
100100
value: 'resume',
101-
description: `${dirLabel} (target: ${lastConfig.target})`,
101+
description: `${dirLabel} (provider: ${lastConfig.target})`,
102102
});
103103
}
104104
}
@@ -108,7 +108,7 @@ async function promptMainMenu(
108108
choices.push({
109109
name: '🔄 Rerun last config',
110110
value: 'rerun',
111-
description: `${evalCount} eval file(s), target: ${lastConfig.target}`,
111+
description: `${evalCount} eval file(s), provider: ${lastConfig.target}`,
112112
});
113113
}
114114

@@ -143,7 +143,7 @@ async function promptNewEvaluation(cwd: string): Promise<InteractiveConfig | und
143143
return undefined;
144144
}
145145

146-
// Step 3: Select target
146+
// Step 3: Select provider
147147
const target = await promptTargetSelection(cwd, selectedFiles[0].path);
148148

149149
// Step 4: Advanced options
@@ -217,12 +217,12 @@ async function promptTargetSelection(cwd: string, firstEvalPath: string): Promis
217217
}
218218

219219
if (targetNames.length === 1) {
220-
console.log(`${ANSI_DIM}Using target: ${targetNames[0]}${ANSI_RESET}`);
220+
console.log(`${ANSI_DIM}Using provider: ${targetNames[0]}${ANSI_RESET}`);
221221
return targetNames[0];
222222
}
223223

224224
return search<string>({
225-
message: 'Select a target (type to search)',
225+
message: 'Select a provider (type to search)',
226226
source: async (term) => {
227227
const filtered = term
228228
? targetNames.filter((t) => t.toLowerCase().includes(term.toLowerCase()))
@@ -232,7 +232,7 @@ async function promptTargetSelection(cwd: string, firstEvalPath: string): Promis
232232
return {
233233
name: t,
234234
value: t,
235-
description: def ? `provider: ${def.provider}` : undefined,
235+
description: def ? `backend: ${def.provider}` : undefined,
236236
};
237237
});
238238
},
@@ -263,7 +263,7 @@ async function findTargetsFile(
263263
}
264264

265265
for (const dir of dirsToSearch) {
266-
for (const candidate of TARGET_FILE_CANDIDATES) {
266+
for (const candidate of PROVIDER_FILE_CANDIDATES) {
267267
const fullPath = `${dir}/${candidate}`;
268268
if (await fileExists(fullPath)) {
269269
return fullPath;
@@ -314,7 +314,7 @@ async function promptReviewAndConfirm(config: InteractiveConfig, cwd: string): P
314314
console.log(`\n${ANSI_BOLD}Review Configuration${ANSI_RESET}`);
315315
console.log(`${ANSI_DIM}${'─'.repeat(40)}${ANSI_RESET}`);
316316
console.log(`${ANSI_GREEN}Eval files:${ANSI_RESET}\n${evalDisplay}`);
317-
console.log(`${ANSI_GREEN}Target:${ANSI_RESET} ${config.target}`);
317+
console.log(`${ANSI_GREEN}Provider:${ANSI_RESET} ${config.target}`);
318318
console.log(`${ANSI_GREEN}Workers:${ANSI_RESET} ${config.workers}`);
319319
console.log(`${ANSI_GREEN}Cache:${ANSI_RESET} ${config.cache ? 'yes' : 'no'}`);
320320
console.log(`${ANSI_DIM}${'─'.repeat(40)}${ANSI_RESET}`);

0 commit comments

Comments
 (0)