Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CONCEPTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ Shared domain vocabulary for this project — entities, named processes, and sta

**Target runtime** — The placement/transport mode for invoking a target provider, such as host execution, sandbox/container placement, CLI subprocess, app-server protocol, RPC, or SDK child runner. Runtime describes how the selected agent is invoked. Advanced home/env/profile-style overlays are provider or runtime configuration details, not the authored testbed recipe. Runtime is separate from the environment that prepares files, services, and cwd.

**Grader (selection)** — Not a distinct entity type. A grader is a regular target, listed under `targets` like any other and selected for a grading role via `defaults.grader`, CLI `--grader-target`, or an assertion-level target override. Authoring a separate `graders:` list is a hard error: move each entry into `targets`. This also means a grader can itself be the target of an ordinary eval (oracle/calibration checks), which a separate schema would block. Do not confuse this with **grader (method)** — an assertion `type` such as `llm-rubric`, `code-grader`, or `g-eval` that names *how* scoring works, independent of *which* target performs it; or with a rubric's `criteria`/`value`, which is the grading *prompt*. AgentV once called grader-selection "judge" (`judge_target`) before a deliberate, full rename to "grader" — do not reintroduce "judge" without a stronger reason than surface-level clarity, since peer frameworks (Margin-Lab/evals, Harbor) don't treat judge/grader as a first-class schema concept either.

**Provider runtime boundary** — The process boundary between AgentV's evaluation orchestrator and the agent runtime a provider invokes. CLI-backed providers place the agent runtime outside the orchestrator; SDK providers should run through an AgentV-owned child-runner boundary when runtime teardown can threaten run artifact finalization. This boundary does not own repository/testbed setup by default.

## Evaluation Model
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ targets:
api_key: "{{ env.LOCAL_OPENAI_PROXY_API_KEY }}"
model: "{{ env.LOCAL_OPENAI_PROXY_MODEL }}"

graders:
- id: local-openai-grader
provider: openai
runtime: host
config:
api_format: chat
base_url: "{{ env.LOCAL_OPENAI_PROXY_BASE_URL }}"
Expand Down
13 changes: 10 additions & 3 deletions apps/web/src/content/docs/docs/next/evaluation/running-evals.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,9 @@ default.
### YAML config (`config.yaml` plus optional `config.local.yaml`)

Project config and eval YAML share the same eval-definition graph for targets,
graders, tests, defaults, and supported execution defaults. Small projects can keep that
tests, defaults, and supported execution defaults. A grader is not a separate
part of that graph — it is a target, listed under `targets` like any other,
selected for a grading role via `defaults.grader`. Small projects can keep the
graph inline:

```yaml
Expand All @@ -486,9 +488,9 @@ targets:
command: ["codex", "app-server"]
model: gpt-5-codex

graders:
- id: openai-grader
provider: openai
runtime: host
config:
model: gpt-5-mini

Expand All @@ -511,7 +513,6 @@ directly:

```yaml
targets: file://targets.yaml
graders: file://graders.yaml
tests: file://tests.yaml
defaults: file://defaults.yaml
```
Expand All @@ -523,6 +524,12 @@ defaults: file://defaults.yaml
runtime: host
config:
command: ["codex", "app-server"]

- id: openai-grader
provider: openai
runtime: host
config:
model: gpt-5-mini
```

```yaml
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/content/docs/docs/next/targets/cli-provider.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ targets:
config:
command: python agent.py --prompt {PROMPT} --out {OUTPUT_FILE}

graders:
- id: azure-grader
provider: azure
runtime: host
config:
endpoint: "{{ env.AZURE_OPENAI_ENDPOINT }}"
api_key: "{{ env.AZURE_OPENAI_API_KEY }}"
Expand Down Expand Up @@ -133,9 +133,9 @@ targets:
config:
command: cp fixtures/{EVAL_ID}.expected.txt {OUTPUT_FILE}

graders:
- id: azure-grader
provider: azure
runtime: host
config:
endpoint: "{{ env.AZURE_OPENAI_ENDPOINT }}"
api_key: "{{ env.AZURE_OPENAI_API_KEY }}"
Expand Down
6 changes: 4 additions & 2 deletions apps/web/src/content/docs/docs/next/targets/coding-agents.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ settings live under `config`.
Use `defaults.grader`, CLI `--grader` / `--grader-target`, or an
evaluator-specific target override for LLM-based grading. Grader selection is
separate from the coding-agent target, so target definitions do not carry a
grader field.
grader field. A grader is not a distinct kind of entity either — it is a
regular target, listed under `targets` like any other, selected for a grading
role.

```yaml
targets:
Expand All @@ -27,9 +29,9 @@ targets:
model: gpt-5-codex
reasoning_effort: high

