
Evals
- 116 installs
- 17.2k repo stars
- Updated August 1, 2026
- danielmiessler/personal_ai_infrastructure
Define datasets, graders, and regression suites to measure agent prompt, tool, and end-to-end behavior before promoting changes.
About
The evals skill in danielmiessler/personal_ai_infrastructure operationalizes evaluation for Personal AI Infrastructure: curating prompt suites, scoring responses, comparing model versions, catching tool regressions, and documenting pass/fail thresholds before shipping agent behavior changes.
- Eval set and case authoring
- Automated graders and rubrics
- Regression tracking across prompts
- Tool-use and trajectory checks
- Release gating for agent updates
Evals by the numbers
- 116 all-time installs (skills.sh)
- Ranked #3,899 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/danielmiessler/personal_ai_infrastructure --skill evalsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 116 |
|---|---|
| repo stars | ★ 17.2k |
| Last updated | August 1, 2026 |
| Repository | danielmiessler/personal_ai_infrastructure ↗ |
What it does
Define datasets, graders, and regression suites to measure agent prompt, tool, and end-to-end behavior before promoting changes.
Files
Customization
Before executing, check for user customizations at: ~/.claude/PAI/USER/SKILLCUSTOMIZATIONS/Evals/
If this directory exists, load and apply any PREFERENCES.md, configurations, or resources found there. These override default behavior. If the directory does not exist, proceed with skill defaults.
🚨 MANDATORY: Voice Notification (REQUIRED BEFORE ANY ACTION)
You MUST send this notification BEFORE doing anything else when this skill is invoked.
1. Send voice notification:
curl -s -X POST http://localhost:31337/notify \
-H "Content-Type: application/json" \
-d '{"message": "Running the WORKFLOWNAME workflow in the Evals skill to ACTION"}' \
> /dev/null 2>&1 &2. Output text notification:
Running the **WorkflowName** workflow in the **Evals** skill to ACTION...This is not optional. Execute this curl command immediately upon skill invocation.
Evals - AI Agent Evaluation Framework
Comprehensive agent evaluation system based on Anthropic's "Demystifying Evals for AI Agents" (Jan 2026).
Key differentiator: Evaluates agent workflows (transcripts, tool calls, multi-turn conversations), not just single outputs.
---
When to Activate
- "run evals", "test this agent", "evaluate", "check quality", "benchmark"
- "regression test", "capability test"
- "run scenario", "multi-turn eval", "simulated user test"
- "create scenario", "simulate conversation"
- Compare agent behaviors across changes
- Validate agent workflows before deployment
- Verify ALGORITHM ISC rows
- Create new evaluation tasks from failures
---
Core Concepts
Three Grader Types
| Type | Strengths | Weaknesses | Use For |
|---|---|---|---|
| Code-based | Fast, cheap, deterministic, reproducible | Brittle, lacks nuance | Tests, state checks, tool verification |
| Model-based | Flexible, captures nuance, scalable | Non-deterministic, expensive | Quality rubrics, assertions, comparisons |
| Human | Gold standard, handles subjectivity | Expensive, slow | Calibration, spot checks, A/B testing |
Evaluation Types
| Type | Pass Target | Purpose |
|---|---|---|
| Capability | ~70% | Stretch goals, measuring improvement potential |
| Regression | ~99% | Quality gates, detecting backsliding |
Key Metrics
- pass@k: Probability of at least 1 success in k trials (measures capability)
- pass^k: Probability all k trials succeed (measures consistency/reliability)
---
Workflow Routing
| Request Pattern | Route To |
|---|---|
| Run eval, evaluate suite, run tests, benchmark | Workflows/RunEval.md |
| Compare models, model comparison, A/B test models | Workflows/CompareModels.md |
| Compare prompts, prompt comparison, test prompts | Workflows/ComparePrompts.md |
| Create judge, model grader, evaluation judge | Workflows/CreateJudge.md |
| Create use case, new eval, test case, create suite | Workflows/CreateUseCase.md |
| Run scenario, multi-turn eval, simulated user test | Workflows/RunScenario.md |
| Create scenario, new multi-turn eval, simulate conversation | Workflows/CreateScenario.md |
| View results, eval results, scores, pass rate | Workflows/ViewResults.md |
CLI Quick Reference
| Trigger | Tool |
|---|---|
| Run suite | Tools/AlgorithmBridge.ts |
| Log failure | Tools/FailureToTask.ts log |
| Convert failures | Tools/FailureToTask.ts convert-all |
| Create suite | Tools/SuiteManager.ts create |
| Check saturation | Tools/SuiteManager.ts check-saturation |
| Run scenario | Tools/ScenarioRunner.ts --scenario <path> |
---
Quick Reference
CLI Commands
# Run an eval suite
bun run ${CLAUDE_SKILL_DIR}/Tools/AlgorithmBridge.ts -s <suite>
# Log a failure for later conversion
bun run ${CLAUDE_SKILL_DIR}/Tools/FailureToTask.ts log "description" -c category -s severity
# Convert failures to test tasks
bun run ${CLAUDE_SKILL_DIR}/Tools/FailureToTask.ts convert-all
# Manage suites
bun run ${CLAUDE_SKILL_DIR}/Tools/SuiteManager.ts create <name> -t capability -d "description"
bun run ${CLAUDE_SKILL_DIR}/Tools/SuiteManager.ts list
bun run ${CLAUDE_SKILL_DIR}/Tools/SuiteManager.ts check-saturation <name>
bun run ${CLAUDE_SKILL_DIR}/Tools/SuiteManager.ts graduate <name>ALGORITHM Integration
Evals is a verification method for THE ALGORITHM ISC rows:
# Run eval and update ISC row
bun run ${CLAUDE_SKILL_DIR}/Tools/AlgorithmBridge.ts -s regression-core -r 3 -uISC rows can specify eval verification:
| # | What Ideal Looks Like | Verify |
|---|----------------------|--------|
| 1 | Auth bypass fixed | eval:auth-security |
| 2 | Tests all pass | eval:regression |---
Available Graders
Code-Based (Fast, Deterministic)
| Grader | Use Case |
|---|---|
string_match | Exact substring matching |
regex_match | Pattern matching |
binary_tests | Run test files |
static_analysis | Lint, type-check, security scan |
state_check | Verify system state after execution |
tool_calls | Verify specific tools were called |
Model-Based (Nuanced)
| Grader | Use Case |
|---|---|
llm_rubric | Score against detailed rubric |
natural_language_assert | Check assertions are true |
pairwise_comparison | Compare to reference with position swap |
---
Domain Patterns
Pre-configured grader stacks for common agent types:
| Domain | Primary Graders |
|---|---|
coding | binary_tests + static_analysis + tool_calls + llm_rubric |
conversational | llm_rubric + natural_language_assert + state_check |
research | llm_rubric + natural_language_assert + tool_calls |
computer_use | state_check + tool_calls + llm_rubric |
See Data/DomainPatterns.yaml for full configurations.
---
Task Schema (YAML)
task:
id: "fix-auth-bypass_1"
description: "Fix authentication bypass when password is empty"
type: regression # or capability
domain: coding
graders:
- type: binary_tests
required: [test_empty_pw.py]
weight: 0.30
- type: tool_calls
weight: 0.20
params:
sequence: [read_file, edit_file, run_tests]
- type: llm_rubric
weight: 0.50
params:
rubric: prompts/security_review.md
trials: 3
pass_threshold: 0.75---
Resource Index
| Resource | Purpose |
|---|---|
Types/index.ts | Core type definitions |
Graders/CodeBased/ | Deterministic graders |
Graders/ModelBased/ | LLM-powered graders |
Tools/TranscriptCapture.ts | Capture agent trajectories |
Tools/TrialRunner.ts | Multi-trial execution with pass@k |
Tools/SuiteManager.ts | Suite management and saturation |
Tools/FailureToTask.ts | Convert failures to test tasks |
Tools/AlgorithmBridge.ts | ALGORITHM integration |
Tools/ScenarioRunner.ts | Multi-turn scenario runner (langwatch/scenario) |
Tools/PAIAgentAdapter.ts | Wraps PAI Inference.ts as scenario AgentAdapter |
Tools/ScenarioToTranscript.ts | Scenario result → Evals Transcript/Trial/GraderResult |
Scenarios/ | Authored multi-turn scenarios (.scenario.ts) |
Data/DomainPatterns.yaml | Domain-specific grader configs |
---
Key Principles (from Anthropic)
1. Start with 20-50 real failures - Don't overthink, capture what actually broke 2. Unambiguous tasks - Two experts should reach identical verdicts 3. Balanced problem sets - Test both "should do" AND "should NOT do" 4. Grade outputs, not paths - Don't penalize valid creative solutions 5. Calibrate LLM judges - Against human expert judgment 6. Check transcripts regularly - Verify graders work correctly 7. Monitor saturation - Graduate to regression when hitting 95%+ 8. Build infrastructure early - Evals shape how quickly you can adopt new models
---
Related
- ALGORITHM: Evals is a verification method
- Science: Evals implements scientific method
- Browser: For visual verification graders
Gotchas
- Choose the right grader type: Code-based for deterministic checks (fast, cheap). Model-based for nuanced quality (flexible, expensive). Human for calibration (gold standard, slow).
- pass@k scoring requires multiple runs. A single run doesn't give statistical significance. Default to pass@3 minimum.
- Transcript capture must be enabled BEFORE the test run. Can't retroactively capture transcripts.
- Eval results go to the current work directory — not a global location. Tie evals to the work item.
- Don't evaluate skills with trivial prompts. Simple one-liners may not trigger skill usage. Test prompts must be substantive.
Examples
Example 1: Compare two prompts
User: "evaluate which prompt produces better summaries"
→ Creates eval suite with 3+ test cases
→ Runs both prompts against test cases
→ Model-based grader scores quality
→ Reports pass@k and comparative analysisExample 2: Regression test a skill change
User: "run evals on the Research skill after the update"
→ Uses existing test fixtures for Research
→ Before/after comparison
→ Reports any quality regressionsExecution Log
After completing any workflow, append a single JSONL entry:
echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"Evals","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> ~/.claude/PAI/MEMORY/SKILLS/execution.jsonlReplace WORKFLOW_USED with the workflow executed, 8_WORD_SUMMARY with a brief input description, and SECONDS with approximate wall-clock time. Log status: "error" if the workflow failed.
Evals Best Practices
LLM-as-Judge Design
1. Reasoning before scoring: Always require explanation first 2. Use 1-5 scale: Most reliable, avoid 0-100 3. Different judge model: Don't self-judge 4. Position swapping: Average A-first and B-first results 5. Multi-judge panels: 5-10 models, 7x cheaper than large single judge
---
Creating Use Cases
1. Start with golden example: Use real, proven output as reference 2. Define clear criteria: Mix deterministic (60%) + AI-based (40%) 3. Set pass threshold: 75% is recommended baseline 4. Version prompts: Use semantic versioning 5. Document thoroughly: README should explain what you're testing
---
Running Evaluations
1. Run deterministic first: Fast gate before expensive AI evals 2. Use multiple test cases: Minimum 5-10 for reliability 3. Test edge cases: Include difficult/ambiguous examples 4. Track over time: Run evals regularly for regression detection 5. Report statistics: Include SEM, confidence intervals
---
Interpreting Results
1. Look at individual scores: Not just overall pass/fail 2. Check failed scorers: Understand why tests failed 3. Compare to baseline: Track improvement/regression 4. Validate with human review: AI judges aren't perfect 5. Adjust weights: Based on what matters most
---
Statistical Rigor Requirements
- Report Standard Error of Mean (SEM)
- Confidence intervals (95% default)
- Statistical significance testing
- Pass/fail rates with thresholds
Evals CLI Reference
CLI-First Architecture
This skill follows the CLI-First Architecture pattern:
User Request -> AI orchestrates -> EvalServer CLI -> Deterministic results---
CLI Commands
Use Case Management
# Create new use case
bun run ~/.claude/skills/Evals/EvalServer/cli.ts use-case create --name <name>
# List all use cases
bun run ~/.claude/skills/Evals/EvalServer/cli.ts use-case list
# Show use case details
bun run ~/.claude/skills/Evals/EvalServer/cli.ts use-case show <name>Test Case Management
# Add test case to use case
bun run ~/.claude/skills/Evals/EvalServer/cli.ts test-case add --use-case <name>
# List test cases for use case
bun run ~/.claude/skills/Evals/EvalServer/cli.ts test-case list --use-case <name>Run Evaluations
# Run eval for use case (optional model specification)
bun run ~/.claude/skills/Evals/EvalServer/cli-run.ts --use-case <name> [--model <model>]---
Web UI
Start the EvalServer for visual evaluation:
cd ~/.claude/skills/Evals/EvalServer
bun run dev # Starts on http://localhost:5173Features:
- Real-time eval execution with streaming
- Visual test case management
- Results comparison dashboard
- Bi-directional file <-> UI sync
---
Storage Strategy
Files (Source of Truth)
~/.claude/skills/Evals/
├── UseCases/
│ └── <name>/
│ ├── config.yaml # Criteria, thresholds
│ ├── judge-config.yaml # Judge template data
│ ├── rubric.yaml # Rubric template data
│ ├── test-cases/ # Input/expected pairs
│ ├── golden-outputs/ # Reference standards
│ ├── prompts/ # Versioned prompts
│ └── README.md # Use case documentation
├── Results/
│ └── <use-case>/
│ └── <run-id>/ # Per-run results
└── EvalServer/ # Web UI + execution engineSQLite (Query Optimization)
- Database:
EvalServer/storage/evals.db - Used ONLY for: Fast queries, analytics, comparisons
- Can be rebuilt from files
# Domain-Specific Grader Patterns
# Based on Anthropic's "Demystifying Evals for AI Agents"
domains:
coding:
description: "Coding agent evaluation"
primary_graders:
- type: binary_tests
weight: 0.30
description: "Unit tests must pass"
- type: static_analysis
weight: 0.15
params:
commands: [biome, tsc]
- type: tool_calls
weight: 0.20
params:
sequence: [read_file, edit_file, run_tests]
- type: llm_rubric
weight: 0.25
params:
rubric: |
Evaluate the code change for:
- Correctness: Does it solve the problem?
- Style: Does it follow project conventions?
- Safety: Does it avoid introducing bugs?
- type: state_check
weight: 0.10
tracked_metrics:
- type: transcript
metrics: [n_turns, n_toolcalls, n_total_tokens]
conversational:
description: "Conversational agent evaluation"
primary_graders:
- type: llm_rubric
weight: 0.40
params:
rubric: |
Evaluate the conversation for:
- Task Resolution: Was the user's issue resolved?
- Empathy: Did the agent acknowledge frustration?
- Clarity: Were explanations clear?
- Groundedness: Were claims backed by tool results?
- type: natural_language_assert
weight: 0.25
params:
assertions:
- "Agent showed empathy for customer's situation"
- "Resolution was clearly explained"
- "Agent's response was grounded in tool results"
- type: state_check
weight: 0.15
params:
expect:
status: resolved
- type: tool_calls
weight: 0.20
params:
required:
- tool: verify_identity
- tool: send_confirmation
tracked_metrics:
- type: transcript
metrics: [n_turns]
constraints:
max_turns: 10
research:
description: "Research agent evaluation"
primary_graders:
- type: llm_rubric
weight: 0.50
params:
rubric: |
Evaluate the research output for:
- Groundedness: Are claims backed by sources?
- Coverage: Are key topics addressed?
- Source Quality: Are sources reputable?
- Synthesis: Is information well-organized?
- type: natural_language_assert
weight: 0.30
params:
assertions:
- "Claims are backed by cited sources"
- "Multiple perspectives are considered"
- "Sources are from reputable domains"
- type: tool_calls
weight: 0.20
params:
required:
- tool: web_search
- tool: read_source
tracked_metrics:
- type: transcript
metrics: [n_turns, n_toolcalls]
computer_use:
description: "Computer/GUI agent evaluation"
primary_graders:
- type: state_check
weight: 0.40
params:
verify_gui: true
- type: tool_calls
weight: 0.30
params:
required:
- tool: screenshot
- tool: click
- type: llm_rubric
weight: 0.30
params:
rubric: |
Evaluate the GUI interaction for:
- Efficiency: Were actions minimal and direct?
- Correctness: Was the target element correct?
- Error Handling: Were errors handled gracefully?
tracked_metrics:
- type: latency
metrics: [time_to_last_token]
general:
description: "General-purpose evaluation"
primary_graders:
- type: llm_rubric
weight: 0.50
params:
rubric: |
Evaluate the output for:
- Accuracy: Is the information correct?
- Completeness: Is the task fully addressed?
- Quality: Is the output well-structured?
- type: natural_language_assert
weight: 0.30
params:
assertions:
- "The output addresses the user's request"
- "The information appears accurate"
- type: tool_calls
weight: 0.20
tracked_metrics:
- type: transcript
metrics: [n_turns, n_toolcalls]
# Default thresholds by eval type
thresholds:
capability:
pass_threshold: 0.70
saturation_threshold: 0.95
description: "Stretch goals - expecting <100% pass rate"
regression:
pass_threshold: 0.95
alert_threshold: 0.90
description: "Quality gates - must maintain near 100%"
/**
* Base Grader Interface
* All graders implement this interface for consistent execution
*/
import type { GraderConfig, GraderResult, Transcript, GraderType } from '../Types/index.ts';
export interface GraderContext {
task_id: string;
trial_id: string;
transcript: Transcript;
output: string; // Final output text
working_dir?: string;
reference?: string; // Reference/golden output if available
}
export abstract class BaseGrader {
abstract type: GraderType;
abstract category: 'code_based' | 'model_based' | 'human';
protected config: GraderConfig;
constructor(config: GraderConfig) {
this.config = config;
}
/**
* Execute the grader and return results
*/
abstract grade(context: GraderContext): Promise<GraderResult>;
/**
* Get the weight for this grader
*/
getWeight(): number {
return this.config.weight ?? 1.0;
}
/**
* Check if this grader is required (task fails if grader fails)
*/
isRequired(): boolean {
return this.config.required ?? false;
}
/**
* Create a result object
*/
protected createResult(
score: number,
passed: boolean,
duration_ms: number,
options?: {
reasoning?: string;
details?: Record<string, unknown>;
}
): GraderResult {
return {
grader_type: this.type,
weight: this.getWeight(),
score,
passed,
duration_ms,
reasoning: options?.reasoning,
details: options?.details,
};
}
}
/**
* Grader registry for dynamic instantiation
*/
const graderRegistry = new Map<GraderType, new (config: GraderConfig) => BaseGrader>();
export function registerGrader(type: GraderType, graderClass: new (config: GraderConfig) => BaseGrader): void {
graderRegistry.set(type, graderClass);
}
export function createGrader(config: GraderConfig): BaseGrader {
const GraderClass = graderRegistry.get(config.type);
if (!GraderClass) {
throw new Error(`Unknown grader type: ${config.type}`);
}
return new GraderClass(config);
}
export function listGraders(): GraderType[] {
return Array.from(graderRegistry.keys());
}
/**
* Run multiple graders and aggregate results
*/
export async function runGraders(
graders: BaseGrader[],
context: GraderContext
): Promise<{ results: GraderResult[]; aggregate_score: number; passed: boolean }> {
const results: GraderResult[] = [];
let totalWeight = 0;
let weightedSum = 0;
let allRequiredPassed = true;
for (const grader of graders) {
const result = await grader.grade(context);
results.push(result);
// Aggregate
const weight = grader.getWeight();
totalWeight += weight;
weightedSum += result.score * weight;
// Check required
if (grader.isRequired() && !result.passed) {
allRequiredPassed = false;
}
}
const aggregate_score = totalWeight > 0 ? weightedSum / totalWeight : 0;
const passed = allRequiredPassed && aggregate_score >= 0.5; // Default threshold
return { results, aggregate_score, passed };
}
/**
* Binary Tests Grader
* Run actual test files and check pass/fail
*/
import { BaseGrader, registerGrader, type GraderContext } from '../Base.ts';
import type { GraderConfig, GraderResult, BinaryTestsParams } from '../../Types/index.ts';
import { $ } from 'bun';
export class BinaryTestsGrader extends BaseGrader {
type = 'binary_tests' as const;
category = 'code_based' as const;
async grade(context: GraderContext): Promise<GraderResult> {
const start = performance.now();
const params = this.config.params as BinaryTestsParams;
if (!params?.test_files?.length) {
return this.createResult(0, false, performance.now() - start, {
reasoning: 'No test files configured',
});
}
const workingDir = context.working_dir ?? process.cwd();
const timeout = params.timeout_ms ?? 60000;
const results: { file: string; passed: boolean; output: string; error?: string }[] = [];
for (const testFile of params.test_files) {
try {
// Detect test command based on file extension
const command = params.test_command ?? this.detectTestCommand(testFile);
const result = await $`cd ${workingDir} && timeout ${Math.ceil(timeout/1000)} ${command} ${testFile}`
.quiet()
.nothrow();
const passed = result.exitCode === 0;
results.push({
file: testFile,
passed,
output: result.stdout.toString().slice(-500), // Last 500 chars
error: passed ? undefined : result.stderr.toString().slice(-500),
});
} catch (e) {
results.push({
file: testFile,
passed: false,
output: '',
error: String(e),
});
}
}
const passCount = results.filter(r => r.passed).length;
const score = passCount / params.test_files.length;
const passed = passCount === params.test_files.length;
return this.createResult(score, passed, performance.now() - start, {
reasoning: `${passCount}/${params.test_files.length} tests passed`,
details: {
results,
working_dir: workingDir,
},
});
}
private detectTestCommand(file: string): string {
if (file.endsWith('.py')) return 'python -m pytest';
if (file.endsWith('.ts')) return 'bun test';
if (file.endsWith('.js')) return 'node --test';
if (file.endsWith('.go')) return 'go test';
if (file.endsWith('.rs')) return 'cargo test --';
return 'bun test'; // Default
}
}
registerGrader('binary_tests', BinaryTestsGrader);
/**
* Code-Based Graders Index
* Fast, deterministic graders
*/
// Import to register graders
import './StringMatch.ts';
import './RegexMatch.ts';
import './BinaryTests.ts';
import './StaticAnalysis.ts';
import './StateCheck.ts';
import './ToolCallVerification.ts';
export { StringMatchGrader } from './StringMatch.ts';
export { RegexMatchGrader } from './RegexMatch.ts';
export { BinaryTestsGrader } from './BinaryTests.ts';
export { StaticAnalysisGrader } from './StaticAnalysis.ts';
export { StateCheckGrader } from './StateCheck.ts';
export { ToolCallVerificationGrader } from './ToolCallVerification.ts';
/**
* Regex Match Grader
* Pattern matching with regular expressions
*/
import { BaseGrader, registerGrader, type GraderContext } from '../Base.ts';
import type { GraderConfig, GraderResult, RegexMatchParams } from '../../Types/index.ts';
export class RegexMatchGrader extends BaseGrader {
type = 'regex_match' as const;
category = 'code_based' as const;
async grade(context: GraderContext): Promise<GraderResult> {
const start = performance.now();
const params = this.config.params as RegexMatchParams;
if (!params?.patterns?.length) {
return this.createResult(0, false, performance.now() - start, {
reasoning: 'No patterns configured',
});
}
const flags = params.flags ?? 'gm';
const results = params.patterns.map(pattern => {
try {
const regex = new RegExp(pattern, flags);
const matched = regex.test(context.output);
return { pattern, matched, error: null };
} catch (e) {
return { pattern, matched: false, error: String(e) };
}
});
const matchCount = results.filter(r => r.matched).length;
const errorCount = results.filter(r => r.error).length;
let passed: boolean;
let score: number;
if (params.mode === 'all') {
passed = matchCount === params.patterns.length;
score = matchCount / params.patterns.length;
} else {
passed = matchCount > 0;
score = passed ? 1 : 0;
}
// Penalize for errors
if (errorCount > 0) {
score *= (params.patterns.length - errorCount) / params.patterns.length;
}
return this.createResult(score, passed, performance.now() - start, {
reasoning: `Matched ${matchCount}/${params.patterns.length} patterns${errorCount > 0 ? ` (${errorCount} errors)` : ''} (mode: ${params.mode})`,
details: {
results,
mode: params.mode,
flags,
},
});
}
}
registerGrader('regex_match', RegexMatchGrader);
/**
* State Check Grader
* Verify system state after agent execution
*/
import { BaseGrader, registerGrader, type GraderContext } from '../Base.ts';
import type { GraderConfig, GraderResult, StateCheckParams } from '../../Types/index.ts';
import { existsSync, readFileSync } from 'fs';
import { join } from 'path';
export class StateCheckGrader extends BaseGrader {
type = 'state_check' as const;
category = 'code_based' as const;
async grade(context: GraderContext): Promise<GraderResult> {
const start = performance.now();
const params = this.config.params as StateCheckParams;
const checks: { check: string; passed: boolean; expected?: unknown; actual?: unknown }[] = [];
const workingDir = context.working_dir ?? process.cwd();
// Check expected state object (e.g., security_logs: {event_type: "auth_blocked"})
if (params.expect) {
for (const [key, expected] of Object.entries(params.expect)) {
const checkResult = await this.checkState(key, expected, context);
checks.push({
check: `state.${key}`,
passed: checkResult.passed,
expected,
actual: checkResult.actual,
});
}
}
// Check file contents
if (params.check_files) {
for (const fileCheck of params.check_files) {
const filePath = join(workingDir, fileCheck.path);
if (!existsSync(filePath)) {
checks.push({
check: `file.${fileCheck.path}`,
passed: false,
expected: 'file exists',
actual: 'file not found',
});
continue;
}
const content = readFileSync(filePath, 'utf-8');
// Check contains
if (fileCheck.contains) {
for (const pattern of fileCheck.contains) {
const found = content.includes(pattern);
checks.push({
check: `file.${fileCheck.path}.contains`,
passed: found,
expected: pattern,
actual: found ? 'found' : 'not found',
});
}
}
// Check not_contains
if (fileCheck.not_contains) {
for (const pattern of fileCheck.not_contains) {
const found = content.includes(pattern);
checks.push({
check: `file.${fileCheck.path}.not_contains`,
passed: !found,
expected: `NOT: ${pattern}`,
actual: found ? 'found (should not exist)' : 'not found (correct)',
});
}
}
}
}
// Check environment variables
if (params.check_env) {
for (const [key, expected] of Object.entries(params.check_env)) {
const actual = process.env[key];
checks.push({
check: `env.${key}`,
passed: actual === expected,
expected,
actual,
});
}
}
const passCount = checks.filter(c => c.passed).length;
const score = checks.length > 0 ? passCount / checks.length : 1;
const passed = passCount === checks.length;
return this.createResult(score, passed, performance.now() - start, {
reasoning: `${passCount}/${checks.length} state checks passed`,
details: { checks },
});
}
private async checkState(
key: string,
expected: unknown,
context: GraderContext
): Promise<{ passed: boolean; actual?: unknown }> {
// Check if expected state exists in the transcript's final outcome
if (context.transcript.final_outcome) {
const outcome = context.transcript.final_outcome as Record<string, unknown>;
if (key in outcome) {
const actual = outcome[key];
const passed = this.deepEqual(actual, expected);
return { passed, actual };
}
}
// Also check in the output text for JSON-like patterns
try {
const jsonMatch = context.output.match(/\{[\s\S]*\}/);
if (jsonMatch) {
const parsed = JSON.parse(jsonMatch[0]);
if (key in parsed) {
const actual = parsed[key];
const passed = this.deepEqual(actual, expected);
return { passed, actual };
}
}
} catch {
// Not valid JSON, continue
}
return { passed: false, actual: undefined };
}
private deepEqual(a: unknown, b: unknown): boolean {
if (a === b) return true;
if (typeof a !== typeof b) return false;
if (typeof a !== 'object' || a === null || b === null) return false;
const aObj = a as Record<string, unknown>;
const bObj = b as Record<string, unknown>;
// For expected, check if all expected keys match (subset matching)
for (const key of Object.keys(bObj)) {
if (!(key in aObj)) return false;
if (!this.deepEqual(aObj[key], bObj[key])) return false;
}
return true;
}
}
registerGrader('state_check', StateCheckGrader);
/**
* Static Analysis Grader
* Run linters, type checkers, security scanners
*/
import { BaseGrader, registerGrader, type GraderContext } from '../Base.ts';
import type { GraderConfig, GraderResult, StaticAnalysisParams } from '../../Types/index.ts';
import { $ } from 'bun';
export class StaticAnalysisGrader extends BaseGrader {
type = 'static_analysis' as const;
category = 'code_based' as const;
async grade(context: GraderContext): Promise<GraderResult> {
const start = performance.now();
const params = this.config.params as StaticAnalysisParams;
if (!params?.commands?.length) {
return this.createResult(0, false, performance.now() - start, {
reasoning: 'No analysis commands configured',
});
}
const workingDir = context.working_dir ?? process.cwd();
const results: { command: string; passed: boolean; output: string; warnings: number; errors: number }[] = [];
for (const command of params.commands) {
try {
const result = await $`cd ${workingDir} && ${command}`.quiet().nothrow();
const output = result.stdout.toString() + result.stderr.toString();
const warnings = this.countIssues(output, 'warning');
const errors = this.countIssues(output, 'error');
// Pass if no errors (and no warnings if fail_on_warning is set)
const passed = errors === 0 && (!params.fail_on_warning || warnings === 0);
results.push({
command,
passed,
output: output.slice(-1000), // Last 1000 chars
warnings,
errors,
});
} catch (e) {
results.push({
command,
passed: false,
output: String(e),
warnings: 0,
errors: 1,
});
}
}
const passCount = results.filter(r => r.passed).length;
const totalErrors = results.reduce((sum, r) => sum + r.errors, 0);
const totalWarnings = results.reduce((sum, r) => sum + r.warnings, 0);
const score = passCount / params.commands.length;
const passed = passCount === params.commands.length;
return this.createResult(score, passed, performance.now() - start, {
reasoning: `${passCount}/${params.commands.length} checks passed (${totalErrors} errors, ${totalWarnings} warnings)`,
details: {
results,
total_errors: totalErrors,
total_warnings: totalWarnings,
fail_on_warning: params.fail_on_warning ?? false,
},
});
}
private countIssues(output: string, type: 'warning' | 'error'): number {
const patterns = type === 'error'
? [/error:/gi, /\berror\b/gi, /failed/gi, /\[E\d+\]/g]
: [/warning:/gi, /\bwarn\b/gi, /\[W\d+\]/g];
let count = 0;
for (const pattern of patterns) {
const matches = output.match(pattern);
count += matches?.length ?? 0;
}
return Math.min(count, 100); // Cap at 100
}
}
registerGrader('static_analysis', StaticAnalysisGrader);
/**
* String Match Grader
* Fast deterministic check for exact or pattern matching
*/
import { BaseGrader, registerGrader, type GraderContext } from '../Base.ts';
import type { GraderConfig, GraderResult, StringMatchParams } from '../../Types/index.ts';
export class StringMatchGrader extends BaseGrader {
type = 'string_match' as const;
category = 'code_based' as const;
async grade(context: GraderContext): Promise<GraderResult> {
const start = performance.now();
const params = this.config.params as StringMatchParams;
if (!params?.patterns?.length) {
return this.createResult(0, false, performance.now() - start, {
reasoning: 'No patterns configured',
});
}
const output = params.case_sensitive ? context.output : context.output.toLowerCase();
const patterns = params.patterns.map(p =>
params.case_sensitive ? p : p.toLowerCase()
);
const matches = patterns.map(pattern => output.includes(pattern));
const matchCount = matches.filter(Boolean).length;
let passed: boolean;
let score: number;
if (params.mode === 'all') {
passed = matchCount === patterns.length;
score = matchCount / patterns.length;
} else {
// 'any' mode
passed = matchCount > 0;
score = passed ? 1 : 0;
}
return this.createResult(score, passed, performance.now() - start, {
reasoning: `Matched ${matchCount}/${patterns.length} patterns (mode: ${params.mode})`,
details: {
patterns,
matches: patterns.map((p, i) => ({ pattern: p, matched: matches[i] })),
mode: params.mode,
},
});
}
}
registerGrader('string_match', StringMatchGrader);
/**
* Tool Call Verification Grader
* Verify that specific tools were called with expected parameters
*/
import { BaseGrader, registerGrader, type GraderContext } from '../Base.ts';
import type { GraderConfig, GraderResult, ToolCallsParams } from '../../Types/index.ts';
export class ToolCallVerificationGrader extends BaseGrader {
type = 'tool_calls' as const;
category = 'code_based' as const;
async grade(context: GraderContext): Promise<GraderResult> {
const start = performance.now();
const params = this.config.params as ToolCallsParams;
const toolCalls = context.transcript.tool_calls;
const checks: { check: string; passed: boolean; details?: string }[] = [];
// Check required tool calls
if (params.required) {
for (const req of params.required) {
const matchingCall = toolCalls.find(tc => {
if (tc.name !== req.tool) return false;
// If params specified, check they match
if (req.params) {
for (const [key, expected] of Object.entries(req.params)) {
const actual = tc.params[key];
// Support glob patterns for paths
if (typeof expected === 'string' && expected.includes('*')) {
const pattern = new RegExp('^' + expected.replace(/\*/g, '.*') + '$');
if (!pattern.test(String(actual))) return false;
} else if (actual !== expected) {
return false;
}
}
}
return true;
});
checks.push({
check: `required.${req.tool}`,
passed: !!matchingCall,
details: matchingCall
? `Found: ${JSON.stringify(matchingCall.params).slice(0, 100)}`
: `Not found in ${toolCalls.length} tool calls`,
});
}
}
// Check forbidden tool calls
if (params.forbidden) {
for (const forbidden of params.forbidden) {
const found = toolCalls.some(tc => tc.name === forbidden);
checks.push({
check: `forbidden.${forbidden}`,
passed: !found,
details: found ? 'Found (should not exist)' : 'Not found (correct)',
});
}
}
// Check sequence (tools must be called in order)
if (params.sequence) {
const toolOrder = toolCalls.map(tc => tc.name);
let seqIndex = 0;
for (const tool of toolOrder) {
if (seqIndex < params.sequence.length && tool === params.sequence[seqIndex]) {
seqIndex++;
}
}
const sequenceComplete = seqIndex === params.sequence.length;
checks.push({
check: 'sequence',
passed: sequenceComplete,
details: sequenceComplete
? `Sequence complete: ${params.sequence.join(' → ')}`
: `Incomplete: found ${seqIndex}/${params.sequence.length} in order`,
});
}
// Check max calls
if (params.max_calls !== undefined) {
const withinLimit = toolCalls.length <= params.max_calls;
checks.push({
check: 'max_calls',
passed: withinLimit,
details: `${toolCalls.length} calls (max: ${params.max_calls})`,
});
}
const passCount = checks.filter(c => c.passed).length;
const score = checks.length > 0 ? passCount / checks.length : 1;
const passed = passCount === checks.length;
return this.createResult(score, passed, performance.now() - start, {
reasoning: `${passCount}/${checks.length} tool call checks passed`,
details: {
checks,
total_tool_calls: toolCalls.length,
tool_call_summary: toolCalls.map(tc => tc.name),
},
});
}
}
registerGrader('tool_calls', ToolCallVerificationGrader);
/**
* Graders Index
* Central export for all grader types
*/
// Base
export * from './Base.ts';
// Code-based graders
export * from './CodeBased/index.ts';
// Model-based graders
export * from './ModelBased/index.ts';
// Note: Human graders require separate implementation
// See Graders/Human/ for review workflow
/**
* Model-Based Graders Index
* LLM-powered graders for nuanced evaluation
*/
// Import to register graders
import './LLMRubric.ts';
import './NaturalLanguageAssert.ts';
import './PairwiseComparison.ts';
export { LLMRubricGrader } from './LLMRubric.ts';
export { NaturalLanguageAssertGrader } from './NaturalLanguageAssert.ts';
export { PairwiseComparisonGrader } from './PairwiseComparison.ts';
/**
* LLM Rubric Grader
* Score output against a detailed rubric using an LLM judge
*/
import { BaseGrader, registerGrader, type GraderContext } from '../Base.ts';
import type { GraderConfig, GraderResult, LLMRubricParams } from '../../Types/index.ts';
import { inference, type InferenceLevel } from '../../../../PAI/TOOLS/Inference.ts';
import { readFileSync, existsSync } from 'fs';
export class LLMRubricGrader extends BaseGrader {
type = 'llm_rubric' as const;
category = 'model_based' as const;
async grade(context: GraderContext): Promise<GraderResult> {
const start = performance.now();
const params = this.config.params as LLMRubricParams;
// Load rubric
let rubric = params.rubric;
if (existsSync(params.rubric)) {
rubric = readFileSync(params.rubric, 'utf-8');
}
const scale = params.scale ?? '1-5';
// Map model preference to inference level (default to standard/Sonnet)
const levelMap: Record<string, InferenceLevel> = {
'claude-haiku-4-5-20251001': 'fast',
'claude-sonnet-4-6': 'standard',
'claude-opus-4-6': 'smart',
'claude-sonnet-4-20250514': 'standard',
'claude-opus-4-20250514': 'smart',
};
const level: InferenceLevel = levelMap[params.judge_model ?? ''] ?? 'standard';
// Build prompt
const systemPrompt = this.buildSystemPrompt(scale, params.reasoning_first ?? true);
const userPrompt = this.buildUserPrompt(rubric, params.assertions, context);
try {
const result = await inference({
systemPrompt,
userPrompt,
level,
timeout: 30000,
});
if (!result.success) {
throw new Error(result.error || 'Inference failed');
}
const text = result.output;
const { score, reasoning, assertion_results } = this.parseResponse(text, scale, params.assertions);
const passed = this.scoreToPassed(score, scale);
return this.createResult(score, passed, performance.now() - start, {
reasoning,
details: {
assertion_results,
inference_level: level,
scale,
raw_response: text,
},
});
} catch (e) {
return this.createResult(0, false, performance.now() - start, {
reasoning: `LLM judge error: ${e}`,
});
}
}
private buildSystemPrompt(scale: string, reasoningFirst: boolean): string {
const scaleInstructions = {
'1-5': 'Score from 1 (very poor) to 5 (excellent)',
'1-10': 'Score from 1 (very poor) to 10 (excellent)',
'pass-fail': 'Determine if the output PASSES or FAILS the criteria',
}[scale];
const format = reasoningFirst
? `First explain your reasoning, then provide your score. Format:
REASONING: <your detailed analysis>
SCORE: <your score>`
: `Provide your score first, then explain. Format:
SCORE: <your score>
REASONING: <your explanation>`;
return `You are an expert evaluator assessing AI-generated output against quality criteria.
${scaleInstructions}
${format}
Be objective and fair. Consider both strengths and weaknesses.`;
}
private buildUserPrompt(
rubric: string,
assertions: string[] | undefined,
context: GraderContext
): string {
let prompt = `## Evaluation Rubric
${rubric}
## Output to Evaluate
${context.output}
`;
if (assertions?.length) {
prompt += `
## Specific Assertions to Check
For each assertion, determine if it is TRUE or FALSE:
${assertions.map((a, i) => `${i + 1}. ${a}`).join('\n')}
After the main evaluation, provide assertion results in this format:
ASSERTIONS:
${assertions.map((_, i) => `${i + 1}. TRUE/FALSE`).join('\n')}
`;
}
if (context.reference) {
prompt += `
## Reference Output (for comparison)
${context.reference}
`;
}
prompt += `
## Your Evaluation
Evaluate the output against the rubric and provide your assessment.`;
return prompt;
}
private parseResponse(
text: string,
scale: string,
assertions?: string[]
): { score: number; reasoning: string; assertion_results?: boolean[] } {
// Extract score
let score = 0;
const scoreMatch = text.match(/SCORE:\s*(\d+(?:\.\d+)?|PASS|FAIL)/i);
if (scoreMatch) {
if (scale === 'pass-fail') {
score = scoreMatch[1].toUpperCase() === 'PASS' ? 1 : 0;
} else if (scale === '1-5') {
score = (parseFloat(scoreMatch[1]) - 1) / 4; // Normalize to 0-1
} else if (scale === '1-10') {
score = (parseFloat(scoreMatch[1]) - 1) / 9; // Normalize to 0-1
}
}
// Extract reasoning
const reasoningMatch = text.match(/REASONING:\s*([\s\S]*?)(?=SCORE:|ASSERTIONS:|$)/i);
const reasoning = reasoningMatch?.[1]?.trim() ?? text;
// Extract assertion results
let assertion_results: boolean[] | undefined;
if (assertions?.length) {
const assertionsMatch = text.match(/ASSERTIONS:\s*([\s\S]*?)$/i);
if (assertionsMatch) {
assertion_results = assertions.map((_, i) => {
const lineMatch = assertionsMatch[1].match(new RegExp(`${i + 1}\\.\\s*(TRUE|FALSE)`, 'i'));
return lineMatch?.[1]?.toUpperCase() === 'TRUE';
});
}
}
return { score: Math.max(0, Math.min(1, score)), reasoning, assertion_results };
}
private scoreToPassed(score: number, scale: string): boolean {
if (scale === 'pass-fail') return score >= 0.5;
// For 1-5 and 1-10, pass if score is above middle
return score >= 0.5;
}
}
registerGrader('llm_rubric', LLMRubricGrader);
/**
* Natural Language Assertion Grader
* Check if specific assertions are true about the output
*/
import { BaseGrader, registerGrader, type GraderContext } from '../Base.ts';
import type { GraderConfig, GraderResult, NaturalLanguageAssertParams } from '../../Types/index.ts';
import { inference, type InferenceLevel } from '../../../../PAI/TOOLS/Inference.ts';
export class NaturalLanguageAssertGrader extends BaseGrader {
type = 'natural_language_assert' as const;
category = 'model_based' as const;
async grade(context: GraderContext): Promise<GraderResult> {
const start = performance.now();
const params = this.config.params as NaturalLanguageAssertParams;
if (!params?.assertions?.length) {
return this.createResult(0, false, performance.now() - start, {
reasoning: 'No assertions configured',
});
}
// Map model preference to inference level (default to standard/Sonnet)
const levelMap: Record<string, InferenceLevel> = {
'claude-haiku-4-5-20251001': 'fast',
'claude-sonnet-4-6': 'standard',
'claude-opus-4-6': 'smart',
'claude-sonnet-4-20250514': 'standard',
'claude-opus-4-20250514': 'smart',
};
const level: InferenceLevel = levelMap[params.judge_model ?? ''] ?? 'standard';
const requireAll = params.require_all ?? true;
const systemPrompt = `You are an assertion checker. For each assertion, determine if it is TRUE or FALSE based on the given output.
Be strict and literal. If you cannot clearly verify an assertion, mark it FALSE.
Respond in this exact format for each assertion:
1. TRUE/FALSE: <brief explanation>
2. TRUE/FALSE: <brief explanation>
...`;
const userPrompt = `## Output to Check
${context.output}
## Tool Calls Made (for context)
${context.transcript.tool_calls.map(tc => `- ${tc.name}(${JSON.stringify(tc.params)})`).join('\n') || 'None'}
## Assertions to Verify
${params.assertions.map((a, i) => `${i + 1}. ${a}`).join('\n')}
Check each assertion against the output and tool calls.`;
try {
const result = await inference({
systemPrompt,
userPrompt,
level,
timeout: 30000,
});
if (!result.success) {
throw new Error(result.error || 'Inference failed');
}
const text = result.output;
const results = this.parseResults(text, params.assertions);
const passCount = results.filter(r => r.passed).length;
const score = passCount / params.assertions.length;
const passed = requireAll
? passCount === params.assertions.length
: passCount > 0;
return this.createResult(score, passed, performance.now() - start, {
reasoning: `${passCount}/${params.assertions.length} assertions passed`,
details: {
results,
require_all: requireAll,
inference_level: level,
},
});
} catch (e) {
return this.createResult(0, false, performance.now() - start, {
reasoning: `LLM assertion check error: ${e}`,
});
}
}
private parseResults(
text: string,
assertions: string[]
): { assertion: string; passed: boolean; explanation: string }[] {
return assertions.map((assertion, i) => {
const pattern = new RegExp(`${i + 1}\\.\\s*(TRUE|FALSE):\\s*(.*)`, 'i');
const match = text.match(pattern);
if (match) {
return {
assertion,
passed: match[1].toUpperCase() === 'TRUE',
explanation: match[2].trim(),
};
}
// Try to find by content if numbered format didn't work
const containsTrue = text.toLowerCase().includes(`assertion ${i + 1}`) &&
text.toLowerCase().includes('true');
return {
assertion,
passed: containsTrue,
explanation: 'Could not parse result',
};
});
}
}
registerGrader('natural_language_assert', NaturalLanguageAssertGrader);
/**
* Pairwise Comparison Grader
* Compare output against a reference with position swapping to reduce bias
*/
import { BaseGrader, registerGrader, type GraderContext } from '../Base.ts';
import type { GraderConfig, GraderResult, PairwiseComparisonParams } from '../../Types/index.ts';
import { inference, type InferenceLevel } from '../../../../PAI/TOOLS/Inference.ts';
import { readFileSync, existsSync } from 'fs';
export class PairwiseComparisonGrader extends BaseGrader {
type = 'pairwise_comparison' as const;
category = 'model_based' as const;
async grade(context: GraderContext): Promise<GraderResult> {
const start = performance.now();
const params = this.config.params as PairwiseComparisonParams;
// Load reference
let reference = params.reference;
if (existsSync(params.reference)) {
reference = readFileSync(params.reference, 'utf-8');
}
if (!reference) {
return this.createResult(0, false, performance.now() - start, {
reasoning: 'No reference output available',
});
}
// Map model preference to inference level (default to standard/Sonnet)
const levelMap: Record<string, InferenceLevel> = {
'claude-haiku-4-5-20251001': 'fast',
'claude-sonnet-4-6': 'standard',
'claude-opus-4-6': 'smart',
'claude-sonnet-4-20250514': 'standard',
'claude-opus-4-20250514': 'smart',
};
const level: InferenceLevel = levelMap[params.judge_model ?? ''] ?? 'standard';
const positionSwap = params.position_swap ?? true;
// Run comparison(s)
const results: { position: string; winner: 'A' | 'B' | 'tie'; reasoning: string }[] = [];
// First comparison: Output = A, Reference = B
const result1 = await this.compare(context.output, reference, level, params.criteria);
results.push({ position: 'output_first', ...result1 });
if (positionSwap) {
// Second comparison: Reference = A, Output = B
const result2 = await this.compare(reference, context.output, level, params.criteria);
// Flip winner since positions are swapped
const flippedWinner = result2.winner === 'A' ? 'B' : result2.winner === 'B' ? 'A' : 'tie';
results.push({
position: 'reference_first',
winner: flippedWinner as 'A' | 'B' | 'tie',
reasoning: result2.reasoning,
});
}
// Aggregate results
const outputWins = results.filter(r => r.winner === 'A').length;
const referenceWins = results.filter(r => r.winner === 'B').length;
const ties = results.filter(r => r.winner === 'tie').length;
let score: number;
let aggregateWinner: string;
if (outputWins > referenceWins) {
score = 1.0;
aggregateWinner = 'output';
} else if (referenceWins > outputWins) {
score = 0.0;
aggregateWinner = 'reference';
} else {
score = 0.5;
aggregateWinner = 'tie';
}
// For the score, also consider partial wins
if (positionSwap && results.length === 2) {
score = (outputWins + ties * 0.5) / 2;
}
const passed = score >= 0.5;
return this.createResult(score, passed, performance.now() - start, {
reasoning: `${aggregateWinner} wins (output: ${outputWins}, reference: ${referenceWins}, ties: ${ties})`,
details: {
results,
position_swap: positionSwap,
inference_level: level,
criteria: params.criteria,
},
});
}
private async compare(
outputA: string,
outputB: string,
level: InferenceLevel,
criteria?: string[]
): Promise<{ winner: 'A' | 'B' | 'tie'; reasoning: string }> {
const criteriaText = criteria?.length
? `Focus on these criteria:\n${criteria.map(c => `- ${c}`).join('\n')}`
: 'Consider overall quality, accuracy, clarity, and helpfulness.';
const systemPrompt = `You are comparing two outputs to determine which is better.
${criteriaText}
Respond in this format:
REASONING: <your analysis comparing A and B>
WINNER: A or B or TIE
Be objective. Consider both outputs fairly.`;
const userPrompt = `## Output A
${outputA}
## Output B
${outputB}
Compare these outputs and determine which is better.`;
try {
const result = await inference({
systemPrompt,
userPrompt,
level,
timeout: 30000,
});
if (!result.success) {
throw new Error(result.error || 'Inference failed');
}
const text = result.output;
const winnerMatch = text.match(/WINNER:\s*(A|B|TIE)/i);
const reasoningMatch = text.match(/REASONING:\s*([\s\S]*?)(?=WINNER:|$)/i);
const winner = winnerMatch?.[1]?.toUpperCase() === 'A' ? 'A'
: winnerMatch?.[1]?.toUpperCase() === 'B' ? 'B'
: 'tie';
return {
winner,
reasoning: reasoningMatch?.[1]?.trim() ?? text,
};
} catch (e) {
return {
winner: 'tie',
reasoning: `Comparison error: ${e}`,
};
}
}
}
registerGrader('pairwise_comparison', PairwiseComparisonGrader);
{
"name": "@pai/skill-evals",
"version": "1.1.0",
"type": "module",
"description": "AI agent evaluation framework for PAI — graders, pass@k scoring, multi-turn scenarios",
"scripts": {
"scenario": "bun run Tools/ScenarioRunner.ts"
},
"dependencies": {
"@ai-sdk/anthropic": "2.0.74",
"@langwatch/scenario": "0.4.10",
"ai": "6.0.159"
},
"devDependencies": {
"@types/bun": "latest",
"typescript": "^5.0.0"
}
}
System-Evals - AI Evaluation Framework
Tool Name: evals Architecture: CLI-First (deterministic code execution with AI orchestration) Storage: File-based (source of truth) + SQLite (query optimization) Philosophy: Build deterministic tools, wrap with prompting
---
Overview
Evals is a comprehensive AI evaluation framework for testing both models and prompts across different use cases. It follows the CLI-First Architecture pattern: deterministic CLI commands wrapped with AI orchestration for consistency and reliability.
---
Requirements
Core Operations
1. Use Case Management
- Create new use cases
- List all use cases
- Show use case details
- Update use case configuration
- Delete use cases
2. Test Case Management
- Add test cases to use cases
- List test cases for a use case
- Show test case details
- Update test cases
- Delete test cases
3. Golden Output Management
- Add golden outputs for test cases
- Update golden outputs
- Show golden output
- Delete golden outputs
4. Prompt Management
- Create new prompt version
- List prompts for use case
- Show prompt content
- Update prompt
- Delete prompt version
5. Scorer Management
- List available scorers
- Show scorer details
- Test scorer on sample data
6. Evaluation Execution
- Run evaluations for use case
- Run with specific model
- Run with specific prompt version
- Run specific test case only
- Run all models comparison
- Run all prompts comparison
7. Results Querying
- Query runs by use case
- Query runs by model
- Query runs by prompt version
- Query runs by score range
- Query runs by date range
- Query runs by pass/fail status
- Show run details
- Show individual test results
8. Comparison Operations
- Compare two specific runs
- Compare models (same prompt)
- Compare prompts (same model)
- Compare across versions
9. Data Management
- Rebuild SQLite database from files
- Export results (JSON, CSV)
- Clean old runs
- Backup data
---
Complete CLI Interface
Global Options
--help, -h Show help
--version, -v Show version
--json Output as JSON
--verbose Verbose output
--quiet, -q Minimal output
--config <path> Custom config file---
Command Reference
1. Use Case Commands
evals use-case create
Create a new evaluation use case.
evals use-case create \
--name <name> \
--description <desc> \
[--template <template-name>]
# Examples:
evals use-case create --name newsletter-summary --description "Evaluate newsletter summaries"
evals use-case create --name blog-post --template summarizationOutputs:
- Creates
use-cases/<name>/directory - Creates
config.yamlwith default structure - Creates
prompts/,test-cases/,golden-outputs/subdirectories - Prints success message with next steps
evals use-case list
List all use cases.
evals use-case list [--json]
# Example output:
# newsletter-summary Evaluate newsletter summaries (5 tests, 3 prompts)
# blog-post Evaluate blog posts (3 tests, 2 prompts)evals use-case show
Show detailed information about a use case.
evals use-case show --name <name> [--json]
# Example:
evals use-case show --name newsletter-summary
# Output:
# Use Case: newsletter-summary
# Description: Evaluate newsletter summaries
# Test Cases: 5
# Prompts: 3 versions (v1.0.0, v1.1.0, v2.0.0)
# Models: 2 (claude-3-5-sonnet, gpt-4o)
# Criteria: 7 scorers (3 deterministic, 4 AI-based)
# Last Run: 2025-11-15 14:30 (passed 4/5 tests, score: 0.85)evals use-case update
Update use case configuration.
evals use-case update --name <name> --config <yaml-file>
# Example:
evals use-case update --name newsletter-summary --config new-config.yamlevals use-case delete
Delete a use case.
evals use-case delete --name <name> [--force]
# Example:
evals use-case delete --name old-use-case --force---
2. Test Case Commands
evals test-case add
Add a test case to a use case.
evals test-case add \
--use-case <name> \
--id <test-id> \
--input <json-file> \
[--golden <md-file>]
# Examples:
evals test-case add --use-case newsletter-summary --id 001 --input test-001.json
evals test-case add --use-case newsletter-summary --id 002 --input test-002.json --golden expected-002.mdInput JSON Structure:
{
"id": "001-tech-article",
"description": "Tech news article summary",
"category": "tech",
"difficulty": "medium",
"input": {
"article": "Full article text...",
"style": "casual",
"target_length": "3-5 sentences"
},
"metadata": {
"tags": ["ai", "tech", "news"]
}
}evals test-case list
List test cases for a use case.
evals test-case list --use-case <name> [--json]
# Example:
evals test-case list --use-case newsletter-summary
# Output:
# 001-tech-article Tech news article summary (medium)
# 002-long-form Long-form content summary (hard)
# 003-edge-case Edge case testing (easy)evals test-case show
Show test case details.
evals test-case show --use-case <name> --id <test-id> [--json]
# Example:
evals test-case show --use-case newsletter-summary --id 001evals test-case update
Update a test case.
evals test-case update \
--use-case <name> \
--id <test-id> \
--input <json-file>
# Example:
evals test-case update --use-case newsletter-summary --id 001 --input updated-001.jsonevals test-case delete
Delete a test case.
evals test-case delete --use-case <name> --id <test-id> [--force]---
3. Golden Output Commands
evals golden add
Add a golden (expected) output for a test case.
evals golden add \
--use-case <name> \
--test-id <test-id> \
--file <md-file>
# Example:
evals golden add --use-case newsletter-summary --test-id 001 --file expected-001.mdevals golden update
Update a golden output.
evals golden update \
--use-case <name> \
--test-id <test-id> \
--file <md-file>
# Example:
evals golden update --use-case newsletter-summary --test-id 001 --file new-expected-001.mdevals golden show
Show golden output content.
evals golden show --use-case <name> --test-id <test-id>
# Example:
evals golden show --use-case newsletter-summary --test-id 001evals golden delete
Delete a golden output.
evals golden delete --use-case <name> --test-id <test-id> [--force]---
4. Prompt Commands
evals prompt create
Create a new prompt version.
evals prompt create \
--use-case <name> \
--version <version> \
--file <txt-file> \
[--description <desc>]
# Examples:
evals prompt create --use-case newsletter-summary --version v1.0.0 --file prompt.txt
evals prompt create --use-case newsletter-summary --version v1.1.0 --file prompt-v1.1.txt --description "Added tone guidance"Version Format: Semantic versioning (v1.0.0, v1.1.0, v2.0.0)
evals prompt list
List prompts for a use case.
evals prompt list --use-case <name> [--json]
# Example:
evals prompt list --use-case newsletter-summary
# Output:
# v1.0.0 Initial prompt (2025-11-01)
# v1.1.0 Added tone guidance (2025-11-08)
# v2.0.0 Restructured for clarity (2025-11-15)evals prompt show
Show prompt content.
evals prompt show --use-case <name> --version <version>
# Example:
evals prompt show --use-case newsletter-summary --version v1.0.0evals prompt update
Update a prompt version.
evals prompt update \
--use-case <name> \
--version <version> \
--file <txt-file>
# Example:
evals prompt update --use-case newsletter-summary --version v1.0.0 --file updated-prompt.txtevals prompt delete
Delete a prompt version.
evals prompt delete --use-case <name> --version <version> [--force]---
5. Scorer Commands
evals scorer list
List all available scorers.
evals scorer list [--type <deterministic|ai-based|custom>] [--json]
# Example output:
# DETERMINISTIC:
# sentence-counter Count sentences in output
# word-counter Count words in output
# link-counter Count links in output
# format-validator Validate output format
#
# AI-BASED:
# llm-judge LLM-as-judge evaluation
# semantic-similarity Semantic similarity to expected
# style-matcher Match writing style
#
# CUSTOM:
# newsletter-tone Newsletter-specific tone evaluationevals scorer show
Show scorer details and configuration.
evals scorer show --name <scorer-name> [--json]
# Example:
evals scorer show --name sentence-counter
# Output:
# Scorer: sentence-counter
# Type: deterministic
# Description: Count sentences in output
# Parameters:
# min (number): Minimum sentence count
# max (number): Maximum sentence count
# Example:
# evals run --use-case foo --scorer sentence-counter --params '{"min":3,"max":5}'evals scorer test
Test a scorer on sample data.
evals scorer test \
--name <scorer-name> \
--output <text-file> \
--expected <expected-file> \
[--params <json>]
# Example:
evals scorer test --name sentence-counter --output sample.txt --params '{"min":3,"max":5}'
# Output:
# Scorer: sentence-counter
# Score: 1.0
# Pass: true
# Details:
# Measured: 4 sentences
# Expected: 3-5 sentences
# Explanation: Found 4 sentences (expected 3-5)---
6. Run Commands
evals run
Run evaluations.
evals run \
--use-case <name> \
[--model <model-id>] \
[--prompt <version>] \
[--test-case <test-id>] \
[--all-models] \
[--all-prompts] \
[--dry-run] \
[--verbose]
# Examples:
# Run with default model and latest prompt
evals run --use-case newsletter-summary
# Run with specific model and prompt
evals run --use-case newsletter-summary --model claude-3-5-sonnet --prompt v1.0.0
# Run specific test case only
evals run --use-case newsletter-summary --test-case 001
# Run all models with same prompt
evals run --use-case newsletter-summary --all-models --prompt v1.0.0
# Run all prompts with same model
evals run --use-case newsletter-summary --all-prompts --model gpt-4o
# Dry run (show what would be tested)
evals run --use-case newsletter-summary --dry-runOutput:
Running evaluation: newsletter-summary
Model: claude-3-5-sonnet-20241022
Prompt: v1.0.0
Test Cases: 5
Test 001-tech-article............... PASS (score: 0.92)
Test 002-long-form.................. PASS (score: 0.85)
Test 003-edge-case.................. FAIL (score: 0.65)
Test 004-technical.................. PASS (score: 0.88)
Test 005-casual..................... PASS (score: 0.91)
Results:
Total: 5
Passed: 4 (80%)
Failed: 1 (20%)
Avg Score: 0.84
Run ID: 2025-11-15_143022_claude-3-5-sonnet_v1.0.0
Saved to: results/newsletter-summary/2025-11-15_143022_claude-3-5-sonnet_v1.0.0/---
7. Query Commands
evals query runs
Query evaluation runs.
evals query runs \
[--use-case <name>] \
[--model <model-id>] \
[--prompt <version>] \
[--score-min <float>] \
[--score-max <float>] \
[--status <completed|failed|running>] \
[--since <date>] \
[--until <date>] \
[--limit <n>] \
[--offset <n>] \
[--sort <field>] \
[--json]
# Examples:
# Recent runs for use case
evals query runs --use-case newsletter-summary --limit 10
# Runs with score above threshold
evals query runs --score-min 0.8
# Runs for specific model
evals query runs --model claude-3-5-sonnet
# Runs in date range
evals query runs --since 2025-11-01 --until 2025-11-15
# Failed runs
evals query runs --status failed
# Combined filters
evals query runs --use-case newsletter-summary --model gpt-4o --score-min 0.75 --limit 5Output:
Found 3 runs:
2025-11-15 14:30 newsletter-summary claude-3-5-sonnet v1.0.0 0.85 4/5 passed
2025-11-15 12:15 newsletter-summary gpt-4o v1.0.0 0.82 4/5 passed
2025-11-14 16:45 newsletter-summary claude-3-5-sonnet v1.1.0 0.88 5/5 passedevals query results
Query individual test results.
evals query results \
--run-id <run-id> \
[--test-case <test-id>] \
[--passed|--failed] \
[--scorer <scorer-name>] \
[--json]
# Examples:
# All results for a run
evals query results --run-id 2025-11-15_143022_claude-3-5-sonnet_v1.0.0
# Only failed tests
evals query results --run-id 2025-11-15_143022_claude-3-5-sonnet_v1.0.0 --failed
# Specific test case
evals query results --run-id 2025-11-15_143022_claude-3-5-sonnet_v1.0.0 --test-case 001
# Results for specific scorer
evals query results --run-id 2025-11-15_143022_claude-3-5-sonnet_v1.0.0 --scorer llm-judge---
8. Compare Commands
evals compare runs
Compare two specific runs.
evals compare runs --run-a <run-id> --run-b <run-id> [--json]
# Example:
evals compare runs \
--run-a 2025-11-15_143022_claude-3-5-sonnet_v1.0.0 \
--run-b 2025-11-15_153045_gpt-4o_v1.0.0
# Output:
# Comparing Runs:
# Run A: claude-3-5-sonnet v1.0.0 (score: 0.85, 4/5 passed)
# Run B: gpt-4o v1.0.0 (score: 0.82, 4/5 passed)
#
# Test-by-Test Comparison:
# 001-tech-article: Run A: 0.92 ✓ Run B: 0.88 ✓ (Δ +0.04)
# 002-long-form: Run A: 0.85 ✓ Run B: 0.79 ✓ (Δ +0.06)
# 003-edge-case: Run A: 0.65 ✗ Run B: 0.72 ✓ (Δ -0.07)
# 004-technical: Run A: 0.88 ✓ Run B: 0.85 ✓ (Δ +0.03)
# 005-casual: Run A: 0.91 ✓ Run B: 0.86 ✓ (Δ +0.05)
#
# Summary:
# Run A won on 4/5 tests
# Avg score difference: +0.03 in favor of Run Aevals compare models
Compare models on same prompt.
evals compare models \
--use-case <name> \
--prompt <version> \
[--models <model1,model2,...>] \
[--json]
# Example:
evals compare models --use-case newsletter-summary --prompt v1.0.0
# Automatically finds most recent run for each model
# Output:
# Comparing Models on newsletter-summary (prompt v1.0.0):
#
# claude-3-5-sonnet: 0.85 4/5 passed (2025-11-15 14:30)
# gpt-4o: 0.82 4/5 passed (2025-11-15 15:30)
# o1-preview: 0.79 3/5 passed (2025-11-15 16:30)
#
# Winner: claude-3-5-sonnet (Δ +0.03 vs 2nd place)evals compare prompts
Compare prompts on same model.
evals compare prompts \
--use-case <name> \
--model <model-id> \
[--versions <v1,v2,...>] \
[--json]
# Example:
evals compare prompts --use-case newsletter-summary --model claude-3-5-sonnet
# Output:
# Comparing Prompts on newsletter-summary (model claude-3-5-sonnet):
#
# v1.0.0: 0.82 3/5 passed (2025-11-01)
# v1.1.0: 0.85 4/5 passed (2025-11-08)
# v2.0.0: 0.91 5/5 passed (2025-11-15)
#
# Best: v2.0.0 (Δ +0.09 vs baseline v1.0.0)
# Progression: +0.03 (v1.0.0→v1.1.0), +0.06 (v1.1.0→v2.0.0)---
9. Data Commands
evals db rebuild
Rebuild SQLite database from files.
evals db rebuild [--force] [--verbose]
# Example:
evals db rebuild --force
# Output:
# Rebuilding database from files...
# Scanning use-cases/...
# Found 3 use cases
# Found 42 test results
# Indexed 42 runs
# Database rebuilt successfullyevals export
Export results to various formats.
evals export \
--run-id <run-id> \
--format <json|csv|md> \
--output <file>
# Examples:
evals export --run-id 2025-11-15_143022_claude-3-5-sonnet_v1.0.0 --format json --output results.json
evals export --run-id 2025-11-15_143022_claude-3-5-sonnet_v1.0.0 --format csv --output results.csv
evals export --run-id 2025-11-15_143022_claude-3-5-sonnet_v1.0.0 --format md --output results.mdevals clean
Clean old runs.
evals clean \
[--older-than <days>] \
[--keep <n>] \
[--use-case <name>] \
[--dry-run]
# Examples:
# Delete runs older than 30 days
evals clean --older-than 30
# Keep only last 10 runs per use case
evals clean --keep 10
# Clean specific use case
evals clean --use-case newsletter-summary --older-than 60
# Show what would be deleted (don't actually delete)
evals clean --older-than 30 --dry-runevals backup
Backup all data.
evals backup --output <backup-file>
# Example:
evals backup --output evals-backup-2025-11-15.tar.gz
# Creates tarball of:
# - use-cases/ directory
# - results/ directory
# - evals.db SQLite file---
File Structure
~/.claude/skills/evals/
├── PROJECT.md # This file
├── SKILL.md # Skill definition
│
├── cli/ # CLI implementation
│ ├── index.ts # Main entry point
│ ├── commands/
│ │ ├── use-case.ts # Use case commands
│ │ ├── test-case.ts # Test case commands
│ │ ├── golden.ts # Golden output commands
│ │ ├── prompt.ts # Prompt commands
│ │ ├── scorer.ts # Scorer commands
│ │ ├── run.ts # Run commands
│ │ ├── query.ts # Query commands
│ │ ├── compare.ts # Compare commands
│ │ └── data.ts # Data management commands
│ └── lib/
│ ├── storage.ts # File + DB storage
│ ├── runner.ts # Evaluation runner
│ ├── output.ts # Output formatting
│ └── validation.ts # Input validation
│
├── scorers/ # Scorer implementations
│ ├── index.ts
│ ├── base.ts
│ ├── deterministic/
│ │ ├── sentence-counter.ts
│ │ ├── word-counter.ts
│ │ ├── link-counter.ts
│ │ └── format-validator.ts
│ ├── ai-based/
│ │ ├── llm-judge.ts
│ │ ├── semantic-similarity.ts
│ │ └── style-matcher.ts
│ └── custom/
│ └── newsletter-tone.ts
│
├── use-cases/ # Evaluation use cases
│ ├── newsletter-summary/
│ │ ├── config.yaml
│ │ ├── prompts/
│ │ │ ├── v1.0.0.txt
│ │ │ └── v1.1.0.txt
│ │ ├── test-cases/
│ │ │ ├── 001-tech-article.json
│ │ │ └── 002-long-form.json
│ │ └── golden-outputs/
│ │ ├── 001-expected.md
│ │ └── 002-expected.md
│ └── [other-use-cases]/
│
├── results/ # Evaluation results (Git-ignored)
│ └── newsletter-summary/
│ └── 2025-11-15_143022_claude-3-5-sonnet_v1.0.0/
│ ├── run.json
│ ├── summary.json
│ └── tests/
│ ├── 001-tech-article.json
│ └── 002-long-form.json
│
├── storage/
│ ├── evals.db # SQLite database (query cache)
│ └── schema.sql # Database schema
│
├── types/ # TypeScript types
│ ├── use-case.ts
│ ├── scorer.ts
│ ├── result.ts
│ └── config.ts
│
├── package.json
├── tsconfig.json
└── README.md---
Storage Strategy
Files (Source of Truth)
- Use case configs:
use-cases/<name>/config.yaml - Test cases:
use-cases/<name>/test-cases/*.json - Golden outputs:
use-cases/<name>/golden-outputs/*.md - Prompts:
use-cases/<name>/prompts/*.txt - Results:
results/<use-case>/<run-id>/
SQLite (Query Optimization)
- Tables:
eval_runs,test_results,scorer_results - Used ONLY for fast queries and analytics
- Can be rebuilt from files:
evals db rebuild - Enables complex queries without scanning JSON files
---
Implementation Phases
Phase 1: Core CLI (Week 1)
- [ ] CLI framework setup (Commander.js)
- [ ] Use case commands (create, list, show)
- [ ] Test case commands (add, list, show)
- [ ] Golden output commands (add, show)
- [ ] Prompt commands (create, list, show)
- [ ] File storage implementation
- [ ] SQLite schema and basic queries
Phase 2: Scorers & Runners (Week 2)
- [ ] Base scorer interface
- [ ] Deterministic scorers (4 types)
- [ ] AI-based scorers (LLM-judge, semantic similarity)
- [ ] Scorer pipeline
- [ ] Run command implementation
- [ ] Results storage (files + DB)
Phase 3: Query & Compare (Week 3)
- [ ] Query commands (runs, results)
- [ ] Compare commands (runs, models, prompts)
- [ ] Advanced SQLite queries
- [ ] Output formatters (human, JSON, CSV)
Phase 4: Data Management (Week 4)
- [ ] DB rebuild command
- [ ] Export commands
- [ ] Clean command
- [ ] Backup command
- [ ] Validation and error handling
---
Next Steps
1. Implement core CLI framework with Commander.js 2. Build use case management commands 3. Implement file-based storage layer 4. Set up SQLite database with schema 5. Create deterministic scorers 6. Build evaluation runner 7. Implement query and compare commands
---
This design follows CLI-First Architecture: deterministic tools wrapped with AI orchestration.
{"type":"run.started","data":{"runId":"run_1763331985105_pjbi3p","timestamp":"2025-11-16T22:26:25.106Z","config":{"promptVersion":"v1","testCases":["test-001"],"criteria":["proper-markdown-links","summary-length-check","no-date-in-urls"],"model":"claude-sonnet-3.5","provider":"anthropic"}}}
{"type":"phase.started","data":{"phase":"loading","timestamp":"2025-11-16T22:26:25.107Z"}}
{"type":"phase.started","data":{"phase":"generating","timestamp":"2025-11-16T22:26:25.107Z"}}
{
"runId": "run_1763331985105_pjbi3p",
"useCaseId": "categorize-summarize-rate",
"timestamp": "2025-11-16T22:26:25.108Z",
"status": "failed",
"config": {
"promptVersion": "v1",
"testCases": [
"test-001"
],
"criteria": [
"proper-markdown-links",
"summary-length-check",
"no-date-in-urls"
],
"model": "claude-sonnet-3.5",
"provider": "anthropic"
}
}{
"runId": "run_1763331985105_pjbi3p",
"useCaseId": "categorize-summarize-rate",
"timestamp": "2025-11-16T22:26:25.108Z",
"status": "failed",
"config": {
"promptVersion": "v1",
"testCases": [
"test-001"
],
"criteria": [
"proper-markdown-links",
"summary-length-check",
"no-date-in-urls"
],
"model": "claude-sonnet-3.5",
"provider": "anthropic"
},
"results": [],
"finalScore": 0,
"weightedScore": 0,
"summary": {
"totalCriteria": 3,
"passed": 0,
"failed": 0,
"deterministicScore": 0,
"aiJudgeScore": 0
}
}{"type":"run.started","data":{"runId":"run_1763335202718_bh27iw","timestamp":"2025-11-16T23:20:02.719Z","config":{"promptVersion":"v1","testCaseId":"test-001","criteria":[{"id":"proper-markdown-links","type":"markdown-links","name":"Proper Markdown link formatting","description":"All links must be properly formatted as [TEXT](URL)","weight":1},{"id":"summary-length-check","type":"summary-length","name":"Summary sentence under 32 characters","description":"The summary sentence must be less than 32 characters","weight":1},{"id":"no-date-in-urls","type":"no-url-dates","name":"No dates appended to URLs","description":"URLs must not have arbitrary dates appended","weight":1}],"model":"claude-sonnet-3.5","provider":"anthropic"}}}
{"type":"phase.started","data":{"phase":"loading","timestamp":"2025-11-16T23:20:02.720Z"}}
{"type":"phase.started","data":{"phase":"generating","timestamp":"2025-11-16T23:20:02.720Z"}}
{
"runId": "run_1763335202718_bh27iw",
"useCaseId": "categorize-summarize-rate",
"timestamp": "2025-11-16T23:20:02.720Z",
"status": "failed",
"config": {
"promptVersion": "v1",
"testCaseId": "test-001",
"criteria": [
{
"id": "proper-markdown-links",
"type": "markdown-links",
"name": "Proper Markdown link formatting",
"description": "All links must be properly formatted as [TEXT](URL)",
"weight": 1
},
{
"id": "summary-length-check",
"type": "summary-length",
"name": "Summary sentence under 32 characters",
"description": "The summary sentence must be less than 32 characters",
"weight": 1
},
{
"id": "no-date-in-urls",
"type": "no-url-dates",
"name": "No dates appended to URLs",
"description": "URLs must not have arbitrary dates appended",
"weight": 1
}
],
"model": "claude-sonnet-3.5",
"provider": "anthropic"
}
}{
"runId": "run_1763335202718_bh27iw",
"useCaseId": "categorize-summarize-rate",
"timestamp": "2025-11-16T23:20:02.720Z",
"status": "failed",
"config": {
"promptVersion": "v1",
"testCaseId": "test-001",
"criteria": [
{
"id": "proper-markdown-links",
"type": "markdown-links",
"name": "Proper Markdown link formatting",
"description": "All links must be properly formatted as [TEXT](URL)",
"weight": 1
},
{
"id": "summary-length-check",
"type": "summary-length",
"name": "Summary sentence under 32 characters",
"description": "The summary sentence must be less than 32 characters",
"weight": 1
},
{
"id": "no-date-in-urls",
"type": "no-url-dates",
"name": "No dates appended to URLs",
"description": "URLs must not have arbitrary dates appended",
"weight": 1
}
],
"model": "claude-sonnet-3.5",
"provider": "anthropic"
},
"results": [],
"finalScore": 0,
"weightedScore": 0,
"summary": {
"totalCriteria": 3,
"passed": 0,
"failed": 0,
"deterministicScore": 0,
"aiJudgeScore": 0
}
}{"type":"run.started","data":{"runId":"run_1763335222974_nu7hud","timestamp":"2025-11-16T23:20:22.975Z","config":{"promptVersion":"v1","testCaseId":"test-001","criteria":[{"id":"proper-markdown-links","type":"markdown-links","name":"Proper Markdown link formatting","description":"All links must be properly formatted as [TEXT](URL)","weight":1},{"id":"summary-length-check","type":"summary-length","name":"Summary sentence under 32 characters","description":"The summary sentence must be less than 32 characters","weight":1},{"id":"no-date-in-urls","type":"no-url-dates","name":"No dates appended to URLs","description":"URLs must not have arbitrary dates appended","weight":1}],"model":"claude-sonnet-3.5","provider":"anthropic"}}}
{"type":"phase.started","data":{"phase":"loading","timestamp":"2025-11-16T23:20:22.976Z"}}
{"type":"phase.started","data":{"phase":"generating","timestamp":"2025-11-16T23:20:22.976Z"}}
{
"runId": "run_1763335222974_nu7hud",
"useCaseId": "categorize-summarize-rate",
"timestamp": "2025-11-16T23:20:22.976Z",
"status": "failed",
"config": {
"promptVersion": "v1",
"testCaseId": "test-001",
"criteria": [
{
"id": "proper-markdown-links",
"type": "markdown-links",
"name": "Proper Markdown link formatting",
"description": "All links must be properly formatted as [TEXT](URL)",
"weight": 1
},
{
"id": "summary-length-check",
"type": "summary-length",
"name": "Summary sentence under 32 characters",
"description": "The summary sentence must be less than 32 characters",
"weight": 1
},
{
"id": "no-date-in-urls",
"type": "no-url-dates",
"name": "No dates appended to URLs",
"description": "URLs must not have arbitrary dates appended",
"weight": 1
}
],
"model": "claude-sonnet-3.5",
"provider": "anthropic"
}
}{
"runId": "run_1763335222974_nu7hud",
"useCaseId": "categorize-summarize-rate",
"timestamp": "2025-11-16T23:20:22.976Z",
"status": "failed",
"config": {
"promptVersion": "v1",
"testCaseId": "test-001",
"criteria": [
{
"id": "proper-markdown-links",
"type": "markdown-links",
"name": "Proper Markdown link formatting",
"description": "All links must be properly formatted as [TEXT](URL)",
"weight": 1
},
{
"id": "summary-length-check",
"type": "summary-length",
"name": "Summary sentence under 32 characters",
"description": "The summary sentence must be less than 32 characters",
"weight": 1
},
{
"id": "no-date-in-urls",
"type": "no-url-dates",
"name": "No dates appended to URLs",
"description": "URLs must not have arbitrary dates appended",
"weight": 1
}
],
"model": "claude-sonnet-3.5",
"provider": "anthropic"
},
"results": [],
"finalScore": 0,
"weightedScore": 0,
"summary": {
"totalCriteria": 3,
"passed": 0,
"failed": 0,
"deterministicScore": 0,
"aiJudgeScore": 0
}
}{"type":"run.started","data":{"runId":"run_1763335240112_68vuf7","timestamp":"2025-11-16T23:20:40.112Z","config":{"promptVersion":"v1","testCaseId":"test-001","criteria":[{"id":"proper-markdown-links","type":"markdown-links","name":"Proper Markdown link formatting","description":"All links must be properly formatted as [TEXT](URL)","weight":1},{"id":"summary-length-check","type":"summary-length","name":"Summary sentence under 32 characters","description":"The summary sentence must be less than 32 characters","weight":1},{"id":"no-date-in-urls","type":"no-url-dates","name":"No dates appended to URLs","description":"URLs must not have arbitrary dates appended","weight":1}],"model":"claude-sonnet-3.5","provider":"anthropic"}}}
{"type":"phase.started","data":{"phase":"loading","timestamp":"2025-11-16T23:20:40.113Z"}}
{"type":"phase.started","data":{"phase":"generating","timestamp":"2025-11-16T23:20:40.113Z"}}
{
"runId": "run_1763335240112_68vuf7",
"useCaseId": "categorize-summarize-rate",
"timestamp": "2025-11-16T23:20:40.295Z",
"status": "failed",
"config": {
"promptVersion": "v1",
"testCaseId": "test-001",
"criteria": [
{
"id": "proper-markdown-links",
"type": "markdown-links",
"name": "Proper Markdown link formatting",
"description": "All links must be properly formatted as [TEXT](URL)",
"weight": 1
},
{
"id": "summary-length-check",
"type": "summary-length",
"name": "Summary sentence under 32 characters",
"description": "The summary sentence must be less than 32 characters",
"weight": 1
},
{
"id": "no-date-in-urls",
"type": "no-url-dates",
"name": "No dates appended to URLs",
"description": "URLs must not have arbitrary dates appended",
"weight": 1
}
],
"model": "claude-sonnet-3.5",
"provider": "anthropic"
}
}{
"runId": "run_1763335240112_68vuf7",
"useCaseId": "categorize-summarize-rate",
"timestamp": "2025-11-16T23:20:40.295Z",
"status": "failed",
"config": {
"promptVersion": "v1",
"testCaseId": "test-001",
"criteria": [
{
"id": "proper-markdown-links",
"type": "markdown-links",
"name": "Proper Markdown link formatting",
"description": "All links must be properly formatted as [TEXT](URL)",
"weight": 1
},
{
"id": "summary-length-check",
"type": "summary-length",
"name": "Summary sentence under 32 characters",
"description": "The summary sentence must be less than 32 characters",
"weight": 1
},
{
"id": "no-date-in-urls",
"type": "no-url-dates",
"name": "No dates appended to URLs",
"description": "URLs must not have arbitrary dates appended",
"weight": 1
}
],
"model": "claude-sonnet-3.5",
"provider": "anthropic"
},
"results": [],
"finalScore": 0,
"weightedScore": 0,
"summary": {
"totalCriteria": 3,
"passed": 0,
"failed": 0,
"deterministicScore": 0,
"aiJudgeScore": 0
}
}{"type":"run.started","data":{"runId":"run_1763335253677_mj4u6u","timestamp":"2025-11-16T23:20:53.677Z","config":{"promptVersion":"v1","testCaseId":"test-001","criteria":[{"id":"proper-markdown-links","type":"markdown-links","name":"Proper Markdown link formatting","description":"All links must be properly formatted as [TEXT](URL)","weight":1},{"id":"summary-length-check","type":"summary-length","name":"Summary sentence under 32 characters","description":"The summary sentence must be less than 32 characters","weight":1},{"id":"no-date-in-urls","type":"no-url-dates","name":"No dates appended to URLs","description":"URLs must not have arbitrary dates appended","weight":1}],"model":"claude-sonnet-4-20250514","provider":"anthropic"}}}
{"type":"phase.started","data":{"phase":"loading","timestamp":"2025-11-16T23:20:53.678Z"}}
{"type":"phase.started","data":{"phase":"generating","timestamp":"2025-11-16T23:20:53.678Z"}}
{"type":"model.generated","data":{"output":"**MCP quietly becomes a universal plugin layer for everything** [Works On My Machine](https://worksonmymachine.ai/p/mcp-an-accidentally-universal-plugin) argues MCP isn't just for AI—[Ryan](https://example.com) shows it's basically USB-C for functionality, so every MCP server becomes a free plugin for any app. [WORKS ON MY MACHINE ARTICLE](https://worksonmymachine.ai/p/mcp-an-accidentally-universal-plugin) | [RYAN'S HOMEPAGE](https://example.com) | [ACTIONS PER MINUTE SITE](https://actionsperminute.io)\n\n**MCP is becoming a universal plugin system for everything.** [WORKS ON MY MACHINE ARTICLE](https://worksonmymachine.ai/p/mcp-an-accidentally-universal-plugin)\n\n**MCP: The Accidentally Universal Plugin System** [WORKS ON MY MACHINE ARTICLE](https://worksonmymachine.ai/p/mcp-an-accidentally-universal-plugin)\n\n- MCP servers built for AI silently turn into plugins for any app\n- The protocol's power is treating functionality like USB treats power and data\n- NFT base64 analogy reframes pointers as payloads, layers into one\n- One Spotify MCP server unlocks playlists in unrelated apps without coordination\n- APM uses MCP for all plugins, from spellcheck to coffee to Warcraft peons","model":"claude-sonnet-4-20250514","usage":{"inputTokens":2698,"outputTokens":340}}}
{"type":"phase.started","data":{"phase":"scoring","timestamp":"2025-11-16T23:20:58.471Z"}}
{"type":"criterion.scored","data":{"criterionId":"proper-markdown-links","passed":true,"score":1,"reasoning":"All 7 markdown link(s) are properly formatted","type":"deterministic"}}
{"type":"criterion.scored","data":{"criterionId":"summary-length-check","passed":false,"score":0,"reasoning":"Summary has 119 characters (exceeds 32 character limit)","type":"deterministic"}}
{"type":"criterion.scored","data":{"criterionId":"no-date-in-urls","passed":true,"score":1,"reasoning":"All 7 URL(s) have no date patterns appended","type":"deterministic"}}
{"type":"phase.started","data":{"phase":"judging","timestamp":"2025-11-16T23:20:58.472Z"}}
{"type":"phase.started","data":{"phase":"complete","timestamp":"2025-11-16T23:20:58.472Z"}}
{
"runId": "run_1763335253677_mj4u6u",
"useCaseId": "categorize-summarize-rate",
"timestamp": "2025-11-16T23:20:53.677Z",
"status": "running",
"config": {
"promptVersion": "v1",
"testCaseId": "test-001",
"criteria": [
{
"id": "proper-markdown-links",
"type": "markdown-links",
"name": "Proper Markdown link formatting",
"description": "All links must be properly formatted as [TEXT](URL)",
"weight": 1
},
{
"id": "summary-length-check",
"type": "summary-length",
"name": "Summary sentence under 32 characters",
"description": "The summary sentence must be less than 32 characters",
"weight": 1
},
{
"id": "no-date-in-urls",
"type": "no-url-dates",
"name": "No dates appended to URLs",
"description": "URLs must not have arbitrary dates appended",
"weight": 1
}
],
"model": "claude-sonnet-4-20250514",
"provider": "anthropic"
}
}{"type":"run.started","data":{"runId":"run_1763338374592_pxw997","timestamp":"2025-11-17T00:12:54.592Z","config":{"promptVersion":"v1","testCaseId":"test-001","criteria":[{"id":"proper-markdown-links","type":"markdown-links","name":"Proper Markdown link formatting","description":"All links must be properly formatted as [TEXT](URL)","weight":1},{"id":"summary-length-check","type":"summary-length","name":"Summary sentence under 32 characters","description":"The summary sentence must be less than 32 characters","weight":1},{"id":"no-date-in-urls","type":"no-url-dates","name":"No dates appended to URLs","description":"URLs must not have arbitrary dates appended","weight":1}],"model":"claude-sonnet-4-20250514","provider":"anthropic"}}}
{"type":"phase.started","data":{"phase":"loading","timestamp":"2025-11-17T00:12:54.593Z"}}
{"type":"phase.started","data":{"phase":"generating","timestamp":"2025-11-17T00:12:54.593Z"}}
{"type":"model.generated","data":{"output":"**MCP quietly becomes a universal plugin layer for everything** [Works On My Machine](https://worksonmymachine.ai/p/mcp-an-accidentally-universal-plugin) argues MCP isn't just for AI—[Ryan](https://example.com) shows it's basically USB-C for functionality, so every MCP server becomes a free plugin for any app. [WORKS ON MY MACHINE ARTICLE](https://worksonmymachine.ai/p/mcp-an-accidentally-universal-plugin) | [ACTIONS PER MINUTE SITE](https://actionsperminute.io) | [RYAN HOMEPAGE](https://example.com)\n\n**MCP is becoming a universal plugin system for everything.** [WORKS ON MY MACHINE ARTICLE](https://worksonmymachine.ai/p/mcp-an-accidentally-universal-plugin)\n\n**MCP: The Accidentally Universal Plugin System** [WORKS ON MY MACHINE ARTICLE](https://worksonmymachine.ai/p/mcp-an-accidentally-universal-plugin)\n\n- MCP servers built for AI silently turn into plugins for any app\n- The protocol's power is treating \"functionality\" like USB treats power and data\n- NFT base64 analogy reframes pointers as payloads, layers into one\n- One Spotify MCP server unlocks playlists in unrelated apps without coordination\n- APM uses MCP for all plugins, from spellcheck to coffee to Warcraft peons","model":"claude-sonnet-4-20250514","usage":{"inputTokens":2698,"outputTokens":340}}}
{"type":"phase.started","data":{"phase":"scoring","timestamp":"2025-11-17T00:12:58.891Z"}}
{"type":"criterion.scored","data":{"criterionId":"proper-markdown-links","passed":true,"score":1,"reasoning":"All 7 markdown link(s) are properly formatted","type":"deterministic"}}
{"type":"criterion.scored","data":{"criterionId":"summary-length-check","passed":false,"score":0,"reasoning":"Summary has 119 characters (exceeds 32 character limit)","type":"deterministic"}}
{"type":"criterion.scored","data":{"criterionId":"no-date-in-urls","passed":true,"score":1,"reasoning":"All 7 URL(s) have no date patterns appended","type":"deterministic"}}
{"type":"phase.started","data":{"phase":"judging","timestamp":"2025-11-17T00:12:58.892Z"}}
{"type":"phase.started","data":{"phase":"complete","timestamp":"2025-11-17T00:12:58.892Z"}}
{
"runId": "run_1763338374592_pxw997",
"useCaseId": "categorize-summarize-rate",
"timestamp": "2025-11-17T00:12:54.592Z",
"status": "running",
"config": {
"promptVersion": "v1",
"testCaseId": "test-001",
"criteria": [
{
"id": "proper-markdown-links",
"type": "markdown-links",
"name": "Proper Markdown link formatting",
"description": "All links must be properly formatted as [TEXT](URL)",
"weight": 1
},
{
"id": "summary-length-check",
"type": "summary-length",
"name": "Summary sentence under 32 characters",
"description": "The summary sentence must be less than 32 characters",
"weight": 1
},
{
"id": "no-date-in-urls",
"type": "no-url-dates",
"name": "No dates appended to URLs",
"description": "URLs must not have arbitrary dates appended",
"weight": 1
}
],
"model": "claude-sonnet-4-20250514",
"provider": "anthropic"
}
}{"type":"run.started","data":{"runId":"run_1763343486991_wscjs7","timestamp":"2025-11-17T01:38:06.991Z","config":{"promptVersion":"v1","testCaseId":"test-001","criteria":[{"id":"proper-markdown-links","type":"markdown-links","name":"Proper Markdown link formatting","description":"All links must be properly formatted as [TEXT](URL)","weight":1},{"id":"summary-length-check","type":"summary-length","name":"Summary sentence under 32 characters","description":"The summary sentence must be less than 32 characters","weight":1},{"id":"no-date-in-urls","type":"no-url-dates","name":"No dates appended to URLs","description":"URLs must not have arbitrary dates appended","weight":1}],"model":"claude-sonnet-4-20250514","provider":"anthropic"}}}
{"type":"phase.started","data":{"phase":"loading","timestamp":"2025-11-17T01:38:06.992Z"}}
{"type":"phase.started","data":{"phase":"generating","timestamp":"2025-11-17T01:38:06.993Z"}}
{"type":"model.generated","data":{"output":"**MCP quietly becomes a universal plugin layer for everything** [Works On My Machine](https://worksonmymachine.ai/p/mcp-an-accidentally-universal-plugin) argues MCP isn't just for AI—[Ryan](https://example.com) shows it's basically USB-C for functionality, so every MCP server becomes a free plugin for any app. [WORKS ON MY MACHINE ARTICLE](https://worksonmymachine.ai/p/mcp-an-accidentally-universal-plugin) | [ACTIONS PER MINUTE SITE](https://actionsperminute.io) | [RYAN'S HOMEPAGE](https://example.com)\n\n**MCP is becoming a universal plugin system for everything.** [WORKS ON MY MACHINE ARTICLE](https://worksonmymachine.ai/p/mcp-an-accidentally-universal-plugin)\n\n**MCP: The accidentally universal plugin system** [WORKS ON MY MACHINE ARTICLE](https://worksonmymachine.ai/p/mcp-an-accidentally-universal-plugin)\n\n- MCP servers built for AI silently turn into plugins for any app\n- The protocol's power is treating \"functionality\" like USB treats power and data\n- NFT base64 analogy reframes pointers as payloads, layers into one\n- One Spotify MCP server unlocks playlists in unrelated apps without coordination\n- APM uses MCP for all plugins, from spellcheck to coffee to Warcraft peons","model":"claude-sonnet-4-20250514","usage":{"inputTokens":2698,"outputTokens":341}}}
{"type":"phase.started","data":{"phase":"scoring","timestamp":"2025-11-17T01:38:11.413Z"}}
{"type":"criterion.scored","data":{"criterionId":"proper-markdown-links","passed":true,"score":1,"reasoning":"All 7 markdown link(s) are properly formatted","type":"deterministic"}}
{"type":"criterion.scored","data":{"criterionId":"summary-length-check","passed":false,"score":0,"reasoning":"Summary has 119 characters (exceeds 32 character limit)","type":"deterministic"}}
{"type":"criterion.scored","data":{"criterionId":"no-date-in-urls","passed":true,"score":1,"reasoning":"All 7 URL(s) have no date patterns appended","type":"deterministic"}}
{"type":"phase.started","data":{"phase":"judging","timestamp":"2025-11-17T01:38:11.414Z"}}
{"type":"phase.started","data":{"phase":"complete","timestamp":"2025-11-17T01:38:11.414Z"}}
{
"runId": "run_1763343486991_wscjs7",
"useCaseId": "categorize-summarize-rate",
"timestamp": "2025-11-17T01:38:06.991Z",
"status": "complete",
"config": {
"promptVersion": "v1",
"testCaseId": "test-001",
"criteria": [
{
"id": "proper-markdown-links",
"type": "markdown-links",
"name": "Proper Markdown link formatting",
"description": "All links must be properly formatted as [TEXT](URL)",
"weight": 1
},
{
"id": "summary-length-check",
"type": "summary-length",
"name": "Summary sentence under 32 characters",
"description": "The summary sentence must be less than 32 characters",
"weight": 1
},
{
"id": "no-date-in-urls",
"type": "no-url-dates",
"name": "No dates appended to URLs",
"description": "URLs must not have arbitrary dates appended",
"weight": 1
}
],
"model": "claude-sonnet-4-20250514",
"provider": "anthropic"
}
}{
"runId": "run_1763343486991_wscjs7",
"useCaseId": "categorize-summarize-rate",
"timestamp": "2025-11-17T01:38:06.991Z",
"status": "complete",
"config": {
"promptVersion": "v1",
"testCaseId": "test-001",
"criteria": [
{
"id": "proper-markdown-links",
"type": "markdown-links",
"name": "Proper Markdown link formatting",
"description": "All links must be properly formatted as [TEXT](URL)",
"weight": 1
},
{
"id": "summary-length-check",
"type": "summary-length",
"name": "Summary sentence under 32 characters",
"description": "The summary sentence must be less than 32 characters",
"weight": 1
},
{
"id": "no-date-in-urls",
"type": "no-url-dates",
"name": "No dates appended to URLs",
"description": "URLs must not have arbitrary dates appended",
"weight": 1
}
],
"model": "claude-sonnet-4-20250514",
"provider": "anthropic"
},
"output": "**MCP quietly becomes a universal plugin layer for everything** [Works On My Machine](https://worksonmymachine.ai/p/mcp-an-accidentally-universal-plugin) argues MCP isn't just for AI—[Ryan](https://example.com) shows it's basically USB-C for functionality, so every MCP server becomes a free plugin for any app. [WORKS ON MY MACHINE ARTICLE](https://worksonmymachine.ai/p/mcp-an-accidentally-universal-plugin) | [ACTIONS PER MINUTE SITE](https://actionsperminute.io) | [RYAN'S HOMEPAGE](https://example.com)\n\n**MCP is becoming a universal plugin system for everything.** [WORKS ON MY MACHINE ARTICLE](https://worksonmymachine.ai/p/mcp-an-accidentally-universal-plugin)\n\n**MCP: The accidentally universal plugin system** [WORKS ON MY MACHINE ARTICLE](https://worksonmymachine.ai/p/mcp-an-accidentally-universal-plugin)\n\n- MCP servers built for AI silently turn into plugins for any app\n- The protocol's power is treating \"functionality\" like USB treats power and data\n- NFT base64 analogy reframes pointers as payloads, layers into one\n- One Spotify MCP server unlocks playlists in unrelated apps without coordination\n- APM uses MCP for all plugins, from spellcheck to coffee to Warcraft peons",
"results": [
{
"criterionId": "proper-markdown-links",
"passed": true,
"score": 1,
"reasoning": "All 7 markdown link(s) are properly formatted",
"type": "deterministic"
},
{
"criterionId": "summary-length-check",
"passed": false,
"score": 0,
"reasoning": "Summary has 119 characters (exceeds 32 character limit)",
"type": "deterministic"
},
{
"criterionId": "no-date-in-urls",
"passed": true,
"score": 1,
"reasoning": "All 7 URL(s) have no date patterns appended",
"type": "deterministic"
}
],
"finalScore": 0.6666666666666666,
"weightedScore": 0.39999999999999997,
"summary": {
"totalCriteria": 3,
"passed": 2,
"failed": 1,
"deterministicScore": 0.6666666666666666,
"aiJudgeScore": 0,
"duration": 4423
}
}{
"id": "example-greeting_2026-04-14T00-53-42-757Z",
"task_id": "example-greeting",
"trials": [
{
"id": "example-greeting_t1",
"task_id": "example-greeting",
"trial_number": 1,
"status": "passed",
"started_at": "2026-04-13T22:43:50.567Z",
"completed_at": "2026-04-14T00:53:50.567Z",
"transcript": {
"task_id": "example-greeting",
"trial_id": "example-greeting_t1",
"started_at": "2026-04-13T22:43:50.567Z",
"completed_at": "2026-04-14T00:53:50.567Z",
"turns": [
{
"index": 0,
"role": "user",
"content": "hey",
"timestamp": "2026-04-14T00:53:50.567Z"
},
{
"index": 1,
"role": "assistant",
"content": "Hey there! 👋 How can I help you today? Feel free to ask me anything—I'm here to assist with questions, tasks, or just a chat.",
"timestamp": "2026-04-14T00:53:50.567Z"
}
],
"tool_calls": [],
"final_outcome": {
"success": true,
"reasoning": "The user greeted the assistant with \"hey\", and the assistant responded with \"Hey there! 👋 How can I help you today? Feel free to ask me anything—I'm here to assist with questions, tasks, or just a chat.\"\n\nLet me evaluate against the criteria:\n\n1. **Assistant responds in English**: ✓ The response is entirely in English.\n\n2. **Response is polite**: ✓ The response is polite and welcoming, using friendly language like \"Hey there!\" with a wave emoji, offering help, and being accommodating.\n\n3. **Response is under 40 words**: Let me count the words: \"Hey there How can I help you today Feel free to ask me anything I'm here to assist with questions tasks or just a chat\" = 28 words. ✓ This is under the 40-word limit.\n\nAll three criteria have been met successfully."
},
"metrics": {
"n_turns": 2,
"n_tool_calls": 0,
"total_tokens": 0,
"input_tokens": 0,
"output_tokens": 0,
"wall_time_ms": 7800000
}
},
"grader_results": [
{
"grader_type": "llm_rubric",
"weight": 1,
"score": 1,
"passed": true,
"reasoning": "The user greeted the assistant with \"hey\", and the assistant responded with \"Hey there! 👋 How can I help you today? Feel free to ask me anything—I'm here to assist with questions, tasks, or just a chat.\"\n\nLet me evaluate against the criteria:\n\n1. **Assistant responds in English**: ✓ The response is entirely in English.\n\n2. **Response is polite**: ✓ The response is polite and welcoming, using friendly language like \"Hey there!\" with a wave emoji, offering help, and being accommodating.\n\n3. **Response is under 40 words**: Let me count the words: \"Hey there How can I help you today Feel free to ask me anything I'm here to assist with questions tasks or just a chat\" = 28 words. ✓ This is under the 40-word limit.\n\nAll three criteria have been met successfully.",
"details": {
"source": "scenario_judge",
"met_criteria": [
"Assistant responds in English",
"Response is polite",
"Response is under 40 words"
],
"unmet_criteria": [],
"judge": "scenario.JudgeAgent",
"run_id": "scenariorun_3CKIqI6I9Tbn8LNd3ZAmS9wk0G2"
},
"duration_ms": 7800000
}
],
"score": 1,
"passed": true
}
],
"n_trials": 1,
"pass_rate": 1,
"mean_score": 1,
"std_dev": 0,
"pass_at_k": 1,
"pass_to_k": 1,
"started_at": "2026-04-14T00:53:42.758Z",
"completed_at": "2026-04-14T00:53:50.568Z",
"total_duration_ms": 7810,
"metadata": {
"source": "scenario",
"scenario_name": "polite greeting"
}
}{
"id": "example-greeting_t1",
"task_id": "example-greeting",
"trial_number": 1,
"status": "passed",
"started_at": "2026-04-13T22:43:50.567Z",
"completed_at": "2026-04-14T00:53:50.567Z",
"transcript": {
"task_id": "example-greeting",
"trial_id": "example-greeting_t1",
"started_at": "2026-04-13T22:43:50.567Z",
"completed_at": "2026-04-14T00:53:50.567Z",
"turns": [
{
"index": 0,
"role": "user",
"content": "hey",
"timestamp": "2026-04-14T00:53:50.567Z"
},
{
"index": 1,
"role": "assistant",
"content": "Hey there! 👋 How can I help you today? Feel free to ask me anything—I'm here to assist with questions, tasks, or just a chat.",
"timestamp": "2026-04-14T00:53:50.567Z"
}
],
"tool_calls": [],
"final_outcome": {
"success": true,
"reasoning": "The user greeted the assistant with \"hey\", and the assistant responded with \"Hey there! 👋 How can I help you today? Feel free to ask me anything—I'm here to assist with questions, tasks, or just a chat.\"\n\nLet me evaluate against the criteria:\n\n1. **Assistant responds in English**: ✓ The response is entirely in English.\n\n2. **Response is polite**: ✓ The response is polite and welcoming, using friendly language like \"Hey there!\" with a wave emoji, offering help, and being accommodating.\n\n3. **Response is under 40 words**: Let me count the words: \"Hey there How can I help you today Feel free to ask me anything I'm here to assist with questions tasks or just a chat\" = 28 words. ✓ This is under the 40-word limit.\n\nAll three criteria have been met successfully."
},
"metrics": {
"n_turns": 2,
"n_tool_calls": 0,
"total_tokens": 0,
"input_tokens": 0,
"output_tokens": 0,
"wall_time_ms": 7800000
}
},
"grader_results": [
{
"grader_type": "llm_rubric",
"weight": 1,
"score": 1,
"passed": true,
"reasoning": "The user greeted the assistant with \"hey\", and the assistant responded with \"Hey there! 👋 How can I help you today? Feel free to ask me anything—I'm here to assist with questions, tasks, or just a chat.\"\n\nLet me evaluate against the criteria:\n\n1. **Assistant responds in English**: ✓ The response is entirely in English.\n\n2. **Response is polite**: ✓ The response is polite and welcoming, using friendly language like \"Hey there!\" with a wave emoji, offering help, and being accommodating.\n\n3. **Response is under 40 words**: Let me count the words: \"Hey there How can I help you today Feel free to ask me anything I'm here to assist with questions tasks or just a chat\" = 28 words. ✓ This is under the 40-word limit.\n\nAll three criteria have been met successfully.",
"details": {
"source": "scenario_judge",
"met_criteria": [
"Assistant responds in English",
"Response is polite",
"Response is under 40 words"
],
"unmet_criteria": [],
"judge": "scenario.JudgeAgent",
"run_id": "scenariorun_3CKIqI6I9Tbn8LNd3ZAmS9wk0G2"
},
"duration_ms": 7800000
}
],
"score": 1,
"passed": true
}/**
* example-greeting.scenario.ts
*
* Minimum-viable scenario demonstrating PAIAgentAdapter + scenario.userSimulatorAgent
* + scenario.judgeAgent. The "agent under test" is a plain PAI-Inference call.
*
* Run:
* bun skills/Evals/Tools/ScenarioRunner.ts --scenario skills/Evals/Scenarios/example-greeting.scenario.ts
*
* *** API KEY BILLING WARNING ***
* @langwatch/scenario userSimulatorAgent and judgeAgent use @ai-sdk/anthropic
* which bills ANTHROPIC_API_KEY directly, NOT the subscription. Running a
* scenario consumes API credit. The agent-under-test (PAIAgentAdapter) still
* routes through Inference.ts subscription — only the sim + judge billing is
* the API. Set EVALS_ALLOW_API_BILLING=1 to acknowledge and run.
*/
import { anthropic } from '@ai-sdk/anthropic';
import scenario, { type ScenarioConfig } from '@langwatch/scenario';
import { PAIAgentAdapter } from '../Tools/PAIAgentAdapter.ts';
if (process.env.EVALS_ALLOW_API_BILLING !== '1') {
throw new Error(
'Evals scenario is guarded. Set EVALS_ALLOW_API_BILLING=1 to opt in — the @langwatch/scenario user-sim and judge bill the ANTHROPIC_API_KEY, not the subscription.',
);
}
const judgeModel = anthropic('claude-sonnet-4-6');
const config: ScenarioConfig = {
name: 'polite greeting',
description:
'A user greets a general-purpose assistant. The assistant should respond politely, in English, and keep the response concise.',
agents: [
new PAIAgentAdapter({
name: 'pai-assistant',
systemPrompt: 'You are a concise, polite assistant. Keep replies under 40 words.',
level: 'fast',
}),
scenario.userSimulatorAgent({ model: judgeModel }),
scenario.judgeAgent({
model: judgeModel,
criteria: [
'Assistant responds in English',
'Response is polite',
'Response is under 40 words',
],
}),
],
script: [scenario.user(), scenario.agent(), scenario.judge()],
maxTurns: 4,
};
export default config;
Evals as Science
Evals IS the scientific method applied to prompt engineering.
This isn't metaphor - Evals embodies the Science Protocol directly:
| Science Phase | Evals Implementation |
|---|---|
| Goal | Define use case success criteria, pass threshold |
| Observe | Baseline prompt performance measurement |
| Hypothesize | "Variant X will outperform baseline because..." |
| Experiment | Run eval suite with control + treatment prompts |
| Measure | Scores, SEM, confidence intervals |
| Analyze | Compare variants, determine statistical significance |
| Iterate | Refine prompt, run again, or declare success |
---
Scientific Rigor in Evals
Falsifiability (Non-Negotiable)
Every hypothesis MUST be falsifiable. When comparing prompts, ask:
- "What result would DISPROVE that variant X is better?"
- If you can't answer this, your evaluation is not scientific.
Pre-Commitment (Define Before You Run)
- Success criteria are defined BEFORE seeing results
- Pass thresholds are locked when use case is created
- No moving goalposts after data is collected
Plurality (Three-Variant Minimum Recommended)
- Don't just A/B test - consider A/B/C
- Multiple hypotheses = better exploration of solution space
- Reduces confirmation bias toward the first alternative
Confirmation Bias Countermeasures
- Position swapping mitigates positional bias
- Different judge model prevents self-serving evaluation
- Multi-judge panels reduce individual model quirks
- Statistical significance required to declare winner
---
When to Invoke Full Science Protocol
Most eval work runs implicitly as Science. Invoke explicit Science workflows when:
- You've been iterating for 3+ cycles without improvement (paradigm check)
- Results are confusing or contradictory (need structured analysis)
- Stakes are high enough to warrant formal documentation
- The question is "should we be testing something else entirely?"
Evaluation Scorer Types
Deterministic Scorers (60% weight recommended)
| Scorer | Speed | Use Case |
|---|---|---|
sentence-counter | <5ms | Format validation, length requirements |
word-counter | <5ms | Conciseness, length limits |
link-counter | <10ms | Attribution, reference validation |
format-validator | <10ms | Structure, required sections |
voice-validator | <10ms | Forbidden words, style requirements |
string-match | <5ms | Exact substring matching |
length-validator | <5ms | Character count bounds |
json-schema | <20ms | JSON structure validation |
---
AI-Based Scorers (40% weight recommended)
| Scorer | Speed | Use Case |
|---|---|---|
llm-judge-accuracy | ~2s | Factual accuracy, core takeaways |
llm-judge-style | ~2s | Voice authenticity, tone |
link-attribution-judge | ~2s | Author identification, citation quality |
---
Configuration Example
criteria:
deterministic:
- scorer: "sentence-counter"
weight: 0.10
params:
min: 2
max: 3
- scorer: "voice-validator"
weight: 0.10
params:
forbidden_words: ["unveils", "plummeted"]
check_contractions: true
ai_based:
- scorer: "llm-judge-accuracy"
weight: 0.15
params:
judge_model: "claude-3-5-sonnet-20241022"
reasoning_first: true
scale: "1-5"
pass_threshold: 0.75---
Best Practices for Scorer Selection
1. Run deterministic first: Fast gate before expensive AI evals 2. Balance weights: 60% deterministic / 40% AI-based recommended 3. Use appropriate scale: 1-5 most reliable for AI judges 4. Require reasoning first: 13%+ accuracy improvement
# Core Agent Behaviors - Regression Suite
# These are baseline behaviors that should ALWAYS work
name: core-behaviors
description: "Core agent behaviors that must not regress"
type: regression
domain: general
tasks:
- task_file_targeting_basic
- task_tool_sequence_read_before_edit
- task_verification_before_done
- task_no_hallucinated_paths
pass_threshold: 0.95
saturation_threshold: 0.99
created_at: "2026-01-10"
Template Integration
Available Templates
~/.claude/Templates/Evals/
├── Judge.hbs # Configurable LLM-as-Judge prompts
├── Rubric.hbs # Evaluation criteria definitions
├── TestCase.hbs # Test case specifications
├── Comparison.hbs # A/B testing templates
└── Report.hbs # Statistical result reports---
Creating Custom Judges
Use the JUDGE template for custom evaluation:
bun run ~/.claude/Templates/Tools/RenderTemplate.ts \
-t Evals/Judge.hbs \
-d ~/.claude/skills/Evals/UseCases/<name>/judge-config.yaml \
-o ~/.claude/skills/Evals/UseCases/<name>/judge-prompt.mdJudge Config Example
judge:
name: Content Quality Judge
focus: accuracy
scale:
type: 1-5
criteria:
- name: Factual Accuracy
description: Information matches source material
weight: 0.4
- name: Completeness
description: Covers all key points
weight: 0.3
- name: Clarity
description: Easy to understand
weight: 0.3
reasoning_required: true
position_swap: true
output:
format: json---
Creating Rubrics
Use the RUBRIC template for scoring criteria:
bun run ~/.claude/Templates/Tools/RenderTemplate.ts \
-t Evals/Rubric.hbs \
-d ~/.claude/skills/Evals/UseCases/<name>/rubric.yaml \
-o ~/.claude/skills/Evals/UseCases/<name>/rubric.md---
LLM-as-Judge Best Practices
1. Reasoning before scoring: Always require explanation first 2. Use 1-5 scale: Most reliable, avoid 0-100 3. Different judge model: Don't self-judge 4. Position swapping: Average A-first and B-first results 5. Multi-judge panels: 5-10 models, 7x cheaper than large single judge
#!/usr/bin/env bun
/**
* Algorithm Bridge
* Integration between Evals and THE ALGORITHM verification system
*/
import type { AlgorithmEvalRequest, AlgorithmEvalResult, EvalRun, Task } from '../Types/index.ts';
import { loadSuite, checkSaturation } from './SuiteManager.ts';
import { TrialRunner, formatEvalResults } from './TrialRunner.ts';
import { TranscriptCapture, createTranscript } from './TranscriptCapture.ts';
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
import { join } from 'path';
import { parse as parseYaml } from 'yaml';
import { parseArgs } from 'util';
import { $ } from 'bun';
const EVALS_DIR = join(import.meta.dir, '..');
const RESULTS_DIR = join(EVALS_DIR, 'Results');
/**
* Run an eval suite for ALGORITHM verification
*/
export async function runEvalForAlgorithm(
request: AlgorithmEvalRequest
): Promise<AlgorithmEvalResult> {
const suite = loadSuite(request.suite);
if (!suite) {
return {
isc_row: request.isc_row,
suite: request.suite,
passed: false,
score: 0,
summary: `Suite not found: ${request.suite}`,
run_id: 'error',
};
}
// Load tasks from suite
const tasks: Task[] = [];
for (const taskId of suite.tasks) {
const taskPath = findTaskFile(taskId);
if (taskPath && existsSync(taskPath)) {
const task = parseYaml(readFileSync(taskPath, 'utf-8')) as Task;
tasks.push(task);
}
}
if (tasks.length === 0) {
return {
isc_row: request.isc_row,
suite: request.suite,
passed: false,
score: 0,
summary: `No tasks found in suite: ${request.suite}`,
run_id: 'error',
};
}
// Run each task and aggregate
const results: EvalRun[] = [];
let totalScore = 0;
let passedTasks = 0;
for (const task of tasks) {
const runner = new TrialRunner({
task,
executor: async (t, trialNum) => {
// For ALGORITHM integration, we use a simplified executor
// that captures the current agent's work
const transcript = createTranscript(t.id, `trial_${trialNum}`, {
turns: [
{ role: 'system', content: t.description },
{ role: 'assistant', content: 'Task executed via ALGORITHM' },
],
toolCalls: [],
});
return {
output: 'Executed via ALGORITHM bridge',
transcript,
};
},
onTrialComplete: (trial) => {
console.log(` Trial ${trial.trial_number}: ${trial.passed ? '✅ PASS' : '❌ FAIL'} (${trial.score.toFixed(2)})`);
},
});
console.log(`Running task: ${task.id}`);
const run = await runner.run();
results.push(run);
totalScore += run.mean_score;
if (run.pass_rate >= (task.pass_threshold ?? 0.75)) {
passedTasks++;
}
// Save run results
saveRunResults(request.suite, run);
}
const overallScore = totalScore / tasks.length;
const overallPassed = passedTasks === tasks.length ||
overallScore >= (suite.pass_threshold ?? 0.75);
const summary = `${passedTasks}/${tasks.length} tasks passed, score: ${(overallScore * 100).toFixed(1)}%`;
return {
isc_row: request.isc_row,
suite: request.suite,
passed: overallPassed,
score: overallScore,
summary,
run_id: results[0]?.id ?? 'aggregate',
};
}
/**
* Find task file by ID
*/
function findTaskFile(taskId: string): string | null {
const useCasesDir = join(EVALS_DIR, 'UseCases');
const possiblePaths = [
join(useCasesDir, `${taskId}.yaml`),
join(useCasesDir, 'Regression', `${taskId}.yaml`),
join(useCasesDir, 'Capability', `${taskId}.yaml`),
];
for (const path of possiblePaths) {
if (existsSync(path)) return path;
}
return null;
}
/**
* Save run results
*/
function saveRunResults(suiteName: string, run: EvalRun): void {
const suiteResultsDir = join(RESULTS_DIR, suiteName);
if (!existsSync(suiteResultsDir)) mkdirSync(suiteResultsDir, { recursive: true });
const runDir = join(suiteResultsDir, run.id);
if (!existsSync(runDir)) mkdirSync(runDir);
writeFileSync(join(runDir, 'run.json'), JSON.stringify(run, null, 2));
}
/**
* Format result for ISC update
*/
export function formatForISC(result: AlgorithmEvalResult): string {
const icon = result.passed ? '✅' : '❌';
return `${icon} Eval: ${result.summary}`;
}
/**
* Update ISC row with eval result
*/
export async function updateISCWithResult(result: AlgorithmEvalResult): Promise<void> {
const status = result.passed ? 'DONE' : 'BLOCKED';
await $`bun run ~/.claude/skills/THEALGORITHM/Tools/ISCManager.ts update --row ${result.isc_row} --status ${status} --note "${formatForISC(result)}"`.quiet();
}
// CLI interface
if (import.meta.main) {
const { values } = parseArgs({
args: Bun.argv.slice(2),
options: {
suite: { type: 'string', short: 's' },
'isc-row': { type: 'string', short: 'r' },
'update-isc': { type: 'boolean', short: 'u' },
'show-saturation': { type: 'boolean' },
help: { type: 'boolean', short: 'h' },
},
allowPositionals: true,
});
if (values.help || !values.suite) {
console.log(`
AlgorithmBridge - Connect Evals to THE ALGORITHM
Usage:
bun run AlgorithmBridge.ts -s <suite> [-r row] [-u]
Options:
-s, --suite Eval suite to run
-r, --isc-row ISC row number (for result binding)
-u, --update-isc Automatically update ISC with result
--show-saturation Show suite saturation status
-h, --help Show this help
Examples:
# Run suite and show results
bun run AlgorithmBridge.ts -s regression-core
# Run and update ISC row 3
bun run AlgorithmBridge.ts -s regression-core -r 3 -u
# Check saturation status
bun run AlgorithmBridge.ts -s capability-auth --show-saturation
`);
process.exit(0);
}
if (values['show-saturation']) {
const status = checkSaturation(values.suite!);
console.log(`\nSaturation Status: ${values.suite}\n`);
console.log(` Saturated: ${status.saturated ? '⚠️ Yes' : '✅ No'}`);
console.log(` Consecutive above threshold: ${status.consecutive_above_threshold}/3`);
console.log(` Recommendation: ${status.recommended_action}`);
process.exit(0);
}
const request: AlgorithmEvalRequest = {
isc_row: values['isc-row'] ? parseInt(values['isc-row']) : 0,
suite: values.suite!,
};
console.log(`\nRunning eval suite: ${request.suite}\n`);
const result = await runEvalForAlgorithm(request);
console.log(`\n${'='.repeat(50)}`);
console.log(`\n📊 EVAL RESULT: ${result.passed ? '✅ PASSED' : '❌ FAILED'}`);
console.log(` Suite: ${result.suite}`);
console.log(` Score: ${(result.score * 100).toFixed(1)}%`);
console.log(` Summary: ${result.summary}`);
console.log(` Run ID: ${result.run_id}`);
if (values['update-isc'] && request.isc_row > 0) {
await updateISCWithResult(result);
console.log(`\n Updated ISC row ${request.isc_row}`);
}
process.exit(result.passed ? 0 : 1);
}
#!/usr/bin/env bun
/**
* PAIAgentAdapter — wraps PAI's Inference.ts as a scenario AgentAdapter.
*
* Lets scenario.run() drive a PAI agent in multi-turn simulations without
* pulling in the ai-sdk Anthropic provider for the agent-under-test path
* (scenario's UserSimulatorAgent + JudgeAgent still use ai-sdk directly).
*/
import { inference, type InferenceLevel } from '../../../PAI/TOOLS/Inference.ts';
import { AgentAdapter, AgentRole, type AgentInput, type AgentReturnTypes } from '@langwatch/scenario';
export interface PAIAgentAdapterOptions {
systemPrompt?: string;
level?: InferenceLevel;
timeout?: number;
name?: string;
}
export class PAIAgentAdapter extends AgentAdapter {
override role = AgentRole.AGENT;
override name: string;
private opts: Required<Omit<PAIAgentAdapterOptions, 'name'>>;
constructor(options: PAIAgentAdapterOptions = {}) {
super();
this.name = options.name ?? 'pai-agent';
this.opts = {
systemPrompt: options.systemPrompt ?? 'You are a helpful assistant.',
level: options.level ?? 'standard',
timeout: options.timeout ?? 60_000,
};
}
override async call(input: AgentInput): Promise<AgentReturnTypes> {
const userPrompt = this.renderMessages(input.messages);
const result = await inference({
systemPrompt: this.opts.systemPrompt,
userPrompt,
level: this.opts.level,
timeout: this.opts.timeout,
});
if (!result.success) {
throw new Error(`PAIAgentAdapter inference failed: ${result.error ?? 'unknown error'}`);
}
return result.output.trim();
}
private renderMessages(messages: AgentInput['messages']): string {
return messages
.map((m) => {
const role = m.role ?? 'user';
const content = this.extractText(m.content);
return `[${role}]: ${content}`;
})
.join('\n\n');
}
private extractText(content: unknown): string {
if (typeof content === 'string') return content;
if (Array.isArray(content)) {
return content
.map((part) => {
if (typeof part === 'string') return part;
if (part && typeof part === 'object' && 'text' in part) return String((part as { text: unknown }).text);
return '';
})
.filter(Boolean)
.join(' ');
}
return JSON.stringify(content);
}
}
# Task: File Targeting - Basic
# Agent should edit the correct file when asked
id: task_file_targeting_basic
description: "Agent correctly identifies and edits the specified file"
type: regression
domain: coding
source: failure_log
graders:
- type: tool_calls
weight: 0.40
required: true
params:
required:
- tool: read_file
- tool: edit_file
sequence:
- read_file
- edit_file
- type: llm_rubric
weight: 0.30
params:
rubric: |
Evaluate if the agent:
1. Read the CORRECT file (the one specified in the task)
2. Edited the CORRECT file (same as the one read)
3. Did NOT edit unrelated files
Score 5 if all criteria met
Score 3 if correct file edited but also touched others
Score 1 if wrong file edited
reasoning_first: true
scale: "1-5"
- type: natural_language_assert
weight: 0.30
params:
assertions:
- "The agent read the file before editing it"
- "The edit was made to the file specified in the request"
- "No unrelated files were modified"
trials: 1
pass_threshold: 0.75
tags:
- file_targeting
- basic
- regression
created_at: "2026-01-10"