Skip to content

Commit 8bf9c01

Browse files
committed
fix(evals): address the judge review findings
- judge evidence now includes each tool's returned output, so grounding rubrics can see the status/carrier values they check - the exact-argument check runs only for scripted runs; a live model may emit valid-but-different arguments - rubricDigest includes minScore, so a threshold-only change is not comparable - runScenario retains the structured judge verdict on the result, not just a formatted check string - the model-comparison suite passes a judge so judged scenarios are scored - the three originally-judged scenarios drop their finalContent pattern in live mode, so valid paraphrases are not failures - the live and comparison test timeouts scale with EVAL_TRIALS - the context checks assert history role and order, not just presence
1 parent 2023d07 commit 8bf9c01

8 files changed

Lines changed: 89 additions & 36 deletions

File tree

‎apps/sim/evals/agent-tool-use/agent-tool-use.compare.live.test.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ describe.skipIf(!LIVE)('agent tool-use model comparison', () => {
6060
mode: 'live',
6161
model: spec.model,
6262
providerName: spec.provider.label,
63+
...(scenario.judge
64+
? { judge: { completion, model: spec.model, rubric: scenario.judge } }
65+
: {}),
6366
})
6467
runs.push({
6568
provider: spec.provider.id,
@@ -69,7 +72,8 @@ describe.skipIf(!LIVE)('agent tool-use model comparison', () => {
6972
})
7073
}
7174
},
72-
TIMEOUT_MS
75+
/** The test runs TRIALS trials, each with a task call and possibly a judge call. */
76+
TRIALS * TIMEOUT_MS
7377
)
7478

7579
it('meets the per-model pass-rate floor', () => {

‎apps/sim/evals/agent-tool-use/agent-tool-use.live.test.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ describe.skipIf(!LIVE)('agent tool-use eval suite (live DeepSeek)', () => {
119119
).toBeGreaterThanOrEqual(MIN_PASS_RATE)
120120
}
121121
},
122-
TIMEOUT_MS
122+
/** The test runs TRIALS trials, each with a task call and possibly a judge call. */
123+
TRIALS * TIMEOUT_MS
123124
)
124125
})

‎apps/sim/evals/agent-tool-use/executor-harness.ts‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,28 @@ export async function runExecutorScenario(
267267
: `missing [${missingHistory.map((message) => message.content).join(', ')}]`,
268268
})
269269

270+
/** Each prior turn must appear as a subsequence with its role, in order. */
271+
let cursor = 0
272+
const inOrder = memory.history.every((message) => {
273+
while (cursor < requestMessages.length) {
274+
const candidate = requestMessages[cursor]
275+
cursor += 1
276+
if (
277+
candidate.role === message.role &&
278+
typeof candidate.content === 'string' &&
279+
candidate.content.includes(message.content)
280+
) {
281+
return true
282+
}
283+
}
284+
return false
285+
})
286+
checks.push({
287+
name: 'memory-history-order',
288+
passed: inOrder,
289+
detail: 'each prior turn reached the provider in order with its role',
290+
})
291+
270292
const lastHistoryIndex =
271293
memory.history.length === 0
272294
? -1