graders:
- id: openai-grader
provider: openai
runtime: host
config:
model: gpt-5-mini

Expand Down
20 changes: 15 additions & 5 deletions apps/web/src/content/docs/docs/next/targets/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ Targets define which agent or LLM provider to evaluate. AgentV uses one
composable config graph across project manifests and eval files:

- `.agentv/config.yaml` is the project-local discovery and composition root. It
can hold targets, graders, tests, defaults, execution policy, results
settings, and repo-local project policy.
can hold targets, tests, defaults, execution policy, results settings, and
repo-local project policy.
- `$AGENTV_HOME/config.yaml` is the user/operator config. Use it for defaults
that apply across projects, project registry data, default result locations,
and provider defaults that should not be copied into each repo.
Expand Down Expand Up @@ -43,9 +43,9 @@ targets:
command: ["codex", "app-server"]
model: gpt-5-codex

graders:
- id: openai-grader
provider: openai
runtime: host
config:
model: gpt-5-mini

Expand All @@ -61,6 +61,11 @@ the shorthand for the current machine, or object form when you need
settings belong under `config`. Process-backed coding-agent providers use
`config.command` as a non-empty argv array.

A grader is not a separate kind of entity — it is a target selected for a
grading role, either through `defaults.grader` (shown above) or an
assertion-level target override. There is no separate `graders:` list;
authoring one is a hard error telling you to move each entry into `targets`.

## Runtime Modes

Use `runtime: host` when you want AgentV to run the target exactly as it is
Expand Down Expand Up @@ -128,9 +133,9 @@ targets:
command: ["codex", "app-server"]
model: gpt-5-codex

graders:
- id: openai-grader
provider: openai
runtime: host
config:
model: gpt-5-mini

Expand All @@ -151,7 +156,6 @@ can be decomposed like this:

```yaml
targets: file://targets.yaml
graders: file://graders.yaml
tests: file://tests.yaml
defaults: file://defaults.yaml
```
Expand All @@ -167,6 +171,12 @@ a bare array, not an object wrapped in `targets:`:
config:
command: ["codex", "app-server"]
model: gpt-5-codex

- id: openai-grader
provider: openai
runtime: host
config:
model: gpt-5-mini
```

```yaml
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,9 +184,9 @@ targets:
config:
base_url: http://localhost:8080

graders:
- id: azure-grader
provider: azure
runtime: host
config:
endpoint: "{{ env.AZURE_OPENAI_ENDPOINT }}"
api_key: "{{ env.AZURE_OPENAI_API_KEY }}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,10 @@ For smoke tests and dogfood runs against a local OpenAI-compatible proxy, keep
the endpoint, model, and placeholder key in environment variables:

