Skip to content

Commit e8ddf96

Browse files
committed
fix(sdk): align script grader results
1 parent bc2f3a3 commit e8ddf96

38 files changed

Lines changed: 795 additions & 344 deletions

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ export default defineAssertion(({ output }) => {
1212
const pass = text.length > 0;
1313
return {
1414
pass,
15-
assertions: [{ text: pass ? 'Output has content' : 'Output is empty', passed: pass }],
15+
score: pass ? 1 : 0,
16+
reason: pass ? 'Output has content' : 'Output is empty',
1617
};
1718
});
1819
`,
@@ -26,7 +27,14 @@ export default defineAssertion(({ output }) => {
2627
return {
2728
pass: score >= 0.5,
2829
score,
29-
assertions: [{ text: 'Output has content', passed: score === 1.0 }],
30+
reason: score === 1.0 ? 'Output has content' : 'Output is empty',
31+
checks: [
32+
{
33+
text: 'Output has content',
34+
pass: score === 1.0,
35+
reason: score === 1.0 ? 'Output is non-empty' : 'Output is empty',
36+
},
37+
],
3038
};
3139
});
3240
`,

apps/cli/test/commands/create/assertion.test.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,11 @@ describe('agentv create assertion', () => {
3535
);
3636
expect(content).toContain("import { defineAssertion } from '@agentv/sdk';");
3737
expect(content).toContain("const text = output ?? '';");
38-
expect(content).toContain(
39-
"assertions: [{ text: pass ? 'Output has content' : 'Output is empty', passed: pass }]",
40-
);
38+
expect(content).toContain('pass,');
39+
expect(content).toContain('score: pass ? 1 : 0,');
40+
expect(content).toContain("reason: pass ? 'Output has content' : 'Output is empty'");
41+
expect(content).not.toContain('assertions:');
42+
expect(content).not.toContain('passed:');
4143
expect(content).not.toContain('reasoning:');
4244
expect(content).not.toContain('getMessageText');
4345
} finally {

apps/cli/test/commands/eval/vitest.test.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,15 +107,23 @@ process.exit(1);
107107
);
108108

