Skip to content

Commit f65dec7

Browse files
Merge pull request #4 from entropyvortex/fix/judge-confidence-contract
fix(prompts): auto-append JUDGE_CONFIDENCE directive in buildJudgeSys…
2 parents 870420e + 74651b7 commit f65dec7

4 files changed

Lines changed: 72 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,22 @@
33
All notable changes to `ai-consensus-core` will be documented here.
44
Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), [SemVer](https://semver.org/spec/v2.0.0.html).
55

6+
## [0.11.1] — 2026-05-25
7+
8+
### Fixed — judge-confidence parser contract
9+
10+
`buildJudgeSystemPrompt` now idempotently appends the `JUDGE_CONFIDENCE: [number 0-100]` directive, mirroring the `CONFIDENCE: [number 0-100]` handshake that `buildParticipantSystemPrompt` has always emitted. Previously, any caller that supplied a custom `ConsensusOptions.judge.systemPrompt` (instead of relying on `JUDGE_PERSONA.systemPrompt`, which has the directive inline) silently broke the parser contract: `extractJudgeConfidence` would not find the marker, fall through to its 50 default, and return a measurement-shaped value that polluted downstream statistics.
11+
12+
Discovered by a 12-run benchmark in `ai-consensus-mcp` where judge confidence was reported as exactly `μ=50.0, σ=0.0` across every run — the unmistakable fingerprint of the silent default. Every panel in that repo overrode `judgeSystemPrompt` and none re-emitted the marker.
13+
14+
- `buildJudgeSystemPrompt` auto-appends the directive when it is not already present in the supplied prompt
15+
- Idempotency check is case-insensitive substring on `JUDGE_CONFIDENCE`, so `JUDGE_PERSONA`'s inline directive (and any diligent custom caller) is not duplicated
16+
- New contract tests in `prompts.test.ts` mirror the existing participant-side test and fail loudly if a future edit breaks the handshake
17+
18+
### Backward compatibility
19+
20+
No public API change. The only observable difference is that `buildJudgeSystemPrompt`'s output string is longer when the input prompt lacks the marker. Callers that snapshot-test that output will need to regenerate snapshots. Callers that relied on the previous silent-50 behaviour will now see the real model-emitted value (which is the documented intent).
21+
622
## [0.11.0] — 2026-04-30
723

824
### Added — tool calling

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "ai-consensus-core",
3-
"version": "0.11.0",
3+
"version": "0.11.1",
44
"description": "Dependency-light TypeScript implementation of the Consensus Validation Protocol (CVP): multi-model debate with confidence-weighted scoring, disagreement detection, and optional judge synthesis.",
55
"keywords": [
66
"consensus",

src/__tests__/prompts.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,43 @@ describe("buildJudgeSystemPrompt", () => {
193193
});
194194
expect(out).toContain(JUDGE_PERSONA.systemPrompt);
195195
});
196+
197+
it("always ends with the JUDGE_CONFIDENCE marker directive (parser contract)", () => {
198+
// This is the handshake between prompt and parser. If a custom
199+
// judgeSystemPrompt lacks the directive, extractJudgeConfidence silently
200+
// returns its 50 default — a measurement-shaped value that pollutes
201+
// statistics rather than surfacing as missing data.
202+
const out = buildJudgeSystemPrompt({
203+
judgeSystemPrompt:
204+
"You are synthesising a debate. Produce a report. State your confidence.",
205+
question: "Q",
206+
});
207+
expect(out).toMatch(/JUDGE_CONFIDENCE: \[number 0-100\]\s*$/);
208+
});
209+
210+
it("does not duplicate the directive when the input already mentions JUDGE_CONFIDENCE", () => {
211+
// JUDGE_PERSONA.systemPrompt has its own inline JUDGE_CONFIDENCE
212+
// directive. Diligent callers may add one too. In both cases the
213+
// builder must be idempotent.
214+
const out = buildJudgeSystemPrompt({
215+
judgeSystemPrompt: JUDGE_PERSONA.systemPrompt,
216+
question: "Q",
217+
});
218+
const matches = out.match(/JUDGE_CONFIDENCE/gi) ?? [];
219+
expect(matches.length).toBe(1);
220+
});
221+
222+
it("appends the directive verbatim to a custom prompt that lacks it", () => {
223+
const customPrompt = "You are the architecture judge. Pick one option.";
224+
const out = buildJudgeSystemPrompt({
225+
judgeSystemPrompt: customPrompt,
226+
question: "Q",
227+
});
228+
expect(out).toContain(customPrompt);
229+
expect(out).toContain(
230+
"IMPORTANT: End your response with a line in exactly this format:\nJUDGE_CONFIDENCE: [number 0-100]",
231+
);
232+
});
196233
});
197234

198235
describe("buildJudgeUserPrompt", () => {

src/prompts.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,21 +100,38 @@ IMPORTANT: End your response with a line in exactly this format:
100100
CONFIDENCE: [number 0-100]`;
101101
}
102102

103+
/**
104+
* Trailing directive that pins the judge's output to the parser contract
105+
* (`extractJudgeConfidence` looks for this exact token). Mirrors the
106+
* `CONFIDENCE: [number 0-100]` handshake on the participant side.
107+
*/
108+
const JUDGE_CONFIDENCE_DIRECTIVE = `
109+
110+
IMPORTANT: End your response with a line in exactly this format:
111+
JUDGE_CONFIDENCE: [number 0-100]`;
112+
103113
/**
104114
* Build the judge's system prompt. We append the original user prompt to
105115
* the JUDGE_PERSONA's instructions so the model knows what was debated,
106116
* without having to infer it from participant text.
117+
*
118+
* Idempotently appends the `JUDGE_CONFIDENCE: [number 0-100]` directive so
119+
* `parser.extractJudgeConfidence` always finds a real value to parse rather
120+
* than silently returning its 50 default. If the caller's prompt already
121+
* contains a `JUDGE_CONFIDENCE` mention (as `JUDGE_PERSONA.systemPrompt`
122+
* does), the directive is not duplicated.
107123
*/
108124
export function buildJudgeSystemPrompt(params: {
109125
judgeSystemPrompt: string;
110126
question: string;
111127
}): string {
112-
return `${params.judgeSystemPrompt}
128+
const base = `${params.judgeSystemPrompt}
113129
114130
The original prompt that was debated was:
115131
"""
116132
${params.question}
117133
"""`;
134+
return /JUDGE_CONFIDENCE/i.test(base) ? base : `${base}${JUDGE_CONFIDENCE_DIRECTIVE}`;
118135
}
119136

120137
/**

0 commit comments

Comments
 (0)