```yaml
graders:
targets:
- id: local-openai-grader
provider: openai
runtime: host
config:
api_format: chat
base_url: "{{ env.LOCAL_OPENAI_PROXY_BASE_URL }}"
Expand Down
1 change: 0 additions & 1 deletion examples/features/readme-quickstart/.agentv/config.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
targets: file://targets.yaml
graders: file://graders.yaml
tests: file://tests.yaml
defaults: file://defaults.yaml
7 changes: 0 additions & 7 deletions examples/features/readme-quickstart/.agentv/graders.yaml

This file was deleted.

15 changes: 12 additions & 3 deletions examples/features/readme-quickstart/.agentv/targets.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@
runtime: host
config:
api_format: chat
base_url: ${{ LOCAL_OPENAI_PROXY_BASE_URL }}
api_key: ${{ LOCAL_OPENAI_PROXY_API_KEY }}
model: ${{ LOCAL_OPENAI_PROXY_MODEL }}
base_url: "{{ env.LOCAL_OPENAI_PROXY_BASE_URL }}"
api_key: "{{ env.LOCAL_OPENAI_PROXY_API_KEY }}"
model: "{{ env.LOCAL_OPENAI_PROXY_MODEL }}"

- id: local-openai-grader
provider: openai
runtime: host
config:
api_format: chat
base_url: "{{ env.LOCAL_OPENAI_PROXY_BASE_URL }}"
api_key: "{{ env.LOCAL_OPENAI_PROXY_API_KEY }}"
model: "{{ env.LOCAL_OPENAI_PROXY_MODEL }}"
5 changes: 3 additions & 2 deletions examples/features/readme-quickstart/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ graph into direct field refs:

```yaml
targets: file://targets.yaml
graders: file://graders.yaml
tests: file://tests.yaml
defaults: file://defaults.yaml
```

Each referenced file contains that field's value directly, such as a bare target
array in `.agentv/targets.yaml` and a bare defaults object in
`.agentv/defaults.yaml`.
`.agentv/defaults.yaml`. A grader is not a separate kind of entity — it is a
target listed under `targets` like any other, selected for the grading role
via `defaults.grader`.

Run it against a local OpenAI-compatible endpoint:

Expand Down
69 changes: 25 additions & 44 deletions packages/core/src/evaluation/loaders/config-graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,6 @@ export type NormalizedTargetConfig = {
readonly config: Record<string, unknown>;
};

export type NormalizedGraderConfig = {
readonly id: string;
readonly provider: string;
readonly config: Record<string, unknown>;
};

export type ConfigDefaults = {
readonly target?: string;
readonly grader?: string;
Expand All @@ -78,7 +72,6 @@ export type ConfigExecution = {

export type ComposableConfigGraph = {
readonly targets?: readonly NormalizedTargetConfig[];
readonly graders?: readonly NormalizedGraderConfig[];
readonly tests?: readonly unknown[];
readonly defaults?: ConfigDefaults;
readonly execution?: ConfigExecution;
Expand Down Expand Up @@ -124,13 +117,15 @@ export function normalizeComposableConfigGraph(
configPath: string,
options: NormalizeOptions = {},
): ComposableConfigGraph {
if (rawConfig.graders !== undefined) {
throw new Error(
`Field 'graders' in ${configPath} has been removed. A grader is just a target — move each entry into 'targets' and select it via 'defaults.grader' or an assertion's target override, not a separate grader list.`,
);
}
const graph: ComposableConfigGraph = {
...(rawConfig.targets !== undefined
? { targets: parseTargets(rawConfig.targets, `${configPath}:targets`) }
: {}),
...(rawConfig.graders !== undefined
? { graders: parseGraders(rawConfig.graders, `${configPath}:graders`) }
: {}),
...(rawConfig.tests !== undefined
? { tests: parseArray(rawConfig.tests, `${configPath}:tests`) }
: {}),
Expand Down Expand Up @@ -253,20 +248,6 @@ function parseRuntime(value: unknown, location: string): NormalizedRuntimeConfig
throw new Error(`Invalid ${location}: use 'host' or an object with mode: host|profile|sandbox.`);
}

function parseGraders(value: unknown, location: string): readonly NormalizedGraderConfig[] {
return parseArray(value, location).map((entry, index) => {
const graderLocation = `${location}[${index}]`;
if (!isPlainConfigObject(entry)) {
throw new Error(`Invalid ${graderLocation}: grader must be an object.`);
}
const id = readRequiredString(entry.id, `${graderLocation}.id`);
const provider = readRequiredString(entry.provider, `${graderLocation}.provider`);
const config = readOptionalObject(entry.config, `${graderLocation}.config`) ?? {};
validateCommand(config.command, `${graderLocation}.config.command`);
return { id, provider, config };
});
}