109109
const output = JSON.parse(result.stdout);
110+
expect(output.pass).toBe(false);
110111
expect(output.score).toBe(0.5);
111-
expect(output.assertions).toEqual([
112-
{ text: 'welcome banner includes ready status', passed: true },
112+
expect(output.reason).toBe('1/2 Vitest tests passed.');
113+
expect(output.checks).toEqual([
114+
{
115+
text: 'welcome banner includes ready status',
116+
pass: true,
117+
reason: 'Vitest test passed.',
118+
},
113119
{
114120
text: 'welcome banner links to dashboard',
115-
passed: false,
121+
pass: false,
122+
reason: 'Vitest test failed.',
116123
evidence: 'AssertionError: expected link to point at /dashboard',
117124
},
118125
]);
126+
expect(output).not.toHaveProperty('assertions');
119127
expect(output.details).toMatchObject({
120128
vitest_success: false,
121129
num_total_tests: 2,

apps/web/src/content/docs/docs/next/evaluation/sdk.mdx

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,8 @@ export default defineAssertion(({ output }) => {
223223
const pass = wordCount >= 3;
224224
return {
225225
pass,
226-
assertions: [{ text: `Output has ${wordCount} words`, passed: pass }],
226+
score: pass ? 1 : 0,
227+
reason: `Output has ${wordCount} words`,
227228
};
228229
});
229230
```
@@ -241,7 +242,7 @@ export default defineAssertion(({ output, traceSummary }) => {
241242
const isEfficient = (traceSummary?.eventCount ?? 0) <= 10 ? 0.5 : 0;
242243
return {
243244
score: hasContent + isEfficient,
244-
reasoning: 'Checks content exists and is efficient',
245+
reason: 'Checks content exists and is efficient',
245246
};
246247
});
247248
```
@@ -268,16 +269,26 @@ assert:
268269
269270
## Script Graders
270271
271-
Use `defineScriptGrader` from `@agentv/sdk` for full control over scoring with an explicit assertions array:
272+
Use `defineScriptGrader` from `@agentv/sdk` for full control over scoring with optional per-check results:
272273

273274
```typescript
274275
import { defineScriptGrader } from '@agentv/sdk';
275276
276277
export default defineScriptGrader(({ output, traceSummary }) => ({
278+
pass: (output ?? '').length > 0 && (traceSummary?.eventCount ?? 0) <= 5,
277279
score: (output ?? '').length > 0 && (traceSummary?.eventCount ?? 0) <= 5 ? 1.0 : 0.5,
278-
assertions: [
279-
{ text: 'Answer is not empty', passed: (output ?? '').length > 0 },
280-
{ text: 'Efficient tool usage', passed: (traceSummary?.eventCount ?? 0) <= 5 },
280+
reason: 'Checks answer text and tool usage',
281+
checks: [
282+
{
283+
text: 'Answer is not empty',
284+
pass: (output ?? '').length > 0,
285+
reason: (output ?? '').length > 0 ? 'Output is non-empty' : 'Output is empty',
286+
},
287+
{
288+
text: 'Efficient tool usage',
289+
pass: (traceSummary?.eventCount ?? 0) <= 5,
290+
reason: 'Trace event count is within limit',
291+
},
281292
],
282293
}));
283294
```

apps/web/src/content/docs/docs/next/graders/python-helpers.mdx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,19 +36,24 @@ Use canonical fields instead:
3636
## Example
3737

3838
```python
39-
from agentv_py.grader import Assertion, ScriptGraderResult, define_script
39+
from agentv_py.grader import Check, ScriptGraderResult, define_script
4040

4141

4242
def evaluate(context):
4343
actual = context.output or ""
4444
expected = context.expected_output[0]["content"]
4545
passed = actual.strip() == expected.strip()
4646
return ScriptGraderResult(
47+
pass_=passed,
4748
score=1.0 if passed else 0.0,
48-
assertions=[
49-
Assertion(
49+
reason="Candidate output matches expected output"
50+
if passed
51+
else "Candidate output does not match expected output",
52+
checks=[
53+
Check(
5054
text="Candidate output matches expected output",
51-
passed=passed,
55+
pass_=passed,
56+
reason="Exact string comparison passed" if passed else "Exact string comparison failed",
5257
)
5358
],
5459
)

apps/web/src/content/docs/docs/next/graders/script-graders.mdx

Lines changed: 60 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -53,21 +53,25 @@ Emit a JSON object for numeric scores or multi-aspect results:
5353

5454
```json
5555
{
56+
"pass": true,
5657
"score": 1.0,
57-
"assertions": [
58-
{ "text": "Answer contains correct value (42)", "passed": true }
58+
"reason": "Answer contains the correct value.",
59+
"checks": [
60+
{ "text": "Answer contains correct value (42)", "pass": true, "reason": "42 appears in the output." }
5961
]
6062
}
6163
```
6264

6365
| Output Field | Type | Description |
6466
|-------------|------|-------------|
67+
| `pass` | `boolean` | Aggregate pass/fail decision |
6568
| `score` | `number` | 0.0 to 1.0 |
66-
| `assertions` | `Array<{ text, passed, evidence? }>` | Per-aspect results with verdict and optional evidence |
69+
| `reason` | `string` | Explanation for the aggregate decision |
70+
| `checks` | `Array<{ text, pass, score?, reason, evidence? }>` | Optional per-aspect results with verdict, optional score, reason, and evidence |
6771

6872
### Plain-text output (exit-code convention)
6973

70-
For simple pass/fail checks, skip the JSON protocol entirely. The exit code determines the score and stdout becomes the assertion text:
74+
For simple pass/fail checks, skip the JSON protocol entirely. The exit code determines the score and stdout becomes the check text:
7175

7276
| Exit code | Score | Verdict |
7377
|-----------|-------|---------|
@@ -112,37 +116,42 @@ import json, sys
112116
data = json.load(sys.stdin)
113117
output = data.get("output") or ""
114118

115-
assertions = []
119+
checks = []
116120

117121
if "42" in output:
118-
assertions.append({"text": "Output contains correct value (42)", "passed": True})
122+
checks.append({"text": "Output contains correct value (42)", "pass": True, "reason": "42 appears in the output"})
119123
else:
120-
assertions.append({"text": "Output does not contain expected value (42)", "passed": False})
124+
checks.append({"text": "Output contains correct value (42)", "pass": False, "reason": "42 is missing from the output"})
121125

122-
passed = sum(1 for a in assertions if a["passed"])
123-
score = passed / len(assertions) if assertions else 0.0
126+
passed = sum(1 for check in checks if check["pass"])
127+
score = passed / len(checks) if checks else 0.0
124128

125129
print(json.dumps({
130+
"pass": passed == len(checks),
126131
"score": score,
127-
"assertions": assertions,
132+
"reason": f"{passed}/{len(checks)} checks passed",
133+
"checks": checks,
128134
}))
129135
```
130136

131137
The repo-local helper in `examples/features/sdk-python/` wraps the same contract for that example checkout:
132138

133139
```python
134-
from agentv_py.grader import Assertion, ScriptGraderResult, define_script
140+
from agentv_py.grader import Check, ScriptGraderResult, define_script
135141

136142

137143
def evaluate(context):
138144
candidate = context.output or ""
139145
passed = "42" in candidate
140146
return ScriptGraderResult(
147+
pass_=passed,
141148
score=1.0 if passed else 0.0,
142-
assertions=[
143-
Assertion(
149+
reason="Answer contains the correct value" if passed else "Answer is missing the correct value",
150+
checks=[
151+
Check(
144152
text="Output contains correct value (42)",
145-
passed=passed,
153+
pass_=passed,
154+
reason="42 appears in the output" if passed else "42 is missing from the output",
146155
)
147156
],
148157
)
@@ -162,19 +171,21 @@ import { readFileSync } from "fs";
162171
const data = JSON.parse(readFileSync("/dev/stdin", "utf-8"));
163172
const output: string = data.output ?? "";
164173

165-
const assertions: Array<{ text: string; passed: boolean }> = [];
174+
const checks: Array<{ text: string; pass: boolean; reason: string }> = [];
166175

167176
if (output.includes("42")) {
168-
assertions.push({ text: "Output contains correct value (42)", passed: true });
177+
checks.push({ text: "Output contains correct value (42)", pass: true, reason: "42 appears in the output" });
169178
} else {
170-
assertions.push({ text: "Output does not contain expected value (42)", passed: false });
179+
checks.push({ text: "Output contains correct value (42)", pass: false, reason: "42 is missing from the output" });
171180
}
172181

173-
const passed = assertions.filter(a => a.passed).length;
182+
const passed = checks.filter(check => check.pass).length;
174183

175184
console.log(JSON.stringify({
176-
score: passed > 0 ? 1.0 : 0.0,
177-
assertions,
185+
pass: passed === checks.length,
186+
score: checks.length === 0 ? 0.0 : passed / checks.length,
187+
reason: `${passed}/${checks.length} checks passed`,
188+
checks,
178189
}));
179190
```
180191

@@ -197,18 +208,20 @@ import { defineScriptGrader } from '@agentv/sdk';
197208
198209
export default defineScriptGrader(({ output, criteria }) => {
199210
const outputText = output ?? '';
200-
const assertions: Array<{ text: string; passed: boolean }> = [];
211+
const checks: Array<{ text: string; pass: boolean; reason: string }> = [];
201212
202213
if (outputText.includes(criteria)) {
203-
assertions.push({ text: 'Output matches expected outcome', passed: true });
214+
checks.push({ text: 'Output matches expected outcome', pass: true, reason: 'Criteria text appears in the output.' });
204215
} else {
205-
assertions.push({ text: 'Output does not match expected outcome', passed: false });
216+
checks.push({ text: 'Output matches expected outcome', pass: false, reason: 'Criteria text is missing from the output.' });
206217
}
207218
208-
const passed = assertions.filter(a => a.passed).length;
219+
const passed = checks.filter(check => check.pass).length;
209220
return {
210-
score: assertions.length === 0 ? 0 : passed / assertions.length,
211-
assertions,
221+
pass: checks.length > 0 && passed === checks.length,
222+
score: checks.length === 0 ? 0 : passed / checks.length,
223+
reason: `${passed}/${checks.length} checks passed`,
224+
checks,
212225
};
213226
});
214227
```
@@ -240,7 +253,7 @@ describe('welcome banner', () => {
240253
});
241254
```
242255

243-
Then use AgentV's built-in Vitest adapter as the `script` command. The adapter copies verifier files into a temporary workspace-local path when needed, runs Vitest in `workspace_path`, reads the JSON reporter output, and maps each test outcome to an AgentV assertion:
256+
Then use AgentV's built-in Vitest adapter as the `script` command. The adapter copies verifier files into a temporary workspace-local path when needed, runs Vitest in `workspace_path`, reads the JSON reporter output, and maps each test outcome to an AgentV check:
244257

245258
```yaml
246259
assert:
@@ -253,7 +266,7 @@ AgentV infers the Vitest adapter for verifier-looking files such as `*.test.ts`,
253266

254267
### Lower-Level Workspace Helpers
255268

256-
For tiny one-off file checks, `defineWorkspaceGrader` can resolve the workspace path, read files relative to the workspace, build assertions, and aggregate the score:
269+
For tiny one-off file checks, `defineWorkspaceGrader` can resolve the workspace path, read files relative to the workspace, build checks, and aggregate the score:
257270

258271
```typescript
259272
#!/usr/bin/env bun
@@ -269,7 +282,7 @@ export default defineWorkspaceGrader(async ({ workspace }) => [
269282

270283
Prefer Vitest verifiers when the checks naturally fit `expect(...)`. Use `defineWorkspaceGrader` when you need a very small custom script, custom weighting, or details that do not map cleanly to individual test outcomes.
271284

272-
**SDK exports:** `defineScriptGrader`, `defineVitestWorkspaceGrader`, `defineWorkspaceGrader`, `Message`, `ToolCall`, `Trace`, `TraceSummary`, `ScriptGraderInput`, `ScriptGraderResult`, `Workspace`, `WorkspaceAssertion`
285+
**SDK exports:** `defineScriptGrader`, `defineVitestWorkspaceGrader`, `defineWorkspaceGrader`, `Message`, `ToolCall`, `Trace`, `TraceSummary`, `ScriptGraderInput`, `ScriptGraderResult`, `ScriptGraderCheck`, `Workspace`, `WorkspaceCheck`
273286

274287
## Target Access
275288

@@ -303,15 +316,19 @@ export default defineScriptGrader(async ({ input, output }) => {
303316
.join('\n');
304317
const outputText = output ?? '';
305318
const target = createTargetClient();
306-
if (!target) return { score: 0, assertions: [{ text: 'Target not configured', passed: false }] };
319+
if (!target) return { pass: false, score: 0, reason: 'Target not configured' };
307320
308321
const response = await target.invoke({
309322
question: `Is this relevant to: ${inputText}? Response: ${outputText}`,
310323
systemPrompt: 'Respond with JSON: { "relevant": true/false }'
311324
});
312325

313326
const result = JSON.parse(response.rawText ?? '{}');
314-
return { score: result.relevant ? 1.0 : 0.0 };
327+
return {
328+
pass: result.relevant === true,
329+
score: result.relevant === true ? 1.0 : 0.0,
330+
reason: result.relevant === true ? 'Response is relevant' : 'Response is not relevant',
331+
};
315332
});
316333
```
317334

@@ -392,30 +409,32 @@ import { execFileSync } from "child_process";
392409
const input = JSON.parse(readFileSync("/dev/stdin", "utf-8"));
393410
const cwd = input.workspace_path;
394411

395-
const assertions: Array<{ text: string; passed: boolean }> = [];
412+
const checks: Array<{ text: string; pass: boolean; reason: string }> = [];
396413

397414
// Stage 1: Install dependencies
398415
try {
399416
execFileSync("npm", ["install"], { cwd, stdio: "pipe" });
400-
assertions.push({ text: "npm install passed", passed: true });
401-
} catch { assertions.push({ text: "npm install failed", passed: false }); }
417+
checks.push({ text: "npm install", pass: true, reason: "npm install passed" });
418+
} catch { checks.push({ text: "npm install", pass: false, reason: "npm install failed" }); }
402419

403420
// Stage 2: Typecheck
404421
try {
405422
execFileSync("npx", ["tsc", "--noEmit"], { cwd, stdio: "pipe" });
406-
assertions.push({ text: "typecheck passed", passed: true });
407-
} catch { assertions.push({ text: "typecheck failed", passed: false }); }
423+
checks.push({ text: "typecheck", pass: true, reason: "typecheck passed" });
424+
} catch { checks.push({ text: "typecheck", pass: false, reason: "typecheck failed" }); }
408425

409426
// Stage 3: Run tests
410427
try {
411428
execFileSync("npm", ["test"], { cwd, stdio: "pipe" });
412-
assertions.push({ text: "tests passed", passed: true });
413-
} catch { assertions.push({ text: "tests failed", passed: false }); }
429+
checks.push({ text: "tests", pass: true, reason: "tests passed" });
430+
} catch { checks.push({ text: "tests", pass: false, reason: "tests failed" }); }
414431

415-
const passed = assertions.filter(a => a.passed).length;
432+
const passed = checks.filter(check => check.pass).length;
416433
console.log(JSON.stringify({
417-
score: assertions.length > 0 ? passed / assertions.length : 0,
418-
assertions,
434+
pass: checks.length > 0 && passed === checks.length,
435+
score: checks.length > 0 ? passed / checks.length : 0,
436+
reason: `${passed}/${checks.length} checks passed`,
437+
checks,
419438
}));
420439
```
421440

examples/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,8 @@ Then write type-safe script graders:
118118
import { defineScriptGrader } from '@agentv/sdk';
119119

120120
export default defineScriptGrader(({ output }) => ({
121+
pass: (output ?? '').includes('expected'),
121122
score: (output ?? '').includes('expected') ? 1.0 : 0.0,
122-
assert: [{ text: 'Found expected content', passed: (output ?? '').includes('expected') }],
123+
reason: (output ?? '').includes('expected') ? 'Found expected content' : 'Missing expected content',
123124
}));
124125
```

examples/features/script-grader-sdk/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ The `defineScriptGrader` helper:
6161
import { defineScriptGrader } from '@agentv/sdk';
6262

6363
export default defineScriptGrader(({ output, criteria }) => ({
64+
pass: (output ?? '').includes(criteria),
6465
score: (output ?? '').includes(criteria) ? 1.0 : 0.0,
65-
assertions: [{ text: 'Check passed', passed: (output ?? '').includes(criteria) }],
66+
reason: (output ?? '').includes(criteria) ? 'Check passed' : 'Check failed',
6667
}));
6768
```

0 commit comments

Comments
 (0)