Skip to content

Commit 0f1e2fe

Browse files
committed
fix(providers): emit skill call metadata
1 parent 9379736 commit 0f1e2fe

25 files changed

Lines changed: 353 additions & 629 deletions

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,8 +143,8 @@ assert:
143143
prompt: ./prompts/pass-fail.md
144144
```
145145

146-
Each `target:` value must match a grader `id` from `.agentv/config.yaml`, an
147-
eval-local `graders` entry, or a directly referenced `graders: file://...` field.
146+
Each `target:` value must match a target `id` from `.agentv/config.yaml` or
147+
from an eval-local `targets` entry.
148148

149149
### TypeScript Template
150150

packages/core/src/evaluation/graders/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ export type { LlmGraderOptions } from './llm-grader.js';
5454

5555
export { formatToolCalls } from './format-tool-calls.js';
5656

57-
export { SkillTriggerGrader } from './skill-trigger.js';
5857
export { SkillUsedGrader } from './skill-used.js';
5958

6059
export { assembleLlmGraderPrompt } from './llm-grader-prompt.js';

packages/core/src/evaluation/graders/skill-trigger.ts

Lines changed: 0 additions & 109 deletions
This file was deleted.

packages/core/src/evaluation/loaders/eval-yaml-transpiler.ts

Lines changed: 52 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,11 @@ function assertionToNaturalLanguage(entry: RawAssertEntry): string | null {
114114

115115
switch (type) {
116116
case 'skill-trigger':
117-
// Handled separately — not an NL assertion
117+
throw new Error(staleSkillTriggerMessage(entry));
118+
119+
case 'skill-used':
120+
case 'not-skill-used':
121+
// Handled separately as Agent Skills trigger labels.
118122
return null;
119123

120124
case 'llm-rubric':
@@ -249,12 +253,46 @@ function assertionToNaturalLanguageList(entry: RawAssertEntry): string[] {
249253
return nl !== null ? [nl] : [];
250254
}
251255

252-
/**
253-
* Extract skill-trigger entries from an assertion list.
254-
* Returns entries with type === 'skill-trigger'.
255-
*/
256-
function extractTriggerAssertions(assertions: RawAssertEntry[]): RawAssertEntry[] {
257-
return assertions.filter((a) => a.type === 'skill-trigger');
256+
function staleSkillTriggerMessage(entry: RawAssertEntry): string {
257+
const skill = typeof entry.skill === 'string' ? entry.skill.trim() : '';
258+
const shouldTrigger = entry.should_trigger !== false;
259+
if (!skill) {
260+
return "Authored assertion type 'skill-trigger' has been removed. Use 'skill-used' with value: <skill> for expected skill use, or 'not-skill-used' with value: <skill> when the skill must not be used.";
261+
}
262+
const replacementType = shouldTrigger ? 'skill-used' : 'not-skill-used';
263+
return `Authored assertion type 'skill-trigger' has been removed. Replace skill: ${skill} with type: ${replacementType}, value: ${skill}.`;
264+
}
265+
266+
interface SkillUseAssertion {
267+
readonly skill: string;
268+
readonly shouldTrigger: boolean;
269+
}
270+
271+
function skillNameFromValue(value: unknown): string | undefined {
272+
if (typeof value === 'string' && value.trim()) {
273+
return value.trim();
274+
}
275+
if (value && typeof value === 'object' && !Array.isArray(value)) {
276+
const name = (value as Record<string, unknown>).name;
277+
return typeof name === 'string' && name.trim() ? name.trim() : undefined;
278+
}
279+
return undefined;
280+
}
281+
282+
function extractSkillUseAssertions(assertions: RawAssertEntry[]): SkillUseAssertion[] {
283+
return assertions.flatMap((entry) => {
284+
if (entry.type === 'skill-trigger') {
285+
throw new Error(staleSkillTriggerMessage(entry));
286+
}
287+
if (entry.type !== 'skill-used' && entry.type !== 'not-skill-used') {
288+
return [];
289+
}
290+
const skill = skillNameFromValue(entry.value);
291+
if (!skill) {
292+
return [];
293+
}
294+
return [{ skill, shouldTrigger: entry.type === 'skill-used' }];
295+
});
258296
}
259297

260298
// ---------------------------------------------------------------------------
@@ -363,9 +401,7 @@ export function transpileEvalYaml(suite: unknown, source = 'EVAL.yaml'): Transpi
363401
const suiteAssertions = rawSuite.assert ?? [];
364402

365403
// Suite-level NL assertions (appended to every test)
366-
const suiteNlAssertions: string[] = suiteAssertions
367-
.filter((a) => a.type !== 'skill-trigger')
368-
.flatMap(assertionToNaturalLanguageList);
404+
const suiteNlAssertions: string[] = suiteAssertions.flatMap(assertionToNaturalLanguageList);
369405

370406
/**
371407
* Helper: get or create the EvalsJsonFile for a skill.
@@ -394,7 +430,7 @@ export function transpileEvalYaml(suite: unknown, source = 'EVAL.yaml'): Transpi
394430
);
395431
}
396432

397-
// Collect NL assertions (not skill-trigger)
433+
// Collect NL assertions (not skill-use assertions)
398434
const nlAssertions: string[] = [];
399435

400436
// Prepend test-level criteria as NL assertion
@@ -403,15 +439,15 @@ export function transpileEvalYaml(suite: unknown, source = 'EVAL.yaml'): Transpi
403439
}
404440

405441
for (const entry of caseAssertions) {
406-
if (entry.type !== 'skill-trigger') {
442+
if (entry.type !== 'skill-used' && entry.type !== 'not-skill-used') {
407443
nlAssertions.push(...assertionToNaturalLanguageList(entry));
408444
}
409445
}
410446

411447
// Append suite-level NL assertions
412448
nlAssertions.push(...suiteNlAssertions);
413449

414-
const triggerJudges = extractTriggerAssertions(caseAssertions);
450+
const triggerJudges = extractSkillUseAssertions(caseAssertions);
415451
const { prompt, files: inputFiles } = extractInput(rawCase);
416452
const expectedOutput = extractExpectedOutput(rawCase.expected_output);
417453

@@ -428,7 +464,7 @@ export function transpileEvalYaml(suite: unknown, source = 'EVAL.yaml'): Transpi
428464
};
429465

430466
if (triggerJudges.length === 0) {
431-
// No skill-trigger: place in dominant skill (or _no-skill)
467+
// No skill-use assertion: place in dominant skill (or _no-skill)
432468
// Determine dominant skill by scanning all tests (first occurrence wins)
433469
// We defer this: record with a sentinel and resolve after all tests are processed.
434470
// For now, push to _no-skill; we'll re-assign at the end.
@@ -437,10 +473,8 @@ export function transpileEvalYaml(suite: unknown, source = 'EVAL.yaml'): Transpi
437473
} else {
438474
// Place in each skill with the correct should_trigger value
439475
for (const tj of triggerJudges) {
440-
const skillName = typeof tj.skill === 'string' ? tj.skill : '_no-skill';
441-
const shouldTrigger = tj.should_trigger !== false; // default true
442-
const skillFile = getSkillFile(skillName);
443-
skillFile.evals.push({ ...baseCase, should_trigger: shouldTrigger });
476+
const skillFile = getSkillFile(tj.skill);
477+
skillFile.evals.push({ ...baseCase, should_trigger: tj.shouldTrigger });
444478
}
445479
}
446480
}

packages/core/src/evaluation/loaders/grader-parser.ts

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1390,34 +1390,6 @@ async function parseGraderList(
13901390
continue;
13911391
}
13921392

1393-
if (typeValue === 'skill-trigger') {
1394-
const skillName = asString(rawEvaluator.skill);
1395-
if (!skillName) {
1396-
logWarning(`Skipping skill-trigger evaluator '${name}' in '${evalId}': missing skill`);
1397-
continue;
1398-
}
1399-
const rawShouldTrigger = rawEvaluator.should_trigger;
1400-
const shouldTrigger = typeof rawShouldTrigger === 'boolean' ? rawShouldTrigger : undefined;
1401-
const weight = validateWeight(rawEvaluator.weight, name, evalId);
1402-
const { required, min_score } = parseRequiredAndMinScore(
1403-
rawEvaluator.required,
1404-
(rawEvaluator as Record<string, unknown>).min_score as JsonValue | undefined,
1405-
name,
1406-
evalId,
1407-
);
1408-
pushEvaluator({
1409-
name,
1410-
type: 'skill-trigger',
1411-
skill: skillName,
1412-
...(shouldTrigger !== undefined ? { should_trigger: shouldTrigger } : {}),
1413-
...(weight !== undefined ? { weight } : {}),
1414-
...(required !== undefined ? { required } : {}),
1415-
...(min_score !== undefined ? { min_score } : {}),
1416-
...(negate !== undefined ? { negate } : {}),
1417-
});
1418-
continue;
1419-
}
1420-
14211393
if (typeValue === 'javascript' || typeValue === 'python' || typeValue === 'webhook') {
14221394
const value = asString(rawEvaluator.value);
14231395
if (!value || value.trim().length === 0) {
@@ -2073,10 +2045,6 @@ function generateAssertionName(typeValue: string, rawEvaluator: JsonObject): str
20732045
}
20742046
return typeValue;
20752047
}
2076-
case 'skill-trigger': {
2077-
const skillValue = asString(rawEvaluator.skill);
2078-
return skillValue ? `skill-trigger-${skillValue}` : 'skill-trigger';
2079-
}
20802048
case 'contains':
20812049
return value ? `contains-${value}` : 'contains';
20822050
case 'contains-any':

packages/core/src/evaluation/providers/cli.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
type TargetRuntimeConfig,
1414
runDockerSandboxCommand,
1515
} from './sandbox-runner.js';
16+
import { deriveSkillCallMetadataFromMessages } from './skill-calls.js';
1617
import { buildTargetExecutionEnvelope, captureTargetExecutionLog } from './target-execution.js';
1718
import type { CliResolvedConfig } from './targets.js';
1819
import type {
@@ -543,6 +544,7 @@ export class CliProvider implements Provider {
543544

544545
return {
545546
output: parsed.output,
547+
metadata: deriveSkillCallMetadataFromMessages(parsed.output),
546548
tokenUsage: parsed.tokenUsage,
547549
costUsd: parsed.costUsd,
548550
durationMs: parsed.durationMs ?? measuredDurationMs,
@@ -804,6 +806,7 @@ export class CliProvider implements Provider {
804806

805807
return {
806808
output: parsed.output,
809+
metadata: deriveSkillCallMetadataFromMessages(parsed.output),
807810
tokenUsage: parsed.tokenUsage,
808811
costUsd: parsed.costUsd,
809812
durationMs: parsed.durationMs ?? perRequestFallbackMs,

packages/core/src/evaluation/providers/copilot-cli.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
import { resolveDefaultProviderLogDir } from './log-directory.js';
2424
import { normalizeToolCall } from './normalize-tool-call.js';
2525
import { buildPromptDocument, normalizeInputFiles } from './preread.js';
26+
import { deriveSkillCallMetadataFromMessages } from './skill-calls.js';
2627
import { buildTargetExecutionEnvelope } from './target-execution.js';
2728
import type { CopilotCliResolvedConfig, CopilotCustomProviderConfig } from './targets.js';
2829
import type {
@@ -369,6 +370,7 @@ export class CopilotCliProvider implements Provider {
369370
logFile: logger?.filePath,
370371
},
371372
output: outputMessages,
373+
metadata: deriveSkillCallMetadataFromMessages(outputMessages),
372374
tokenUsage,
373375
costUsd,
374376
durationMs,

packages/core/src/evaluation/providers/copilot-sdk.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
import { resolveDefaultProviderLogDir } from './log-directory.js';
1616
import { normalizeToolCall } from './normalize-tool-call.js';
1717
import { buildPromptDocument, normalizeInputFiles } from './preread.js';
18+
import { deriveSkillCallMetadataFromMessages } from './skill-calls.js';
1819
import type { CopilotSdkResolvedConfig } from './targets.js';
1920
import type {
2021
Message,
@@ -331,6 +332,7 @@ export class CopilotSdkProvider implements Provider {
331332
logFile: logger?.filePath,
332333
},
333334
output,
335+
metadata: deriveSkillCallMetadataFromMessages(output),
334336
tokenUsage,
335337
costUsd,
336338
durationMs,

packages/core/src/evaluation/providers/pi-cli.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
} from './pi-provider-aliases.js';
3636
import { extractPiTextContent, toFiniteNumber } from './pi-utils.js';
3737
import { normalizeInputFiles } from './preread.js';
38+
import { deriveSkillCallMetadataFromMessages } from './skill-calls.js';
3839
import type { PiCliResolvedConfig } from './targets.js';
3940
import type {
4041
Message,
@@ -155,6 +156,7 @@ export class PiCliProvider implements Provider {
155156
logFile: logger?.filePath,
156157
},
157158
output,
159+
metadata: deriveSkillCallMetadataFromMessages(output),
158160
tokenUsage,
159161
durationMs,
160162
startTime,
@@ -771,7 +773,7 @@ function extractMessages(events: unknown[]): readonly Message[] {
771773

772774
// Pi CLI may emit tool_execution_start/tool_execution_end events whose tool
773775
// calls are absent from the final agent_end messages. Reconstruct them and
774-
// inject into the last assistant message so evaluators (e.g. skill-trigger)
776+
// inject into the last assistant message so trajectory and skill-use graders
775777
// can detect them.
776778
const eventToolCalls = extractToolCallsFromEvents(events);
777779
if (eventToolCalls.length > 0) {

packages/core/src/evaluation/providers/pi-coding-agent.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
} from './pi-provider-aliases.js';
2929
import { extractPiTextContent, toFiniteNumber, toPiContentArray } from './pi-utils.js';
3030
import { normalizeInputFiles } from './preread.js';
31+
import { deriveSkillCallMetadataFromMessages } from './skill-calls.js';
3132
import type { PiCodingAgentResolvedConfig } from './targets.js';
3233
import type {
3334
Message,
@@ -512,6 +513,7 @@ export class PiCodingAgentProvider implements Provider {
512513
provider: this.config.subprovider,
513514
},
514515
output,
516+
metadata: deriveSkillCallMetadataFromMessages(output),
515517
tokenUsage,
516518
costUsd,
517519
durationMs,

0 commit comments

Comments
 (0)