function parseDefaults(value: unknown, location: string): ConfigDefaults {
const defaults = readOptionalObject(value, location);
if (!defaults) {
Expand Down Expand Up @@ -315,27 +296,27 @@ function parseExecution(
}

function validateDefaultSelections(graph: ComposableConfigGraph, configPath: string): void {
// Only validated against targets/graders defined inline in this same config
// document. `defaults.target`/`defaults.grader` may instead name a target
// defined in a separately-discovered `.agentv/targets.yaml`, which this
// graph has no visibility into — that case is resolved (and, on an unknown
// name, reported) lazily at eval-run time, the same way CLI
// `--grader-target` already is.
if (graph.defaults?.target !== undefined && graph.targets && graph.targets.length > 0) {
const targetIds = new Set(graph.targets.map((target) => target.id));
if (!targetIds.has(graph.defaults.target)) {
throw new Error(
`Invalid defaults.target in ${configPath}: '${graph.defaults.target}' does not match a configured target id.`,
);
}
// A grader is just a target selected for a grading role, not a separate
// entity — `defaults.target` and `defaults.grader` both resolve against the
// same `targets` pool. Only validated against targets defined inline in
// this same config document; either may instead name a target defined in a
// separately-discovered `.agentv/targets.yaml`, which this graph has no
// visibility into — that case is resolved (and, on an unknown name,
// reported) lazily at eval-run time, the same way CLI `--grader-target`
// already is.
if (!graph.targets || graph.targets.length === 0) {
return;
}
if (graph.defaults?.grader !== undefined && graph.graders && graph.graders.length > 0) {
const graderIds = new Set(graph.graders.map((grader) => grader.id));
if (!graderIds.has(graph.defaults.grader)) {
throw new Error(
`Invalid defaults.grader in ${configPath}: '${graph.defaults.grader}' does not match a configured grader id.`,
);
}
const targetIds = new Set(graph.targets.map((target) => target.id));
if (graph.defaults?.target !== undefined && !targetIds.has(graph.defaults.target)) {
throw new Error(
`Invalid defaults.target in ${configPath}: '${graph.defaults.target}' does not match a configured target id.`,
);
}
if (graph.defaults?.grader !== undefined && !targetIds.has(graph.defaults.grader)) {
throw new Error(
`Invalid defaults.grader in ${configPath}: '${graph.defaults.grader}' does not match a configured target id.`,
);
}
}

Expand Down
1 change: 0 additions & 1 deletion packages/core/src/evaluation/loaders/config-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,6 @@ function parseConfigObject(
...(refs && { refs }),
...(tags && { tags }),
...(graph.targets && { targets: graph.targets }),
...(graph.graders && { graders: graph.graders }),
...(graph.tests && { tests: graph.tests }),
...(graph.defaults && { defaults: graph.defaults }),
configDir: projectDir,
Expand Down
15 changes: 6 additions & 9 deletions packages/core/src/evaluation/validation/eval-file.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -834,14 +834,6 @@ const ConfigTargetSchema = z
})
.strict();

const ConfigGraderSchema = z
.object({
id: z.string().min(1),
provider: z.string().min(1),
config: JsonRecordSchema.optional(),
})
.strict();

const ConfigDefaultsSchema = z
.object({
target: z.string().min(1).optional(),
Expand Down Expand Up @@ -918,7 +910,12 @@ export const EvalFileSchemaInput: z.ZodType = z.object({
})
.optional(),
// Shared composable config graph fields
graders: z.union([z.array(ConfigGraderSchema), z.string().min(1)]).optional(),
graders: z
.never({
invalid_type_error:
"Top-level 'graders' has been removed. A grader is just a target — move each entry into 'targets' and select it via 'defaults.grader' or an assertion's target override.",
})
.optional(),
defaults: z.union([ConfigDefaultsSchema, z.string().min(1)]).optional(),
// Removed legacy aliases
eval_cases: z
Expand Down
Loading
Loading