‎apps/sim/evals/agent-tool-use/harness.ts‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { toolsMockFns } from '@sim/testing/mocks/tools.mock'
77
import { isRecordLike } from '@sim/utils/object'
88
import type { ChatCompletionChunk } from 'openai/resources/chat/completions'
99
import type { CompletionUsage } from 'openai/resources/completions'
10-
import { type JudgeRubric, judgeAnswer } from '@/evals/agent-tool-use/judge'
10+
import { type JudgeRubric, type JudgeVerdict, judgeAnswer } from '@/evals/agent-tool-use/judge'
1111
import type {
1212
AgentToolUseExpectations,
1313
AgentToolUseResult,
@@ -438,6 +438,7 @@ export async function runScenario(
438438
success: response.success,
439439
...(response.error ? { error: response.error } : {}),
440440
durationMs: Date.now() - startedAt,
441+
result: response.output,
441442
})
442443
return response
443444
}
@@ -481,9 +482,10 @@ export async function runScenario(
481482
const iterations = completed?.iterations ?? 0
482483
const checks = scoreExpectations(expected, toolCalls, finalContent, iterations, streamError, mode)
483484

485+
let judgeVerdict: JudgeVerdict | undefined
484486
if (options.judge) {
485487
try {
486-
const verdict = await judgeAnswer({
488+
judgeVerdict = await judgeAnswer({
487489
completion: options.judge.completion,
488490
model: options.judge.model,
489491
rubric: options.judge.rubric,
@@ -493,15 +495,16 @@ export async function runScenario(
493495
})
494496
checks.push({
495497
name: 'judge',
496-
passed: verdict.passed,
497-
detail: `weighted ${verdict.weightedScore.toFixed(2)} [judge ${verdict.identity.model} ${verdict.identity.rubricDigest}]; ${verdict.rationale}`,
498+
passed: judgeVerdict.passed,
499+
detail: `weighted ${judgeVerdict.weightedScore.toFixed(2)} [judge ${judgeVerdict.identity.model} ${judgeVerdict.identity.rubricDigest}]; ${judgeVerdict.rationale}`,
498500
})
499501
} catch (error) {
500502
checks.push({ name: 'judge', passed: false, detail: `judge failed: ${String(error)}` })
501503
}
502504
}
503505

504-
if (invocations.length > 0) {
506+
/** Only scripted runs know the exact arguments; a live model may choose valid ones. */
507+
if (invocations.length > 0 && mode !== 'live') {
505508
const expectedCalls = expectedExecutableCalls(scenario)
506509
const mismatched = invocations.filter(
507510
(invocation) =>
@@ -533,6 +536,7 @@ export async function runScenario(
533536
checks,
534537
finalContent,
535538
toolInvocations: invocations,
539+
...(judgeVerdict ? { judge: judgeVerdict } : {}),
536540
metrics: {
537541
iterations,
538542
toolCalls: toolCalls.length,

‎apps/sim/evals/agent-tool-use/judge.test.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,12 @@ describe('judge identity', () => {
128128
expect(rubricDigest(reordered)).toBe(rubricDigest(rubric))
129129
})
130130

131+
it('changes the digest when only the passing threshold changes', () => {
132+
expect(rubricDigest({ criteria: rubric.criteria, minScore: 0.9 })).not.toBe(
133+
rubricDigest(rubric)
134+
)
135+
})
136+
131137
it('changes the digest when a criterion changes', () => {
132138
const changed = {
133139
criteria: [

‎apps/sim/evals/agent-tool-use/judge.ts‎

Lines changed: 16 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
import { createHash } from 'node:crypto'
22
import type { ChatCompletionChunk } from 'openai/resources/chat/completions'
3-
import type { JudgeCriterion, JudgeRubric } from '@/evals/agent-tool-use/types'
3+
import type {
4+
JudgeCriterion,
5+
JudgeIdentity,
6+
JudgeRubric,
7+
JudgeScore,
8+
JudgeVerdict,
9+
} from '@/evals/agent-tool-use/types'
410
import type { OpenAICompatCreateCompletion } from '@/providers/openai-compat/streaming-tool-loop'
511

6-
export type { JudgeCriterion, JudgeRubric }
12+
export type { JudgeCriterion, JudgeIdentity, JudgeRubric, JudgeScore, JudgeVerdict }
713

814
/**
915
* LLM-as-judge scoring for open-ended answers.
@@ -29,36 +35,19 @@ export interface JudgeInput {
2935
export const JUDGE_PARSER_VERSION = '1'
3036

3137
/**
32-
* Who judged and how. Two scores are only comparable when this matches; a delta
33-
* across differing identity is not evidence of improvement.
38+
* Stable digest of the rubric's grading contract, independent of key order. The
39+
* passing threshold is part of the contract: changing only `minScore` changes
40+
* which scores pass, so it must change the digest too.
3441
*/
35-
export interface JudgeIdentity {
36-
model: string
37-
rubricDigest: string
38-
parserVersion: string
39-
temperature?: number
40-
}
41-
42-
export interface JudgeScore {
43-
scores: Record<string, number>
44-
rationale: string
45-
weightedScore: number
46-
passed: boolean
47-
}
48-
49-
export interface JudgeVerdict extends JudgeScore {
50-
identity: JudgeIdentity
51-
}
52-
53-
/** Stable digest of the rubric's grading contract, independent of key order. */
5442
export function rubricDigest(rubric: JudgeRubric): string {
55-
const canonical = JSON.stringify(
56-
rubric.criteria.map((criterion) => ({
43+
const canonical = JSON.stringify({
44+
minScore: rubric.minScore ?? 0.5,
45+
criteria: rubric.criteria.map((criterion) => ({
5746
id: criterion.id,
5847
description: criterion.description,
5948
weight: criterion.weight ?? 1,
60-
}))
61-
)
49+
})),
50+
})
6251
return `sha256:${createHash('sha256').update(canonical).digest('hex').slice(0, 16)}`
6352
}
6453

‎apps/sim/evals/agent-tool-use/scenarios.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -311,8 +311,8 @@ export const AGENT_TOOL_USE_SCENARIOS: AgentToolUseScenario[] = [
311311
finalContent: /A-1937.*shipped/,
312312
maxIterations: 3,
313313
},
314-
/** A live model may answer with the user-facing order number and the grounded status. */
315-
liveExpect: { finalContent: /shipped/i },
314+
/** The rubric decides grounding; a content pattern would reject valid paraphrases. */
315+
liveExpect: { finalContent: undefined },
316316
judge: {
317317
criteria: [
318318
{
@@ -571,6 +571,7 @@ export const AGENT_TOOL_USE_SCENARIOS: AgentToolUseScenario[] = [
571571
],
572572
minScore: 0.7,
573573
},
574+
liveExpect: { finalContent: undefined },
574575
},
575576
{
576577
id: 'long-chain-dependency',
@@ -634,6 +635,7 @@ export const AGENT_TOOL_USE_SCENARIOS: AgentToolUseScenario[] = [
634635
toolCallSequence: undefined,
635636
requiredTools: ['list_orders', 'get_order', 'get_shipping'],
636637
successfulToolCalls: 3,
638+
finalContent: undefined,
637639
},
638640
judge: {
639641
criteria: [

‎apps/sim/evals/agent-tool-use/types.ts‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,27 @@ export interface JudgeRubric {
8383
minScore?: number
8484
}
8585

86+
/** Who judged and how. Two scores are comparable only when this matches. */
87+
export interface JudgeIdentity {
88+
model: string
89+
rubricDigest: string
90+
parserVersion: string
91+
temperature?: number
92+
}
93+
94+
/** A parsed judge response, before the identity envelope is attached. */
95+
export interface JudgeScore {
96+
scores: Record<string, number>
97+
rationale: string
98+
weightedScore: number
99+
passed: boolean
100+
}
101+
102+
/** A judge verdict retained in structured form so reports can compare it. */
103+
export interface JudgeVerdict extends JudgeScore {
104+
identity: JudgeIdentity
105+
}
106+
86107
/** A single agent behavior case. */
87108
export interface AgentToolUseScenario {
88109
id: string
@@ -125,6 +146,8 @@ export interface EvalToolInvocation {
125146
success: boolean
126147
error?: string
127148
durationMs: number
149+
/** The tool's returned output, so a judge can verify grounding. */
150+
result?: unknown
128151
}
129152

130153
/** Measured properties of one completed run. */
@@ -163,5 +186,7 @@ export interface AgentToolUseResult {
163186
finalContent: string
164187
toolInvocations: EvalToolInvocation[]
165188
metrics: AgentToolUseMetrics
189+
/** Structured judge verdict, present when a judge ran. */
190+
judge?: JudgeVerdict
166191
error?: string
167192
}

0 commit comments

Comments
 (0)