Skip to content

Commit 2f2f46a

Browse files
authored
feat: add composable target runtime config contract (#1625)
* feat(config): add composable AgentV config graph * feat(results): add target execution envelopes * feat(providers): isolate coding agent sdk targets * feat(providers): add explicit codex runtime targets * feat(runtime): add sandbox target runner * feat(providers): add pi rpc runtime boundary * feat(providers): align coding agent target contracts * docs: document target runtime config graph * test: align runtime provider fixtures with config contract * test: use explicit codex sdk provider in CLI fixture
1 parent ad619ef commit 2f2f46a

112 files changed

Lines changed: 9333 additions & 1838 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: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ Test AI targets on real repo tasks and measure what actually works.
1717
- **Eval suite / imports / tests** are the task corpus: the prompts, cases, datasets, and imported benchmarks 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
- **Workspace / fixtures / graders** are task-owned context: repos, setup scripts, files, fixtures, isolation, 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`, either by label from `targets.yaml` or with an eval-local target object.
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.
2121
- **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.
22-
- **Evaluate options** configure runner-level behavior such as repeat policy, optional timeouts, and `max_concurrency` under `evaluate_options`.
22+
- **Execution** configures runner-level behavior such as general suite concurrency with `execution.max_concurrency`; `evaluate_options` holds eval behaviors such as repeat policy and budgets.
2323
- **Default test** configures inherited per-test defaults such as score `threshold`.
2424
- **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.
2525

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

34-
**2. Configure targets** in `.agentv/targets.yaml` — point to the system under test, such as an agent, provider, gateway, replay source, or CLI wrapper. Provider-specific budgets belong here:
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:
3535

3636
```yaml
3737
targets:
38-
- label: local-openai
38+
- id: local-openai
3939
provider: openai
40-
api_format: chat
41-
base_url: ${{ LOCAL_OPENAI_PROXY_BASE_URL }}
42-
api_key: ${{ LOCAL_OPENAI_PROXY_API_KEY }}
43-
model: ${{ LOCAL_OPENAI_PROXY_MODEL }}
40+
runtime: host
41+
config:
42+
api_format: chat
43+
base_url: ${{ LOCAL_OPENAI_PROXY_BASE_URL }}
44+
api_key: ${{ LOCAL_OPENAI_PROXY_API_KEY }}
45+
model: ${{ LOCAL_OPENAI_PROXY_MODEL }}
46+
47+
graders:
48+
- id: local-openai-grader
49+
provider: openai
50+
config:
51+
api_format: chat
52+
base_url: ${{ LOCAL_OPENAI_PROXY_BASE_URL }}
53+
api_key: ${{ LOCAL_OPENAI_PROXY_API_KEY }}
54+
model: ${{ LOCAL_OPENAI_PROXY_MODEL }}
55+
56+
defaults:
57+
target: local-openai
58+
grader: local-openai-grader
4459
```
4560
4661
**3. Create shared test defaults** in `evals/default-test.yaml`. This is a promptfoo-style partial test config that AgentV applies to each test:
@@ -68,7 +83,7 @@ description: Code generation quality
6883
tags:
6984
experiment: with-skills
7085
target: local-openai
71-
evaluate_options:
86+
execution:
7287
max_concurrency: 1
7388
7489
default_test: file://./default-test.yaml
@@ -104,8 +119,14 @@ description: Code generation quality with eval-local target settings
104119
tags:
105120
experiment: with-skills
106121
target:
107-
extends: local-openai
108-
model: gpt-5.4-mini
122+
id: local-mini
123+
provider: openai
124+
runtime: host
125+
config:
126+
api_format: chat
127+
base_url: ${{ LOCAL_OPENAI_PROXY_BASE_URL }}
128+
api_key: ${{ LOCAL_OPENAI_PROXY_API_KEY }}
129+
model: gpt-5.4-mini
109130
evaluate_options:
110131
repeat:
111132
count: 2
@@ -119,7 +140,7 @@ tests:
119140
input: Write FizzBuzz in Python
120141
```
121142

122-
`target: local-openai` resolves the target label from `.agentv/targets.yaml` or `targets.yaml` and uses its default provider, model, hooks, and provider settings. The object form above starts from `local-openai`, then applies the eval-local fields for this eval. If `extends` is omitted, the object defines the full target inline 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.
143+
`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.
123144

124145
Use `default_test.threshold` for the inherited per-test pass cutoff. `default_test` can also point at a shared file, matching promptfoo's external defaults pattern:
125146

@@ -150,7 +171,7 @@ agentv results compare .agentv/results/<baseline-run-id>/index.jsonl .agentv/res
150171

151172
## Results
152173

153-
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 `targets.yaml`; both are recorded as metadata, not path segments. The root `index.jsonl` manifest 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.
174+
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 root `index.jsonl` manifest 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.
154175

155176
```bash
156177
agentv eval evals/my-eval.eval.yaml

apps/cli/package.json

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,9 @@
2828
"test:watch": "bun test --watch"
2929
},
3030
"dependencies": {
31-
"@anthropic-ai/claude-agent-sdk": "^0.2.49",
32-
"@github/copilot-sdk": "^1.0.3",
3331
"@hono/node-server": "^1.19.11",
3432
"@inquirer/prompts": "^8.2.1",
3533
"@earendil-works/pi-ai": "^0.74.0",
36-
"@openai/codex-sdk": "^0.136.0",
3734
"cmd-ts": "^0.14.3",
3835
"dotenv": "^16.4.5",
3936
"fast-glob": "^3.3.3",
@@ -44,9 +41,21 @@
4441
"yaml": "^2.8.3"
4542
},
4643
"peerDependencies": {
44+
"@anthropic-ai/claude-agent-sdk": "^0.2.88",
45+
"@github/copilot-sdk": "^1.0.3",
46+
"@openai/codex-sdk": "^0.136.0",
4747
"@earendil-works/pi-coding-agent": "^0.74.0"
4848
},
4949
"peerDependenciesMeta": {
50+
"@anthropic-ai/claude-agent-sdk": {
51+
"optional": true
52+
},
53+
"@github/copilot-sdk": {
54+
"optional": true
55+
},
56+
"@openai/codex-sdk": {
57+
"optional": true
58+
},
5059
"@earendil-works/pi-coding-agent": {
5160
"optional": true
5261
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -636,7 +636,7 @@ function normalizeOptions(
636636
yamlExecution?: ExecutionDefaults,
637637
): NormalizedOptions {
638638
const cliWorkers = normalizeOptionalNumber(rawOptions.workers);
639-
const configWorkers = config?.execution?.workers ?? yamlExecution?.workers;
639+
const configWorkers = config?.execution?.maxConcurrency ?? yamlExecution?.max_concurrency;
640640
const workers = cliWorkers ?? configWorkers ?? 0;
641641

642642
const cliOutputDir = normalizeString(rawOptions.output);

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export interface EvaluationSummary {
2222
readonly passedCount: number;
2323
readonly byFailureStage: Readonly<Record<string, number>>;
2424
readonly byFailureReason: Readonly<Record<string, number>>;
25+
readonly byTargetErrorKind: Readonly<Record<string, number>>;
2526
}
2627

2728
const HISTOGRAM_BREAKPOINTS = [0, 0.2, 0.4, 0.6, 0.8, 1];
@@ -112,6 +113,7 @@ export function calculateEvaluationSummary(
112113
passedCount: 0,
113114
byFailureStage: {},
114115
byFailureReason: {},
116+
byTargetErrorKind: {},
115117
};
116118
}
117119

@@ -150,6 +152,7 @@ export function calculateEvaluationSummary(
150152
// Aggregate by failure stage and reason (execution errors only)
151153
const byFailureStage: Record<string, number> = {};
152154
const byFailureReason: Record<string, number> = {};
155+
const byTargetErrorKind: Record<string, number> = {};
153156
for (const result of executionErrors) {
154157
if (result.failureStage) {
155158
byFailureStage[result.failureStage] = (byFailureStage[result.failureStage] ?? 0) + 1;
@@ -158,6 +161,10 @@ export function calculateEvaluationSummary(
158161
byFailureReason[result.failureReasonCode] =
159162
(byFailureReason[result.failureReasonCode] ?? 0) + 1;
160163
}
164+
const targetErrorKind = result.targetExecution?.errorKind;
165+
if (targetErrorKind) {
166+
byTargetErrorKind[targetErrorKind] = (byTargetErrorKind[targetErrorKind] ?? 0) + 1;
167+
}
161168
}
162169

163170
return {
@@ -177,6 +184,7 @@ export function calculateEvaluationSummary(
177184
passedCount,
178185
byFailureStage,
179186
byFailureReason,
187+
byTargetErrorKind,
180188
};
181189
}
182190

@@ -297,6 +305,14 @@ export function formatEvaluationSummary(
297305
}
298306
}
299307

308+
const targetErrorEntries = Object.entries(summary.byTargetErrorKind);
309+
if (targetErrorEntries.length > 0) {
310+
lines.push('\nTarget runtime errors by kind:');
311+
for (const [kind, count] of targetErrorEntries) {
312+
lines.push(` ${kind}: ${count}`);
313+
}
314+
}
315+
300316
return lines.join('\n');
301317
}
302318

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ targets:
4848
- label: cli-target
4949
provider: mock
5050
- label: codex-target
51-
provider: codex
51+
provider: codex-sdk
5252
model: gpt-5-default
5353
`;
5454
await writeFile(targetsPath, targetsContent, 'utf8');

apps/dashboard/src/components/ResultTable.tsx

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -615,6 +615,26 @@ function primaryTrialArtifactPath(trial: EvalCaseTrial): string | null {
615615
);
616616
}
617617

618+
function formatTargetError(
619+
error: string | undefined,
620+
kind: string | undefined,
621+
): string | undefined {
622+
if (!error) return kind ? `Target error: ${kind}` : undefined;
623+
return kind ? `[target:${kind}] ${error}` : error;
624+
}
625+
626+
function targetErrorKind(value: {
627+
targetExecution?: { error_kind?: string; errorKind?: string };
628+
target_execution?: { error_kind?: string; errorKind?: string };
629+
}): string | undefined {
630+
return (
631+
value.target_execution?.error_kind ??
632+
value.target_execution?.errorKind ??
633+
value.targetExecution?.error_kind ??
634+
value.targetExecution?.errorKind
635+
);
636+
}
637+
618638
function TrialResultCell({
619639
column,
620640
row,
@@ -656,7 +676,12 @@ function TrialResultCell({
656676
case 'review':
657677
return <span className="text-gray-700">-</span>;
658678
case 'error':
659-
return <TruncatedMuted value={trial.error} tone="text-red-300" />;
679+
return (
680+
<TruncatedMuted
681+
value={formatTargetError(trial.error, targetErrorKind(trial))}
682+
tone="text-red-300"
683+
/>
684+
);
660685
default:
661686
return <span className="text-gray-700">-</span>;
662687
}
@@ -840,7 +865,12 @@ function ResultCell({
840865
case 'cost_tokens':
841866
return <CostTokenCell row={row} repeatGroup={repeatGroup} />;
842867
case 'error':
843-
return <TruncatedMuted value={row.result.error} tone="text-red-300" />;
868+
return (
869+
<TruncatedMuted
870+
value={formatTargetError(row.result.error, targetErrorKind(row.result))}
871+
tone="text-red-300"
872+
/>
873+
);
844874
default:
845875
return <span className="text-gray-600">-</span>;
846876
}

apps/dashboard/src/lib/types.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,41 @@ export interface TokenUsage {
7070
cached?: number;
7171
}
7272

73+
export interface TargetExecutionEnvelope {
74+
schema_version?: 'agentv.target_execution.v1';
75+
status?: 'success' | 'error' | string;
76+
target_id?: string;
77+
provider_id?: string;
78+
provider_kind?: string;
79+
runtime_mode?: string;
80+
runtimeMode?: string;
81+
error_kind?: string;
82+
errorKind?: string;
83+
message?: string;
84+
exit_code?: number | null;
85+
exitCode?: number | null;
86+
signal?: string | null;
87+
duration_ms?: number;
88+
durationMs?: number;
89+
command?: {
90+
argv?: string[];
91+
command_line?: string;
92+
cwd?: string;
93+
};
94+
artifacts?: {
95+
target_execution_path?: string;
96+
stdout_path?: string;
97+
stderr_path?: string;
98+
transcript_path?: string;
99+
transcript_raw_path?: string;
100+
summary_path?: string;
101+
metrics_path?: string;
102+
file_changes_path?: string;
103+
output_path?: string;
104+
answer_path?: string;
105+
};
106+
}
107+
73108
export interface ScoreEntry {
74109
name?: string;
75110
type?: string;
@@ -100,6 +135,11 @@ export interface EvalCaseTrial {
100135
scores?: ScoreEntry[];
101136
assertions?: AssertionEntry[];
102137
error?: string;
138+
targetExecution?: TargetExecutionEnvelope;
139+
target_execution?: TargetExecutionEnvelope;
140+
target_execution_path?: string;
141+
stdout_path?: string;
142+
stderr_path?: string;
103143
execution_status?: string;
104144
cost_usd?: number;
105145
total_tokens?: number;
@@ -228,6 +268,11 @@ export interface EvalResult {
228268
score: number;
229269
executionStatus?: string;
230270
error?: string;
271+
targetExecution?: TargetExecutionEnvelope;
272+
target_execution?: TargetExecutionEnvelope;
273+
target_execution_path?: string;
274+
stdout_path?: string;
275+
stderr_path?: string;
231276
costUsd?: number;
232277
durationMs?: number;
233278
tokenUsage?: TokenUsage;

0 commit comments

Comments
 (0)