@fede0089/skill-eval 1.3.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -18
- package/dist/commands/functional.js +58 -52
- package/dist/commands/trigger.js +1 -0
- package/dist/index.js +15 -11
- package/dist/reporters/html-reporter.js +8 -8
- package/dist/utils/table-renderer.js +13 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -6,11 +6,11 @@ A CLI tool for evaluating Agent Skills locally. Tests whether your skill trigger
|
|
|
6
6
|
|
|
7
7
|
Skills are instructions that change how the agent behaves. But a single successful run isn't enough to trust one — agents are non-deterministic, and a good isolated result can be exactly that: an isolated case.
|
|
8
8
|
|
|
9
|
-
Skill evals let you turn that intuition into evidence: run several comparable tasks with
|
|
9
|
+
Skill evals let you turn that intuition into evidence: run several comparable tasks with the skill, measure them against the same criteria, and optionally compare against a baseline or historical skill branches.
|
|
10
10
|
|
|
11
11
|
## How it works
|
|
12
12
|
|
|
13
|
-
For each eval prompt, skill-eval spins up parallel agent processes
|
|
13
|
+
For each eval prompt, skill-eval spins up parallel agent processes with the current skill installed by default. You can optionally add the no-skill baseline or historical skill branches for side-by-side comparison. Each agent runs headlessly and produces a transcript. An LLM judge then grades each transcript against your expectations. Results are aggregated into **pass@k** metrics, giving you a clear view of how the skill behaves in isolation or relative to comparison targets.
|
|
14
14
|
|
|
15
15
|
```
|
|
16
16
|
eval prompt
|
|
@@ -20,7 +20,7 @@ For each eval prompt, skill-eval spins up parallel agent processes — some with
|
|
|
20
20
|
└───────┬───────┘
|
|
21
21
|
│
|
|
22
22
|
┌───────────┴───────────┐
|
|
23
|
-
─ with skill ─
|
|
23
|
+
─ with skill ─ ─ baseline (opt) ─
|
|
24
24
|
┌──────┴──────┐ ┌─────┴──────┐
|
|
25
25
|
agent 1 agent 2 agent 3 agent 4
|
|
26
26
|
│ │ │ │
|
|
@@ -32,6 +32,8 @@ For each eval prompt, skill-eval spins up parallel agent processes — some with
|
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
> The `trigger` command only runs with-skill trials and checks whether the skill dispatch tool was actually invoked — no judge or baseline needed.
|
|
35
|
+
>
|
|
36
|
+
> The baseline branch is opt-in: enable it with `--compare-baseline` (no-skill control) or `--compare-ref <ref>` (historical skill versions).
|
|
35
37
|
|
|
36
38
|
## Installation
|
|
37
39
|
|
|
@@ -66,7 +68,7 @@ npm link # makes `skill-eval` available globally
|
|
|
66
68
|
# Checks that the skill is triggered (invoked) for each prompt
|
|
67
69
|
skill-eval trigger --workspace <path> --skill <path> [options] [agent]
|
|
68
70
|
|
|
69
|
-
# Checks that the skill produces correct output
|
|
71
|
+
# Checks that the skill produces correct output (skill-only by default)
|
|
70
72
|
skill-eval functional --workspace <path> --skill <path> [options] [agent]
|
|
71
73
|
```
|
|
72
74
|
|
|
@@ -81,6 +83,7 @@ skill-eval functional --workspace <path> --skill <path> [options] [agent]
|
|
|
81
83
|
| `--timeout <seconds>` | no | none | Kill the agent after this many seconds |
|
|
82
84
|
| `--eval-id <id>` | no | all | Run only the eval with this numeric ID |
|
|
83
85
|
| `--compare-ref [refs...]` | no | — | Git references to compare against |
|
|
86
|
+
| `--compare-baseline` | no | `false` | Also run the no-skill baseline alongside the skill |
|
|
84
87
|
| `-v, --debug` | no | `false` | Enable verbose debug logging |
|
|
85
88
|
| `[agent]` | no | `gemini-cli` | Agent backend to use |
|
|
86
89
|
|
|
@@ -156,6 +159,7 @@ Refer to your runner's documentation for the full list of available settings and
|
|
|
156
159
|
This repo includes a `mock-skill/` directory — a complete, working example of a license-generator skill with trigger and functional evals. Run it directly with:
|
|
157
160
|
|
|
158
161
|
```sh
|
|
162
|
+
npm run test:unit # run the unit test suite
|
|
159
163
|
npm run test:trigger # trigger evaluation against mock-skill
|
|
160
164
|
npm run test:functional # functional evaluation against mock-skill
|
|
161
165
|
```
|
|
@@ -180,17 +184,4 @@ The factory, preflight check, and CLI all pick it up automatically.
|
|
|
180
184
|
### Adding a new report format
|
|
181
185
|
|
|
182
186
|
1. Create `src/reporters/<format>-reporter.ts` implementing `Reporter`.
|
|
183
|
-
2.
|
|
184
|
-
3. Add the format string to `ReportFormat` in `src/types/index.ts`.
|
|
185
|
-
er, binary: '<cli-binary-name>' },
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
The factory, preflight check, and CLI all pick it up automatically.
|
|
189
|
-
|
|
190
|
-
> Implement `applyRunnerConfig(evalConfigBaseDir, worktreePath)` to copy `evalConfigBaseDir/<your-agent>/` into the appropriate config directory in the worktree (e.g. `.claude/` for a Claude runner). No-op silently if the directory doesn't exist.
|
|
191
|
-
|
|
192
|
-
### Adding a new report format
|
|
193
|
-
|
|
194
|
-
1. Create `src/reporters/<format>-reporter.ts` implementing `Reporter`.
|
|
195
|
-
2. Export it and add a case in `createReporter()` in `src/reporters/index.ts`.
|
|
196
|
-
3. Add the format string to `ReportFormat` in `src/types/index.ts`.
|
|
187
|
+
2. Add a case for it in `createReporter()` in `src/reporters/index.ts`.
|
|
@@ -13,7 +13,7 @@ import { renderFunctionalTable, renderRunHeader } from '../utils/table-renderer.
|
|
|
13
13
|
import { JsonReporter } from '../reporters/index.js';
|
|
14
14
|
import chalk from 'chalk';
|
|
15
15
|
import { git } from '../utils/git.js';
|
|
16
|
-
export async function functionalCommand(agent, workspace, skillPath, maxAgents = 4, injectedSuite, numTrials = 3, reporter = new JsonReporter(), timeoutMs, evalId, compareRefs = []) {
|
|
16
|
+
export async function functionalCommand(agent, workspace, skillPath, maxAgents = 4, injectedSuite, numTrials = 3, reporter = new JsonReporter(), timeoutMs, evalId, compareRefs = [], compareBaseline = false) {
|
|
17
17
|
if (!injectedSuite)
|
|
18
18
|
preflight(agent, workspace, skillPath);
|
|
19
19
|
const suite = injectedSuite || evalLoader.loadEvalSuite(skillPath);
|
|
@@ -63,10 +63,10 @@ export async function functionalCommand(agent, workspace, skillPath, maxAgents =
|
|
|
63
63
|
}));
|
|
64
64
|
}
|
|
65
65
|
// 3. Baseline Runner
|
|
66
|
-
const withoutSkillRunner = new EvalRunner({
|
|
66
|
+
const withoutSkillRunner = compareBaseline ? new EvalRunner({
|
|
67
67
|
agent, workspace, skillPath, skillName: skill_name, runDir, isBaseline: true, debug, timeoutMs,
|
|
68
68
|
variant: 'baseline'
|
|
69
|
-
});
|
|
69
|
+
}) : undefined;
|
|
70
70
|
const taskResults = [];
|
|
71
71
|
let withSkillTasksAllPassedCount = 0;
|
|
72
72
|
let baselineTasksAllPassedCount = 0;
|
|
@@ -76,10 +76,12 @@ export async function functionalCommand(agent, workspace, skillPath, maxAgents =
|
|
|
76
76
|
let barrier = Promise.resolve();
|
|
77
77
|
// Subtask labels
|
|
78
78
|
const skillVersions = Array.from(variantRunners.keys());
|
|
79
|
-
const subtaskLabels =
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
79
|
+
const subtaskLabels = compareBaseline
|
|
80
|
+
? [
|
|
81
|
+
...Array.from({ length: numTrials }, (_, i) => `Without Skill ${i + 1}`),
|
|
82
|
+
...skillVersions.flatMap(v => Array.from({ length: numTrials }, (_, i) => `${v} ${i + 1}`))
|
|
83
|
+
]
|
|
84
|
+
: skillVersions.flatMap(v => Array.from({ length: numTrials }, (_, i) => `${v} ${i + 1}`));
|
|
83
85
|
try {
|
|
84
86
|
renderRunHeader({ command: 'functional', skillName: skill_name, agent, workspace, tasks: tasks.length, trials: numTrials, maxAgents, timeoutMs, runDir, evalId });
|
|
85
87
|
Logger.write(`──────────────────────────────────────────────────\n`);
|
|
@@ -98,34 +100,36 @@ export async function functionalCommand(agent, workspace, skillPath, maxAgents =
|
|
|
98
100
|
await thisBarrier;
|
|
99
101
|
// 1. Without Skill trials
|
|
100
102
|
const baselineTrialPromises = [];
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
103
|
+
if (withoutSkillRunner) {
|
|
104
|
+
for (let idx = 0; idx < numTrials; idx++) {
|
|
105
|
+
const trialId = idx + 1;
|
|
106
|
+
const trialCtx = multi?.getTrialCtx(trialId) ?? uiCtx;
|
|
107
|
+
const release = await pool.acquire();
|
|
108
|
+
const p = withRetry((attempt) => withoutSkillRunner.runFunctionalTask(task, i, trialId, trialCtx, attempt)
|
|
109
|
+
.catch((error) => ({
|
|
110
|
+
id: trialId,
|
|
111
|
+
transcript: { error: error instanceof Error ? error.message : String(error) },
|
|
112
|
+
assertionResults: [{ assertion: 'Without Skill Execution', passed: false, reason: String(error) }],
|
|
113
|
+
trialPassed: false,
|
|
114
|
+
isError: true
|
|
115
|
+
})), 2, 1000, (nextAttempt, lastTrial) => {
|
|
116
|
+
const reason = lastTrial.assertionResults[0]?.reason ?? 'infrastructure error';
|
|
117
|
+
trialCtx.updateLog(`Retry ${nextAttempt}/2 — ${reason.substring(0, 50)}`);
|
|
118
|
+
}).then(trial => {
|
|
119
|
+
if (multi) {
|
|
120
|
+
const reason = trial.assertionResults.find(r => !r.passed)?.reason;
|
|
121
|
+
const passedCount = trial.assertionResults.filter(r => r.passed).length;
|
|
122
|
+
const totalCount = trial.assertionResults.length;
|
|
123
|
+
multi.markTrialComplete(trialId, trial.trialPassed, reason, trial.isError, passedCount, totalCount);
|
|
124
|
+
}
|
|
125
|
+
return trial;
|
|
126
|
+
}).finally(release);
|
|
127
|
+
baselineTrialPromises.push(p);
|
|
128
|
+
}
|
|
125
129
|
}
|
|
126
130
|
// 2. Skill variants trials
|
|
127
131
|
const variantTrialsPromises = {};
|
|
128
|
-
let currentSubtaskIdx = numTrials;
|
|
132
|
+
let currentSubtaskIdx = compareBaseline ? numTrials : 0;
|
|
129
133
|
for (const [version, runner] of variantRunners.entries()) {
|
|
130
134
|
variantTrialsPromises[version] = [];
|
|
131
135
|
for (let idx = 0; idx < numTrials; idx++) {
|
|
@@ -158,19 +162,17 @@ export async function functionalCommand(agent, workspace, skillPath, maxAgents =
|
|
|
158
162
|
}
|
|
159
163
|
// All slots for this prompt have been acquired — signal the next prompt.
|
|
160
164
|
resolveBarrier();
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
...Object.values(variantTrialsPromises).map(ps => Promise.all(ps))
|
|
164
|
-
]);
|
|
165
|
+
const variantsTrials = await Promise.all(Object.values(variantTrialsPromises).map(ps => Promise.all(ps)));
|
|
166
|
+
const woTrials = withoutSkillRunner ? await Promise.all(baselineTrialPromises) : [];
|
|
165
167
|
const woPassedCount = woTrials.filter(t => t.trialPassed).length;
|
|
166
|
-
if (woPassedCount === woTrials.length)
|
|
168
|
+
if (withoutSkillRunner && woPassedCount === woTrials.length)
|
|
167
169
|
baselineTasksAllPassedCount++;
|
|
168
170
|
const taskSkillTrials = {};
|
|
169
171
|
const variantNames = Object.keys(variantTrialsPromises);
|
|
170
172
|
let localAllPassed = true;
|
|
171
173
|
for (let vIdx = 0; vIdx < variantNames.length; vIdx++) {
|
|
172
174
|
const vName = variantNames[vIdx];
|
|
173
|
-
const vTrials =
|
|
175
|
+
const vTrials = variantsTrials[vIdx];
|
|
174
176
|
taskSkillTrials[vName] = vTrials.map(t => ({ ...t, transcript: undefined }));
|
|
175
177
|
if (vName === 'local') {
|
|
176
178
|
const passedCount = vTrials.filter(t => t.trialPassed).length;
|
|
@@ -185,7 +187,7 @@ export async function functionalCommand(agent, workspace, skillPath, maxAgents =
|
|
|
185
187
|
const taskResult = {
|
|
186
188
|
taskId: task.id,
|
|
187
189
|
prompt: task.prompt,
|
|
188
|
-
baselineTrials: woTrials.map(t => ({ ...t, transcript: undefined })),
|
|
190
|
+
baselineTrials: withoutSkillRunner ? woTrials.map(t => ({ ...t, transcript: undefined })) : [],
|
|
189
191
|
skillTrials: taskSkillTrials
|
|
190
192
|
};
|
|
191
193
|
taskResults.push(taskResult);
|
|
@@ -199,25 +201,28 @@ export async function functionalCommand(agent, workspace, skillPath, maxAgents =
|
|
|
199
201
|
await ui.run(tasks.length);
|
|
200
202
|
// ==== REPORTING ====
|
|
201
203
|
const allSkillVersions = taskResults.length > 0 ? Object.keys(taskResults[0].skillTrials) : ['local'];
|
|
202
|
-
const
|
|
204
|
+
const hasBaseline = compareBaseline;
|
|
205
|
+
const allVersions = hasBaseline ? ['baseline', ...allSkillVersions] : allSkillVersions;
|
|
203
206
|
const scores = {};
|
|
204
207
|
const passAtK = {};
|
|
205
208
|
const assertionPassRate = {};
|
|
206
209
|
const tokenStats = {};
|
|
207
210
|
const durationStats = {};
|
|
208
211
|
// 1. Baseline Metrics
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
212
|
+
if (hasBaseline) {
|
|
213
|
+
const { passAtK: woPassAtK } = aggregatePassAtK(taskResults, numTrials, r => r.baselineTrials);
|
|
214
|
+
const woAssertionPassRate = aggregateAssertionPassRate(taskResults, r => r.baselineTrials);
|
|
215
|
+
const woPercentage = Math.round(woAssertionPassRate * 100);
|
|
216
|
+
scores['baseline'] = `${woPercentage}%`;
|
|
217
|
+
passAtK['baseline'] = Math.round(woPassAtK * 1000) / 1000;
|
|
218
|
+
assertionPassRate['baseline'] = Math.round(woAssertionPassRate * 1000) / 1000;
|
|
219
|
+
const woTokens = aggregateTokenStats(taskResults.flatMap(r => r.baselineTrials));
|
|
220
|
+
if (woTokens)
|
|
221
|
+
tokenStats['baseline'] = woTokens;
|
|
222
|
+
const woDuration = aggregateDurationStats(taskResults.flatMap(r => r.baselineTrials));
|
|
223
|
+
if (woDuration)
|
|
224
|
+
durationStats['baseline'] = woDuration;
|
|
225
|
+
}
|
|
221
226
|
// 2. Skill Variants Metrics
|
|
222
227
|
for (const version of allSkillVersions) {
|
|
223
228
|
const { passAtK: wiPassAtK } = aggregatePassAtK(taskResults, numTrials, r => r.skillTrials[version] || []);
|
|
@@ -235,6 +240,7 @@ export async function functionalCommand(agent, workspace, skillPath, maxAgents =
|
|
|
235
240
|
}
|
|
236
241
|
const report = {
|
|
237
242
|
timestamp: startTime.toISOString(),
|
|
243
|
+
command: 'functional',
|
|
238
244
|
skill_name,
|
|
239
245
|
agent,
|
|
240
246
|
metrics: {
|
package/dist/commands/trigger.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -8,7 +8,9 @@ import { HtmlReporter } from './reporters/index.js';
|
|
|
8
8
|
import { DEFAULT_AGENT } from './runners/registry.js';
|
|
9
9
|
import * as path from 'path';
|
|
10
10
|
import * as fs from 'fs';
|
|
11
|
+
import { createRequire } from 'module';
|
|
11
12
|
import { fileURLToPath } from 'url';
|
|
13
|
+
const pkg = createRequire(import.meta.url)('../package.json');
|
|
12
14
|
export const program = new Command();
|
|
13
15
|
const errorHandler = (err) => {
|
|
14
16
|
if (err instanceof AppError) {
|
|
@@ -26,7 +28,7 @@ const errorHandler = (err) => {
|
|
|
26
28
|
program
|
|
27
29
|
.name('skill-eval')
|
|
28
30
|
.description('CLI to evaluate agent skills triggering and functionality')
|
|
29
|
-
.version(
|
|
31
|
+
.version(pkg.version)
|
|
30
32
|
.option('-v, --debug', 'Enable debug logging', false);
|
|
31
33
|
program.on('option:debug', () => {
|
|
32
34
|
process.env.DEBUG = 'true';
|
|
@@ -36,16 +38,16 @@ program
|
|
|
36
38
|
.description('Evaluate triggering of an agent skill')
|
|
37
39
|
.requiredOption('--workspace <path>', 'Path to the workspace/repo to evaluate against')
|
|
38
40
|
.requiredOption('--skill <path>', 'Path to the skill directory')
|
|
39
|
-
.option('--agents <number>', 'Number of parallel agents')
|
|
40
|
-
.option('--trials <number>', 'Number of trials per task for pass@k calculation')
|
|
41
|
+
.option('--agents <number>', 'Number of parallel agents', '4')
|
|
42
|
+
.option('--trials <number>', 'Number of trials per task for pass@k calculation', '3')
|
|
41
43
|
.option('--timeout <seconds>', 'Agent timeout in seconds')
|
|
42
44
|
.option('--eval-id <id>', 'Run only the eval with this ID (numeric)')
|
|
43
45
|
.option('--compare-ref [refs...]', 'Compare against historical git references')
|
|
44
46
|
.action((agent, options) => {
|
|
45
47
|
const workspace = path.resolve(options.workspace);
|
|
46
48
|
const selectedAgent = agent || DEFAULT_AGENT;
|
|
47
|
-
const maxAgents = parseInt(options.agents, 10)
|
|
48
|
-
const numTrials =
|
|
49
|
+
const maxAgents = parseInt(options.agents, 10);
|
|
50
|
+
const numTrials = parseInt(options.trials, 10);
|
|
49
51
|
const timeoutMs = options.timeout ? parseInt(options.timeout, 10) * 1000 : undefined;
|
|
50
52
|
const evalId = options.evalId !== undefined ? parseInt(options.evalId, 10) : undefined;
|
|
51
53
|
const compareRefs = options.compareRef || [];
|
|
@@ -53,23 +55,25 @@ program
|
|
|
53
55
|
});
|
|
54
56
|
program
|
|
55
57
|
.command('functional [agent]')
|
|
56
|
-
.description('Evaluate functional correctness of an agent skill
|
|
58
|
+
.description('Evaluate functional correctness of an agent skill against expectations')
|
|
57
59
|
.requiredOption('--workspace <path>', 'Path to the workspace/repo to evaluate against')
|
|
58
60
|
.requiredOption('--skill <path>', 'Path to the skill directory')
|
|
59
|
-
.option('--agents <number>', 'Number of parallel agents')
|
|
60
|
-
.option('--trials <number>', 'Number of trials per task for pass@k calculation')
|
|
61
|
+
.option('--agents <number>', 'Number of parallel agents', '4')
|
|
62
|
+
.option('--trials <number>', 'Number of trials per task for pass@k calculation', '3')
|
|
61
63
|
.option('--timeout <seconds>', 'Agent timeout in seconds')
|
|
62
64
|
.option('--eval-id <id>', 'Run only the eval with this ID (numeric)')
|
|
63
65
|
.option('--compare-ref [refs...]', 'Compare against historical git references')
|
|
66
|
+
.option('--compare-baseline', 'Also run the no-skill baseline alongside the skill')
|
|
64
67
|
.action((agent, options) => {
|
|
65
68
|
const workspace = path.resolve(options.workspace);
|
|
66
69
|
const selectedAgent = agent || DEFAULT_AGENT;
|
|
67
|
-
const maxAgents = parseInt(options.agents, 10)
|
|
68
|
-
const numTrials =
|
|
70
|
+
const maxAgents = parseInt(options.agents, 10);
|
|
71
|
+
const numTrials = parseInt(options.trials, 10);
|
|
69
72
|
const timeoutMs = options.timeout ? parseInt(options.timeout, 10) * 1000 : undefined;
|
|
70
73
|
const evalId = options.evalId !== undefined ? parseInt(options.evalId, 10) : undefined;
|
|
71
74
|
const compareRefs = options.compareRef || [];
|
|
72
|
-
|
|
75
|
+
const compareBaseline = !!options.compareBaseline;
|
|
76
|
+
functionalCommand(selectedAgent, workspace, options.skill, maxAgents, undefined, numTrials, new HtmlReporter(), timeoutMs, evalId, compareRefs, compareBaseline).catch(errorHandler);
|
|
73
77
|
});
|
|
74
78
|
const isMain = process.argv[1] && (() => {
|
|
75
79
|
try {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { Logger } from '../utils/logger.js';
|
|
4
|
-
import { formatTokens, formatDuration } from '../utils/table-renderer.js';
|
|
4
|
+
import { formatTokens, formatDuration, hasFunctionalBaseline } from '../utils/table-renderer.js';
|
|
5
5
|
import { computeAssertionPassRate } from '../core/statistics.js';
|
|
6
6
|
export class HtmlReporter {
|
|
7
7
|
generate(report, runDir) {
|
|
@@ -32,7 +32,7 @@ function passColorClass(val) {
|
|
|
32
32
|
return 'red';
|
|
33
33
|
}
|
|
34
34
|
function isFunctional(report) {
|
|
35
|
-
return report.
|
|
35
|
+
return report.command === 'functional' || hasFunctionalBaseline(report);
|
|
36
36
|
}
|
|
37
37
|
// ---------------------------------------------------------------------------
|
|
38
38
|
// Metrics Grid
|
|
@@ -50,7 +50,7 @@ function renderMetricsGrid(report) {
|
|
|
50
50
|
const { metrics, results } = report;
|
|
51
51
|
const functional = isFunctional(report);
|
|
52
52
|
const skillVersions = results.length > 0 ? Object.keys(results[0].skillTrials) : ['local'];
|
|
53
|
-
const allVersions = functional ? ['baseline', ...skillVersions] : skillVersions;
|
|
53
|
+
const allVersions = functional && hasFunctionalBaseline(report) ? ['baseline', ...skillVersions] : skillVersions;
|
|
54
54
|
const headerCells = allVersions.map(v => `<th>${v}</th>`).join('');
|
|
55
55
|
const successRows = `<tr>
|
|
56
56
|
<td>Success Rate</td>
|
|
@@ -112,7 +112,9 @@ function renderExpectationCell(trials, expIdx, variant, taskId, clickable, total
|
|
|
112
112
|
const detail = `<tr class="exp-detail-row" id="${escapeHtml(detailId)}"><td colspan="${totalCols}"><div class="exp-detail-inner"><div class="exp-detail-header">Judge per trial · <span class="variant-pill">${escapeHtml(variant)}</span></div>${lines}</div></td></tr>`;
|
|
113
113
|
return { cell, detail };
|
|
114
114
|
}
|
|
115
|
-
function renderExpectationsTable(result,
|
|
115
|
+
function renderExpectationsTable(result, isFunctionalEval) {
|
|
116
|
+
const skillVersions = Object.keys(result.skillTrials);
|
|
117
|
+
const allVersions = isFunctionalEval && (result.baselineTrials?.length ?? 0) > 0 ? ['baseline', ...skillVersions] : skillVersions;
|
|
116
118
|
let canonical = [];
|
|
117
119
|
for (const v of allVersions) {
|
|
118
120
|
const trials = v === 'baseline' ? result.baselineTrials : result.skillTrials[v];
|
|
@@ -159,7 +161,7 @@ function avgTrialDuration(trials) {
|
|
|
159
161
|
}
|
|
160
162
|
function renderTaskMiniGrid(result, isFunctionalEval) {
|
|
161
163
|
const skillVersions = Object.keys(result.skillTrials);
|
|
162
|
-
const allVersions = isFunctionalEval ? ['baseline', ...skillVersions] : skillVersions;
|
|
164
|
+
const allVersions = isFunctionalEval && (result.baselineTrials?.length ?? 0) > 0 ? ['baseline', ...skillVersions] : skillVersions;
|
|
163
165
|
const headerCells = allVersions.map(v => `<th>${v}</th>`).join('');
|
|
164
166
|
const successRows = `<tr>
|
|
165
167
|
<td>Success Rate</td>
|
|
@@ -199,11 +201,9 @@ function renderTaskMiniGrid(result, isFunctionalEval) {
|
|
|
199
201
|
</div>`;
|
|
200
202
|
}
|
|
201
203
|
function renderTaskDetails(result, isFunctionalEval) {
|
|
202
|
-
const skillVersions = Object.keys(result.skillTrials);
|
|
203
|
-
const allVersions = isFunctionalEval ? ['baseline', ...skillVersions] : skillVersions;
|
|
204
204
|
const sections = [];
|
|
205
205
|
sections.push(renderTaskMiniGrid(result, isFunctionalEval));
|
|
206
|
-
sections.push(renderExpectationsTable(result,
|
|
206
|
+
sections.push(renderExpectationsTable(result, isFunctionalEval));
|
|
207
207
|
return `<div class="task-details" id="details-${result.taskId}">${sections.join('')}</div>`;
|
|
208
208
|
}
|
|
209
209
|
// ---------------------------------------------------------------------------
|
|
@@ -51,6 +51,12 @@ export function formatTokens(n) {
|
|
|
51
51
|
return `${Math.round(n / 1_000)}K`;
|
|
52
52
|
return `${n}`;
|
|
53
53
|
}
|
|
54
|
+
export function hasFunctionalBaseline(report) {
|
|
55
|
+
if (report.results.some(result => (result.baselineTrials?.length ?? 0) > 0)) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
return report.metrics.passAtK['baseline'] !== undefined || report.metrics.assertionPassRate['baseline'] !== undefined;
|
|
59
|
+
}
|
|
54
60
|
function formatTokenStatsLine(stats) {
|
|
55
61
|
const total = formatTokens(stats.avgTotal);
|
|
56
62
|
const input = formatTokens(stats.avgInput);
|
|
@@ -189,7 +195,8 @@ export function renderFunctionalTable(report) {
|
|
|
189
195
|
const { results, metrics } = report;
|
|
190
196
|
// Identify all versions present (baseline + skill versions)
|
|
191
197
|
const skillVersions = results.length > 0 ? Object.keys(results[0].skillTrials) : ['local'];
|
|
192
|
-
const
|
|
198
|
+
const hasBaseline = hasFunctionalBaseline(report);
|
|
199
|
+
const allVersions = hasBaseline ? ['baseline', ...skillVersions] : skillVersions;
|
|
193
200
|
const header = ['ID', 'Prompt'];
|
|
194
201
|
for (const version of allVersions) {
|
|
195
202
|
header.push(version);
|
|
@@ -201,9 +208,11 @@ export function renderFunctionalTable(report) {
|
|
|
201
208
|
const row = [result.taskId.toString(), promptSnippet];
|
|
202
209
|
// Baseline
|
|
203
210
|
const woTrials = result.baselineTrials || [];
|
|
204
|
-
if (
|
|
205
|
-
|
|
206
|
-
|
|
211
|
+
if (hasBaseline) {
|
|
212
|
+
if (woTrials.some(t => t.isError) && !woTrials.every(t => t.isError))
|
|
213
|
+
hasPartialErrors = true;
|
|
214
|
+
row.push(formatAssertionRate(woTrials));
|
|
215
|
+
}
|
|
207
216
|
// Skills
|
|
208
217
|
for (const version of skillVersions) {
|
|
209
218
|
const wiTrials = result.skillTrials[version] || [];
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fede0089/skill-eval",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "CLI to evaluate agent skills triggering",
|
|
3
|
+
"version": "1.4.1",
|
|
4
|
+
"description": "CLI to evaluate agent skills triggering and functionality",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"skill-eval": "dist/index.js"
|