dsh-llm-verifier 0.1.13 → 0.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -22
- package/README.md +328 -301
- package/cordis.patch.yml +4 -4
- package/lib/caller-BWwSVNZf.js.map +1 -0
- package/lib/client.js +176 -14
- package/lib/client.js.map +1 -0
- package/lib/core.js.map +1 -0
- package/lib/index.js +400 -51
- package/lib/index.js.map +1 -0
- package/lib/types/auto.d.ts.map +1 -0
- package/lib/types/auto.js +8 -2
- package/lib/types/auto.js.map +1 -0
- package/lib/types/cache.d.ts.map +1 -0
- package/lib/types/cache.js.map +1 -0
- package/lib/types/caller.d.ts.map +1 -0
- package/lib/types/caller.js.map +1 -0
- package/lib/types/client-i18n.d.ts +27 -0
- package/lib/types/client-i18n.d.ts.map +1 -0
- package/lib/types/client-i18n.js +18 -0
- package/lib/types/client-i18n.js.map +1 -0
- package/lib/types/client.d.ts +15 -2
- package/lib/types/client.d.ts.map +1 -0
- package/lib/types/client.js +137 -10
- package/lib/types/client.js.map +1 -0
- package/lib/types/config.d.ts +4 -0
- package/lib/types/config.d.ts.map +1 -0
- package/lib/types/config.js +5 -3
- package/lib/types/config.js.map +1 -0
- package/lib/types/core.d.ts.map +1 -0
- package/lib/types/core.js.map +1 -0
- package/lib/types/engine.d.ts.map +1 -0
- package/lib/types/engine.js.map +1 -0
- package/lib/types/images.d.ts.map +1 -0
- package/lib/types/images.js.map +1 -0
- package/lib/types/index.d.ts +2 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/index.js +162 -9
- package/lib/types/index.js.map +1 -0
- package/lib/types/plan-gate.d.ts +27 -0
- package/lib/types/plan-gate.d.ts.map +1 -0
- package/lib/types/plan-gate.js +60 -0
- package/lib/types/plan-gate.js.map +1 -0
- package/lib/types/router.d.ts +28 -32
- package/lib/types/router.d.ts.map +1 -0
- package/lib/types/router.js +41 -9
- package/lib/types/router.js.map +1 -0
- package/lib/types/session.d.ts +11 -0
- package/lib/types/session.d.ts.map +1 -0
- package/lib/types/session.js +41 -11
- package/lib/types/session.js.map +1 -0
- package/lib/types/statistics.d.ts.map +1 -0
- package/lib/types/statistics.js.map +1 -0
- package/lib/types/team-gate.d.ts +20 -0
- package/lib/types/team-gate.d.ts.map +1 -0
- package/lib/types/team-gate.js +59 -0
- package/lib/types/team-gate.js.map +1 -0
- package/lib/types/top-logprobs.d.ts.map +1 -0
- package/lib/types/top-logprobs.js.map +1 -0
- package/lib/types/topic-storage.d.ts.map +1 -0
- package/lib/types/topic-storage.js.map +1 -0
- package/package.json +114 -112
- package/src/auto.test.ts +95 -86
- package/src/auto.ts +140 -133
- package/src/cache.ts +115 -115
- package/src/caller.test.ts +34 -34
- package/src/caller.ts +151 -151
- package/src/client-i18n.ts +18 -0
- package/src/client.tsx +383 -213
- package/src/config.ts +189 -183
- package/src/core.test.ts +71 -71
- package/src/core.ts +205 -205
- package/src/engine.test.ts +136 -136
- package/src/engine.ts +140 -140
- package/src/images.ts +33 -33
- package/src/index.ts +394 -247
- package/src/parity.test.ts +85 -85
- package/src/plan-gate.test.ts +56 -0
- package/src/plan-gate.ts +66 -0
- package/src/router.test.ts +107 -89
- package/src/router.ts +361 -309
- package/src/session.test.ts +71 -36
- package/src/session.ts +132 -103
- package/src/statistics.test.ts +80 -80
- package/src/statistics.ts +328 -328
- package/src/team-gate.test.ts +92 -0
- package/src/team-gate.ts +80 -0
- package/src/top-logprobs.test.ts +84 -84
- package/src/top-logprobs.ts +153 -153
- package/src/topic-storage.test.ts +31 -31
- package/src/topic-storage.ts +42 -42
package/src/parity.test.ts
CHANGED
|
@@ -1,85 +1,85 @@
|
|
|
1
|
-
import { execFileSync } from 'node:child_process'
|
|
2
|
-
import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
3
|
-
import { tmpdir } from 'node:os'
|
|
4
|
-
import { join } from 'node:path'
|
|
5
|
-
import { describe, expect, it } from 'vitest'
|
|
6
|
-
import { ScoreCache } from './cache.ts'
|
|
7
|
-
import { extractProgressScore, extractScore, pivotRoundPairs } from './core.ts'
|
|
8
|
-
import { resolveConfig } from './config.ts'
|
|
9
|
-
|
|
10
|
-
const PYTHON_ROOT = join(import.meta.dirname, '..', '..', 'llm-as-a-verifier')
|
|
11
|
-
const hasPythonUpstream = existsSync(join(PYTHON_ROOT, 'llm_verifier'))
|
|
12
|
-
|
|
13
|
-
function python(payload: unknown): unknown {
|
|
14
|
-
const script = [
|
|
15
|
-
'import json,sys',
|
|
16
|
-
'sys.path.insert(0, sys.argv[1])',
|
|
17
|
-
'from llm_verifier.fine_grained_reward import extract_score',
|
|
18
|
-
'from llm_verifier.pivot_tournament import pivot_round_pairs',
|
|
19
|
-
'd=json.loads(sys.stdin.read())',
|
|
20
|
-
"result = extract_score(d['text'], d['tokens'], d['positions'], d['tag']) if d['kind']=='score' else pivot_round_pairs(d['n'], d['pivots'])",
|
|
21
|
-
'print(json.dumps(result))',
|
|
22
|
-
].join(';')
|
|
23
|
-
const source = payload as Record<string, unknown>
|
|
24
|
-
const compatible = source.kind === 'score' ? { ...source, positions: (source.positions as Array<Array<{ token: string; logprob: number }>>).map(position => position.map(item => [item.token, item.logprob])) } : source
|
|
25
|
-
const result = execFileSync('py', ['-3', '-c', script, PYTHON_ROOT], { input: JSON.stringify(compatible), encoding: 'utf8' })
|
|
26
|
-
return JSON.parse(result)
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
describe('Python parity fixtures', () => {
|
|
30
|
-
it.skipIf(!hasPythonUpstream)('matches literal and distribution score extraction', () => {
|
|
31
|
-
const fixtures = [
|
|
32
|
-
{ text: '<score_A> A </score_A>', tokens: [], positions: [], tag: '<score_A>' },
|
|
33
|
-
{ text: '<score_A> A </score_A> then <score_A> T </score_A>', tokens: [], positions: [], tag: '<score_A>' },
|
|
34
|
-
{ text: '', tokens: ['<score_A>'], positions: [[], [{ token: 'A', logprob: Math.log(0.7) }, { token: 'T', logprob: Math.log(0.3) }]], tag: '<score_A>' },
|
|
35
|
-
{ text: '', tokens: ['<score_A'], positions: [[], [{ token: '>B', logprob: Math.log(0.8) }, { token: '>S', logprob: Math.log(0.2) }]], tag: '<score_A>' },
|
|
36
|
-
]
|
|
37
|
-
for (const fixture of fixtures) {
|
|
38
|
-
const ts = extractScore(fixture, fixture.tag)
|
|
39
|
-
const py = python({ kind: 'score', ...fixture }) as number
|
|
40
|
-
expect(ts).toBeCloseTo(py, 12)
|
|
41
|
-
}
|
|
42
|
-
})
|
|
43
|
-
it.skipIf(!hasPythonUpstream)('matches pivot pair generation', () => {
|
|
44
|
-
expect(pivotRoundPairs(7, [1, 4, 5])).toEqual(python({ kind: 'pivot', n: 7, pivots: [1, 4, 5] }))
|
|
45
|
-
})
|
|
46
|
-
it('matches reversed progress convention', () => {
|
|
47
|
-
expect(extractProgressScore({ text: '<c1> T </c1>', tokens: [], positions: [] }, '<c1>')).toBe(1)
|
|
48
|
-
})
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
describe('DSH model routing and cache', () => {
|
|
52
|
-
it('accepts any non-empty DSH provider and model route', () => {
|
|
53
|
-
expect(resolveConfig({ provider: 'openai', model: 'gpt-5' })).toMatchObject({ provider: 'openai', model: 'gpt-5' })
|
|
54
|
-
expect(resolveConfig({ provider: 'anthropic', model: 'claude-sonnet' })).toMatchObject({ provider: 'anthropic', model: 'claude-sonnet' })
|
|
55
|
-
expect(() => resolveConfig({ provider: '', model: 'gpt-5' })).toThrow(/provider must be non-empty/)
|
|
56
|
-
expect(resolveConfig({ autoRouteMaxItemChars: 1000, autoRouteMaxInputChars: 2000, autoMaxModelCallsPerTask: 4, autoMaxModelCallsPerSession: 8 })).toMatchObject({ autoRouteMaxItemChars: 1000, autoRouteMaxInputChars: 2000, autoMaxModelCallsPerTask: 4, autoMaxModelCallsPerSession: 8 })
|
|
57
|
-
expect(() => resolveConfig({ autoRouteMaxCandidates: 17 })).toThrow(/between 3 and 16/)
|
|
58
|
-
expect(() => resolveConfig({ autoRouteMaxItemChars: 2000, autoRouteMaxInputChars: 3000 })).toThrow(/fit at least two/)
|
|
59
|
-
})
|
|
60
|
-
it('rejects malformed persisted entries and recreates them', async () => {
|
|
61
|
-
const dir = mkdtempSync(join(tmpdir(), 'dsh-verifier-invalid-'))
|
|
62
|
-
try {
|
|
63
|
-
const file = join(dir, 'scores.json')
|
|
64
|
-
writeFileSync(file, JSON.stringify({ version: 1, entries: { bad: { scoreA: 'NaN', scoreB: 0, usage: {}, createdAt: -1 } } }))
|
|
65
|
-
const cache = new ScoreCache(file, 100)
|
|
66
|
-
let creates = 0
|
|
67
|
-
const result = await cache.getOrCreate('bad', async () => { creates++; return { scoreA: .8, scoreB: .2, usage: { calls: 1, attempts: 1, retries: 0, inputTokens: 1, cachedInputTokens: 0, outputTokens: 1, reasoningTokens: 0 }, scoringMode: 'explicit-tag' as const, createdAt: Date.now() } })
|
|
68
|
-
expect(result.hit).toBe(false); expect(creates).toBe(1)
|
|
69
|
-
} finally { rmSync(dir, { recursive: true, force: true }) }
|
|
70
|
-
})
|
|
71
|
-
it('persists successful results and avoids duplicate creation', async () => {
|
|
72
|
-
const dir = mkdtempSync(join(tmpdir(), 'dsh-verifier-'))
|
|
73
|
-
try {
|
|
74
|
-
const file = join(dir, 'scores.json')
|
|
75
|
-
const first = new ScoreCache(file, 100)
|
|
76
|
-
let creates = 0
|
|
77
|
-
const create = async () => { creates++; return { scoreA: 1, scoreB: 0, usage: { calls: 1, attempts: 1, retries: 0, inputTokens: 1, cachedInputTokens: 0, outputTokens: 1, reasoningTokens: 0 }, scoringMode: 'top-logprobs' as const, createdAt: Date.now() } }
|
|
78
|
-
expect((await first.getOrCreate('key', create)).hit).toBe(false)
|
|
79
|
-
expect((await first.getOrCreate('key', create)).hit).toBe(true)
|
|
80
|
-
const second = new ScoreCache(file, 100)
|
|
81
|
-
expect((await second.getOrCreate('key', create)).hit).toBe(true)
|
|
82
|
-
expect(creates).toBe(1)
|
|
83
|
-
} finally { rmSync(dir, { recursive: true, force: true }) }
|
|
84
|
-
})
|
|
85
|
-
})
|
|
1
|
+
import { execFileSync } from 'node:child_process'
|
|
2
|
+
import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { describe, expect, it } from 'vitest'
|
|
6
|
+
import { ScoreCache } from './cache.ts'
|
|
7
|
+
import { extractProgressScore, extractScore, pivotRoundPairs } from './core.ts'
|
|
8
|
+
import { resolveConfig } from './config.ts'
|
|
9
|
+
|
|
10
|
+
const PYTHON_ROOT = join(import.meta.dirname, '..', '..', 'llm-as-a-verifier')
|
|
11
|
+
const hasPythonUpstream = existsSync(join(PYTHON_ROOT, 'llm_verifier'))
|
|
12
|
+
|
|
13
|
+
function python(payload: unknown): unknown {
|
|
14
|
+
const script = [
|
|
15
|
+
'import json,sys',
|
|
16
|
+
'sys.path.insert(0, sys.argv[1])',
|
|
17
|
+
'from llm_verifier.fine_grained_reward import extract_score',
|
|
18
|
+
'from llm_verifier.pivot_tournament import pivot_round_pairs',
|
|
19
|
+
'd=json.loads(sys.stdin.read())',
|
|
20
|
+
"result = extract_score(d['text'], d['tokens'], d['positions'], d['tag']) if d['kind']=='score' else pivot_round_pairs(d['n'], d['pivots'])",
|
|
21
|
+
'print(json.dumps(result))',
|
|
22
|
+
].join(';')
|
|
23
|
+
const source = payload as Record<string, unknown>
|
|
24
|
+
const compatible = source.kind === 'score' ? { ...source, positions: (source.positions as Array<Array<{ token: string; logprob: number }>>).map(position => position.map(item => [item.token, item.logprob])) } : source
|
|
25
|
+
const result = execFileSync('py', ['-3', '-c', script, PYTHON_ROOT], { input: JSON.stringify(compatible), encoding: 'utf8' })
|
|
26
|
+
return JSON.parse(result)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('Python parity fixtures', () => {
|
|
30
|
+
it.skipIf(!hasPythonUpstream)('matches literal and distribution score extraction', () => {
|
|
31
|
+
const fixtures = [
|
|
32
|
+
{ text: '<score_A> A </score_A>', tokens: [], positions: [], tag: '<score_A>' },
|
|
33
|
+
{ text: '<score_A> A </score_A> then <score_A> T </score_A>', tokens: [], positions: [], tag: '<score_A>' },
|
|
34
|
+
{ text: '', tokens: ['<score_A>'], positions: [[], [{ token: 'A', logprob: Math.log(0.7) }, { token: 'T', logprob: Math.log(0.3) }]], tag: '<score_A>' },
|
|
35
|
+
{ text: '', tokens: ['<score_A'], positions: [[], [{ token: '>B', logprob: Math.log(0.8) }, { token: '>S', logprob: Math.log(0.2) }]], tag: '<score_A>' },
|
|
36
|
+
]
|
|
37
|
+
for (const fixture of fixtures) {
|
|
38
|
+
const ts = extractScore(fixture, fixture.tag)
|
|
39
|
+
const py = python({ kind: 'score', ...fixture }) as number
|
|
40
|
+
expect(ts).toBeCloseTo(py, 12)
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
it.skipIf(!hasPythonUpstream)('matches pivot pair generation', () => {
|
|
44
|
+
expect(pivotRoundPairs(7, [1, 4, 5])).toEqual(python({ kind: 'pivot', n: 7, pivots: [1, 4, 5] }))
|
|
45
|
+
})
|
|
46
|
+
it('matches reversed progress convention', () => {
|
|
47
|
+
expect(extractProgressScore({ text: '<c1> T </c1>', tokens: [], positions: [] }, '<c1>')).toBe(1)
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
describe('DSH model routing and cache', () => {
|
|
52
|
+
it('accepts any non-empty DSH provider and model route', () => {
|
|
53
|
+
expect(resolveConfig({ provider: 'openai', model: 'gpt-5' })).toMatchObject({ provider: 'openai', model: 'gpt-5' })
|
|
54
|
+
expect(resolveConfig({ provider: 'anthropic', model: 'claude-sonnet' })).toMatchObject({ provider: 'anthropic', model: 'claude-sonnet' })
|
|
55
|
+
expect(() => resolveConfig({ provider: '', model: 'gpt-5' })).toThrow(/provider must be non-empty/)
|
|
56
|
+
expect(resolveConfig({ autoRouteMaxItemChars: 1000, autoRouteMaxInputChars: 2000, autoMaxModelCallsPerTask: 4, autoMaxModelCallsPerSession: 8 })).toMatchObject({ autoRouteMaxItemChars: 1000, autoRouteMaxInputChars: 2000, autoMaxModelCallsPerTask: 4, autoMaxModelCallsPerSession: 8 })
|
|
57
|
+
expect(() => resolveConfig({ autoRouteMaxCandidates: 17 })).toThrow(/between 3 and 16/)
|
|
58
|
+
expect(() => resolveConfig({ autoRouteMaxItemChars: 2000, autoRouteMaxInputChars: 3000 })).toThrow(/fit at least two/)
|
|
59
|
+
})
|
|
60
|
+
it('rejects malformed persisted entries and recreates them', async () => {
|
|
61
|
+
const dir = mkdtempSync(join(tmpdir(), 'dsh-verifier-invalid-'))
|
|
62
|
+
try {
|
|
63
|
+
const file = join(dir, 'scores.json')
|
|
64
|
+
writeFileSync(file, JSON.stringify({ version: 1, entries: { bad: { scoreA: 'NaN', scoreB: 0, usage: {}, createdAt: -1 } } }))
|
|
65
|
+
const cache = new ScoreCache(file, 100)
|
|
66
|
+
let creates = 0
|
|
67
|
+
const result = await cache.getOrCreate('bad', async () => { creates++; return { scoreA: .8, scoreB: .2, usage: { calls: 1, attempts: 1, retries: 0, inputTokens: 1, cachedInputTokens: 0, outputTokens: 1, reasoningTokens: 0 }, scoringMode: 'explicit-tag' as const, createdAt: Date.now() } })
|
|
68
|
+
expect(result.hit).toBe(false); expect(creates).toBe(1)
|
|
69
|
+
} finally { rmSync(dir, { recursive: true, force: true }) }
|
|
70
|
+
})
|
|
71
|
+
it('persists successful results and avoids duplicate creation', async () => {
|
|
72
|
+
const dir = mkdtempSync(join(tmpdir(), 'dsh-verifier-'))
|
|
73
|
+
try {
|
|
74
|
+
const file = join(dir, 'scores.json')
|
|
75
|
+
const first = new ScoreCache(file, 100)
|
|
76
|
+
let creates = 0
|
|
77
|
+
const create = async () => { creates++; return { scoreA: 1, scoreB: 0, usage: { calls: 1, attempts: 1, retries: 0, inputTokens: 1, cachedInputTokens: 0, outputTokens: 1, reasoningTokens: 0 }, scoringMode: 'top-logprobs' as const, createdAt: Date.now() } }
|
|
78
|
+
expect((await first.getOrCreate('key', create)).hit).toBe(false)
|
|
79
|
+
expect((await first.getOrCreate('key', create)).hit).toBe(true)
|
|
80
|
+
const second = new ScoreCache(file, 100)
|
|
81
|
+
expect((await second.getOrCreate('key', create)).hit).toBe(true)
|
|
82
|
+
expect(creates).toBe(1)
|
|
83
|
+
} finally { rmSync(dir, { recursive: true, force: true }) }
|
|
84
|
+
})
|
|
85
|
+
})
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { buildPlanPreReviewPrompt, parseVerdictLetter, planFromArguments } from './plan-gate.ts'
|
|
3
|
+
|
|
4
|
+
describe('plan-gate', () => {
|
|
5
|
+
it('reads the plan markdown out of exit_plan_mode arguments', () => {
|
|
6
|
+
expect(planFromArguments({ plan: ' # Plan\nStep 1 ' })).toBe('# Plan\nStep 1')
|
|
7
|
+
expect(planFromArguments({ plan: ' ' })).toBe('')
|
|
8
|
+
expect(planFromArguments({})).toBe('')
|
|
9
|
+
expect(planFromArguments({ plan: 42 })).toBe('')
|
|
10
|
+
expect(planFromArguments('plan')).toBe('')
|
|
11
|
+
expect(planFromArguments(null)).toBe('')
|
|
12
|
+
expect(planFromArguments(undefined)).toBe('')
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('builds plan review prompt with requirements and an exact verdict line', () => {
|
|
16
|
+
const prompt = buildPlanPreReviewPrompt('Implement Redis cache', '1. Connect redis 2. Cache queries', 5000)
|
|
17
|
+
expect(prompt).toContain('You are an expert independent technical plan verifier')
|
|
18
|
+
expect(prompt).toContain('Implement Redis cache')
|
|
19
|
+
expect(prompt).toContain('1. Connect redis 2. Cache queries')
|
|
20
|
+
expect(prompt).toContain('Verdict: <single uppercase letter A-T>')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('parses a whole verdict line into the A-T score scale', () => {
|
|
24
|
+
const perfect = parseVerdictLetter('Verdict: A\nSummary: Outstanding plan.\nVerification is complete.')
|
|
25
|
+
expect(perfect?.verdict).toBe('A')
|
|
26
|
+
expect(perfect?.score).toBe(1.0)
|
|
27
|
+
expect(perfect?.feedback).toContain('Outstanding plan')
|
|
28
|
+
|
|
29
|
+
const failed = parseVerdictLetter('Verdict: T\nSummary: Completely unfeasible.')
|
|
30
|
+
expect(failed?.verdict).toBe('T')
|
|
31
|
+
expect(failed?.score).toBe(0.0)
|
|
32
|
+
|
|
33
|
+
const middle = parseVerdictLetter('Verdict: C\nSummary: Mostly good.')
|
|
34
|
+
expect(middle?.verdict).toBe('C')
|
|
35
|
+
expect(middle?.score).toBeCloseTo(1 - 2 / 19, 3)
|
|
36
|
+
|
|
37
|
+
expect(parseVerdictLetter('Summary: fine\n verdict: d \nNotes: ok')?.score).toBeCloseTo(1 - 3 / 19, 3)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('reads a verdict line wrapped in markdown decoration', () => {
|
|
41
|
+
expect(parseVerdictLetter('**Verdict: A**\nSummary: ok')?.verdict).toBe('A')
|
|
42
|
+
expect(parseVerdictLetter('**Verdict**: B')?.score).toBeCloseTo(1 - 1 / 19, 3)
|
|
43
|
+
expect(parseVerdictLetter('- Verdict: C.')?.verdict).toBe('C')
|
|
44
|
+
expect(parseVerdictLetter('1. Verdict: D)')?.verdict).toBe('D')
|
|
45
|
+
expect(parseVerdictLetter('`Verdict: E`')?.verdict).toBe('E')
|
|
46
|
+
expect(parseVerdictLetter('### Verdict: T')?.score).toBe(0)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('never scores prose, a missing verdict, or a trailing explanation', () => {
|
|
50
|
+
expect(parseVerdictLetter('Verdict: Failed\nSummary: the plan cannot work')).toBeUndefined()
|
|
51
|
+
expect(parseVerdictLetter('Verdict: Approved')).toBeUndefined()
|
|
52
|
+
expect(parseVerdictLetter('Verdict: A because the plan is great')).toBeUndefined()
|
|
53
|
+
expect(parseVerdictLetter('Summary: no verdict line at all')).toBeUndefined()
|
|
54
|
+
expect(parseVerdictLetter('')).toBeUndefined()
|
|
55
|
+
})
|
|
56
|
+
})
|
package/src/plan-gate.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { sanitizeVerifierText } from './session.ts'
|
|
2
|
+
|
|
3
|
+
export interface PlanVerdict {
|
|
4
|
+
verdict: string
|
|
5
|
+
score: number
|
|
6
|
+
feedback: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Read the plan markdown out of one `exit_plan_mode` argument object.
|
|
11
|
+
* @param args - Parsed tool arguments; anything but `{ plan: string }` yields ''.
|
|
12
|
+
* @returns Trimmed plan markdown, or '' when the call carries no plan.
|
|
13
|
+
*/
|
|
14
|
+
export function planFromArguments(args: unknown): string {
|
|
15
|
+
if (typeof args !== 'object' || args === null) return ''
|
|
16
|
+
const plan = (args as { plan?: unknown }).plan
|
|
17
|
+
return typeof plan === 'string' ? plan.trim() : ''
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function buildPlanPreReviewPrompt(problem: string, planText: string, maxChars = 20000): string {
|
|
21
|
+
return [
|
|
22
|
+
'You are an expert independent technical plan verifier. A candidate implementation plan is about to be submitted to the human for approval.',
|
|
23
|
+
'Evaluate whether the plan is sound, executable, and comprehensive.',
|
|
24
|
+
'Scoring criteria:',
|
|
25
|
+
'A: Flawless, thorough, with clear steps, rigorous verification, edge cases handled.',
|
|
26
|
+
'B-J: Mostly sound, minor gaps or nuances missing.',
|
|
27
|
+
'K-S: Noticeable architectural gaps, missing regression checks, or ambiguities.',
|
|
28
|
+
'T: Fundamentally flawed, dangerous, or missing core requirements.',
|
|
29
|
+
'',
|
|
30
|
+
'Task requirement:',
|
|
31
|
+
sanitizeVerifierText(problem, 4000),
|
|
32
|
+
'',
|
|
33
|
+
'Proposed Plan:',
|
|
34
|
+
sanitizeVerifierText(planText, maxChars),
|
|
35
|
+
'',
|
|
36
|
+
'Output format:',
|
|
37
|
+
'The first line must be exactly "Verdict: <single uppercase letter A-T>" — one letter, nothing else on the line.',
|
|
38
|
+
'Line 2: Summary: <One sentence assessment>',
|
|
39
|
+
'Line 3+: Key strengths, blind spots, and verification guidance.',
|
|
40
|
+
].join('\n\n')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Parse the `Verdict: <A-T>` line the judge prompts require.
|
|
45
|
+
*
|
|
46
|
+
* Markdown decoration around the required line is stripped first, so "**Verdict:
|
|
47
|
+
* A**", "- Verdict: B." or a numbered list marker still produce a grade — a
|
|
48
|
+
* formatting habit must never silently switch the gate off. Prose that is not a
|
|
49
|
+
* grade is still rejected: "Verdict: Failed" and "Verdict: Approved" carry no
|
|
50
|
+
* verdict letter, and an answer without a verdict line has no score at all —
|
|
51
|
+
* callers must skip such a review, never treat it as a pass. A is 1.0 and T is
|
|
52
|
+
* 0.0, matching the top-logprob A-T scale used by the judge prompts.
|
|
53
|
+
* @param text - Raw judge model output.
|
|
54
|
+
* @returns The parsed verdict, or undefined when no verdict line is present.
|
|
55
|
+
*/
|
|
56
|
+
export function parseVerdictLetter(text: string): PlanVerdict | undefined {
|
|
57
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
58
|
+
const line = rawLine.replace(/[*_`~#>]/g, ' ').replace(/^\s*(?:[-+]|\d+[.)])\s+/, ' ').trim()
|
|
59
|
+
const match = /^verdict\s*:\s*([A-Ta-t])\s*[.)]?$/i.exec(line)
|
|
60
|
+
if (match === null) continue
|
|
61
|
+
const verdict = (match[1] as string).toUpperCase()
|
|
62
|
+
const score = Math.max(0, Math.min(1, 1 - (verdict.charCodeAt(0) - 65) / 19))
|
|
63
|
+
return { verdict, score, feedback: text.trim() }
|
|
64
|
+
}
|
|
65
|
+
return undefined
|
|
66
|
+
}
|
package/src/router.test.ts
CHANGED
|
@@ -1,89 +1,107 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import { Session } from '@deepseek-ai/dsh-session'
|
|
3
|
-
import { createToolResultMessage, createUserMessage } from '@deepseek-ai/dsh-llm'
|
|
4
|
-
import { analyzeStructuredRoute, AutoVerifierRouter, buildSemanticRoutePrompt, parseSemanticRoute, semanticDecision, semanticRouteHint, type RouterPolicy } from './router.ts'
|
|
5
|
-
|
|
6
|
-
function session() {
|
|
7
|
-
const value = Session.create('session-00000000-0000-4000-8000-000000000077' as never)
|
|
8
|
-
value.append('user/message', createUserMessage({ content: [{ type: 'text', text: 'Choose and implement the best solution' }], source: { kind: 'user' } }), { surfaceOp: 'append' })
|
|
9
|
-
return value
|
|
10
|
-
}
|
|
11
|
-
function tool(value: ReturnType<typeof session>, name: string, id: string, text: string, turn = 1, step = 1) {
|
|
12
|
-
value.append('tool/call', { turn, step, callId: id as never, name, arguments: '{}' })
|
|
13
|
-
value.append('tool/result', { turn, step, message: createToolResultMessage({ callId: id as never, content: [{ type: 'text', text }], isError: false }) }, { surfaceOp: 'append' })
|
|
14
|
-
}
|
|
15
|
-
const policy: RouterPolicy = { mode: 'smart', minConfidence: .9, maxCandidates: 8, maxPerTask: 5, maxPerSession: 20, maxModelCallsPerTask: 48, maxModelCallsPerSession: 160, maxInputChars: 60000, maxItemChars: 20000 }
|
|
16
|
-
const envelope = (count: number) => JSON.stringify({ protocol: 'dsh-verifier-candidates', version: 1, groupId: 'auth', candidates: Array.from({ length: count }, (_, i) => ({ id: String(i + 1), label: 'C' + (i + 1), status: 'completed', content: 'candidate ' + (i + 1) })) })
|
|
17
|
-
|
|
18
|
-
describe('production structured routing', () => {
|
|
19
|
-
it('requires a trusted versioned workflow envelope', () => {
|
|
20
|
-
const value = session(); tool(value, 'workflow', 'w', envelope(3))
|
|
21
|
-
expect(analyzeStructuredRoute(value.events)).toMatchObject({ kind: 'select', source: 'structured' })
|
|
22
|
-
const untrusted = session(); tool(untrusted, 'workflow', 'w', JSON.stringify({ verifier_candidates: ['a', 'b'] }))
|
|
23
|
-
expect(analyzeStructuredRoute(untrusted.events)).toBeUndefined()
|
|
24
|
-
})
|
|
25
|
-
it('does not guess that unrelated synchronous subagents are alternatives', () => {
|
|
26
|
-
const value = session(); tool(value, 'subagent', 'a', 'frontend analysis', 1, 2); tool(value, 'subagent', 'b', 'backend analysis', 1, 2)
|
|
27
|
-
expect(analyzeStructuredRoute(value.events)).toBeUndefined()
|
|
28
|
-
expect(semanticRouteHint(value.events)).toBe(true)
|
|
29
|
-
})
|
|
30
|
-
it('deduplicates identical todo snapshots', () => {
|
|
31
|
-
const value = session()
|
|
32
|
-
const todos = [{ content: 'Implement', status: 'in_progress' as const }, { content: 'Test', status: 'pending' as const }]
|
|
33
|
-
value.append('todo/write', { todos }); value.append('todo/write', { todos })
|
|
34
|
-
expect(analyzeStructuredRoute(value.events)).toBeUndefined()
|
|
35
|
-
value.append('todo/write', { todos: [{ content: 'Implement', status: 'completed' }, { content: 'Test', status: 'in_progress' }] })
|
|
36
|
-
expect(analyzeStructuredRoute(value.events)).toMatchObject({ kind: 'track', evidenceSeqs: expect.any(Array) })
|
|
37
|
-
})
|
|
38
|
-
it('redacts and bounds trusted candidate content', () => {
|
|
39
|
-
const value = session(); tool(value, 'workflow', 'w', JSON.stringify({ protocol: 'dsh-verifier-candidates', version: 1, groupId: 'g', candidates: [{ id: 'a', status: 'completed', content: 'token = abc ' + 'x'.repeat(100) }, { id: 'b', status: 'completed', content: 'password: secret ' + 'y'.repeat(100) }] }))
|
|
40
|
-
const decision = analyzeStructuredRoute(value.events, 8, 40)
|
|
41
|
-
expect(decision?.kind).toBe('compare')
|
|
42
|
-
if (decision?.kind === 'compare') { expect(decision.candidates[0].content).not.toContain('abc'); expect(decision.candidates[0].content.length).toBeLessThan(80) }
|
|
43
|
-
})
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
describe('semantic evidence references', () => {
|
|
47
|
-
it('accepts exact strict JSON references and rejects prose/unknown keys', () => {
|
|
48
|
-
const valid = JSON.stringify({ kind: 'compare', confidence: .95, reason: 'alternatives', candidateCallIds: ['a', 'b'], checkpointSeqs: [] })
|
|
49
|
-
expect(parseSemanticRoute(valid)).toMatchObject({ kind: 'compare', candidateCallIds: ['a', 'b'] })
|
|
50
|
-
expect(parseSemanticRoute('```json\n' + valid + '\n```')).toBeUndefined()
|
|
51
|
-
expect(parseSemanticRoute(JSON.stringify({ ...JSON.parse(valid), extra: true }))).toBeUndefined()
|
|
52
|
-
})
|
|
53
|
-
it('resolves only real paired call ids', () => {
|
|
54
|
-
const value = session(); tool(value, 'subagent', 'a', 'candidate A'); tool(value, 'subagent', 'b', 'candidate B')
|
|
55
|
-
const parsed = parseSemanticRoute(JSON.stringify({ kind: 'compare', confidence: .95, reason: 'same task alternatives', candidateCallIds: ['a', 'b'], checkpointSeqs: [] }))!
|
|
56
|
-
expect(semanticDecision(parsed, value.events)).toMatchObject({ kind: 'compare', source: 'semantic' })
|
|
57
|
-
expect(semanticDecision({ ...parsed, candidateCallIds: ['a', 'missing'] }, value.events)).toBeUndefined()
|
|
58
|
-
expect(buildSemanticRoutePrompt('problem', value.events, 5)).toContain('candidateCallIds')
|
|
59
|
-
})
|
|
60
|
-
it('resolves candidates emitted via PTC mode tool/code-dispatch', () => {
|
|
61
|
-
const value = session()
|
|
62
|
-
value.append('tool/code-dispatch', { subCallId: 'c-1' as never, name: 'subagent', arguments: '{}', isError: false, content: [{ type: 'text', text: 'candidate 1 content' }] })
|
|
63
|
-
value.append('tool/code-dispatch', { subCallId: 'c-2' as never, name: 'subagent', arguments: '{}', isError: false, content: [{ type: 'text', text: 'candidate 2 content' }] })
|
|
64
|
-
expect(semanticRouteHint(value.events)).toBe(true)
|
|
65
|
-
const parsed = parseSemanticRoute(JSON.stringify({ kind: 'compare', confidence: 0.92, reason: 'PTC alternatives', candidateCallIds: ['c-1', 'c-2'], checkpointSeqs: [] }))!
|
|
66
|
-
expect(semanticDecision(parsed, value.events)).toMatchObject({ kind: 'compare', source: 'semantic' })
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
it('
|
|
79
|
-
const value = session()
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
expect(
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { Session } from '@deepseek-ai/dsh-session'
|
|
3
|
+
import { createToolResultMessage, createUserMessage } from '@deepseek-ai/dsh-llm'
|
|
4
|
+
import { analyzeStructuredRoute, AutoVerifierRouter, buildSemanticRoutePrompt, parseSemanticRoute, semanticDecision, semanticRouteHint, type RouterPolicy } from './router.ts'
|
|
5
|
+
|
|
6
|
+
function session() {
|
|
7
|
+
const value = Session.create('session-00000000-0000-4000-8000-000000000077' as never)
|
|
8
|
+
value.append('user/message', createUserMessage({ content: [{ type: 'text', text: 'Choose and implement the best solution' }], source: { kind: 'user' } }), { surfaceOp: 'append' })
|
|
9
|
+
return value
|
|
10
|
+
}
|
|
11
|
+
function tool(value: ReturnType<typeof session>, name: string, id: string, text: string, turn = 1, step = 1) {
|
|
12
|
+
value.append('tool/call', { turn, step, callId: id as never, name, arguments: '{}' })
|
|
13
|
+
value.append('tool/result', { turn, step, message: createToolResultMessage({ callId: id as never, content: [{ type: 'text', text }], isError: false }) }, { surfaceOp: 'append' })
|
|
14
|
+
}
|
|
15
|
+
const policy: RouterPolicy = { mode: 'smart', minConfidence: .9, maxCandidates: 8, maxPerTask: 5, maxPerSession: 20, maxModelCallsPerTask: 48, maxModelCallsPerSession: 160, maxInputChars: 60000, maxItemChars: 20000 }
|
|
16
|
+
const envelope = (count: number) => JSON.stringify({ protocol: 'dsh-verifier-candidates', version: 1, groupId: 'auth', candidates: Array.from({ length: count }, (_, i) => ({ id: String(i + 1), label: 'C' + (i + 1), status: 'completed', content: 'candidate ' + (i + 1) })) })
|
|
17
|
+
|
|
18
|
+
describe('production structured routing', () => {
|
|
19
|
+
it('requires a trusted versioned workflow envelope', () => {
|
|
20
|
+
const value = session(); tool(value, 'workflow', 'w', envelope(3))
|
|
21
|
+
expect(analyzeStructuredRoute(value.events)).toMatchObject({ kind: 'select', source: 'structured' })
|
|
22
|
+
const untrusted = session(); tool(untrusted, 'workflow', 'w', JSON.stringify({ verifier_candidates: ['a', 'b'] }))
|
|
23
|
+
expect(analyzeStructuredRoute(untrusted.events)).toBeUndefined()
|
|
24
|
+
})
|
|
25
|
+
it('does not guess that unrelated synchronous subagents are alternatives', () => {
|
|
26
|
+
const value = session(); tool(value, 'subagent', 'a', 'frontend analysis', 1, 2); tool(value, 'subagent', 'b', 'backend analysis', 1, 2)
|
|
27
|
+
expect(analyzeStructuredRoute(value.events)).toBeUndefined()
|
|
28
|
+
expect(semanticRouteHint(value.events)).toBe(true)
|
|
29
|
+
})
|
|
30
|
+
it('deduplicates identical todo snapshots', () => {
|
|
31
|
+
const value = session()
|
|
32
|
+
const todos = [{ content: 'Implement', status: 'in_progress' as const }, { content: 'Test', status: 'pending' as const }]
|
|
33
|
+
value.append('todo/write', { todos }); value.append('todo/write', { todos })
|
|
34
|
+
expect(analyzeStructuredRoute(value.events)).toBeUndefined()
|
|
35
|
+
value.append('todo/write', { todos: [{ content: 'Implement', status: 'completed' }, { content: 'Test', status: 'in_progress' }] })
|
|
36
|
+
expect(analyzeStructuredRoute(value.events)).toMatchObject({ kind: 'track', evidenceSeqs: expect.any(Array) })
|
|
37
|
+
})
|
|
38
|
+
it('redacts and bounds trusted candidate content', () => {
|
|
39
|
+
const value = session(); tool(value, 'workflow', 'w', JSON.stringify({ protocol: 'dsh-verifier-candidates', version: 1, groupId: 'g', candidates: [{ id: 'a', status: 'completed', content: 'token = abc ' + 'x'.repeat(100) }, { id: 'b', status: 'completed', content: 'password: secret ' + 'y'.repeat(100) }] }))
|
|
40
|
+
const decision = analyzeStructuredRoute(value.events, 8, 40)
|
|
41
|
+
expect(decision?.kind).toBe('compare')
|
|
42
|
+
if (decision?.kind === 'compare') { expect(decision.candidates[0].content).not.toContain('abc'); expect(decision.candidates[0].content.length).toBeLessThan(80) }
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
describe('semantic evidence references', () => {
|
|
47
|
+
it('accepts exact strict JSON references and rejects prose/unknown keys', () => {
|
|
48
|
+
const valid = JSON.stringify({ kind: 'compare', confidence: .95, reason: 'alternatives', candidateCallIds: ['a', 'b'], checkpointSeqs: [] })
|
|
49
|
+
expect(parseSemanticRoute(valid)).toMatchObject({ kind: 'compare', candidateCallIds: ['a', 'b'] })
|
|
50
|
+
expect(parseSemanticRoute('```json\n' + valid + '\n```')).toBeUndefined()
|
|
51
|
+
expect(parseSemanticRoute(JSON.stringify({ ...JSON.parse(valid), extra: true }))).toBeUndefined()
|
|
52
|
+
})
|
|
53
|
+
it('resolves only real paired call ids', () => {
|
|
54
|
+
const value = session(); tool(value, 'subagent', 'a', 'candidate A'); tool(value, 'subagent', 'b', 'candidate B')
|
|
55
|
+
const parsed = parseSemanticRoute(JSON.stringify({ kind: 'compare', confidence: .95, reason: 'same task alternatives', candidateCallIds: ['a', 'b'], checkpointSeqs: [] }))!
|
|
56
|
+
expect(semanticDecision(parsed, value.events)).toMatchObject({ kind: 'compare', source: 'semantic' })
|
|
57
|
+
expect(semanticDecision({ ...parsed, candidateCallIds: ['a', 'missing'] }, value.events)).toBeUndefined()
|
|
58
|
+
expect(buildSemanticRoutePrompt('problem', value.events, 5)).toContain('candidateCallIds')
|
|
59
|
+
})
|
|
60
|
+
it('resolves candidates emitted via PTC mode tool/code-dispatch', () => {
|
|
61
|
+
const value = session()
|
|
62
|
+
value.append('tool/code-dispatch', { subCallId: 'c-1' as never, name: 'subagent', arguments: '{}', isError: false, content: [{ type: 'text', text: 'candidate 1 content' }] })
|
|
63
|
+
value.append('tool/code-dispatch', { subCallId: 'c-2' as never, name: 'subagent', arguments: '{}', isError: false, content: [{ type: 'text', text: 'candidate 2 content' }] })
|
|
64
|
+
expect(semanticRouteHint(value.events)).toBe(true)
|
|
65
|
+
const parsed = parseSemanticRoute(JSON.stringify({ kind: 'compare', confidence: 0.92, reason: 'PTC alternatives', candidateCallIds: ['c-1', 'c-2'], checkpointSeqs: [] }))!
|
|
66
|
+
expect(semanticDecision(parsed, value.events)).toMatchObject({ kind: 'compare', source: 'semantic' })
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('resolves candidates emitted via Session V3 tool/ptc-dispatch', () => {
|
|
70
|
+
const value = session()
|
|
71
|
+
value.append('tool/ptc-dispatch' as never, { subCallId: 'ptc-1', name: 'subagent', arguments: '{}', isError: false, content: [{ type: 'text', text: 'candidate A from ptc' }] } as never)
|
|
72
|
+
value.append('tool/ptc-dispatch' as never, { subCallId: 'ptc-2', name: 'subagent', arguments: '{}', isError: false, content: [{ type: 'text', text: 'candidate B from ptc' }] } as never)
|
|
73
|
+
expect(semanticRouteHint(value.events)).toBe(true)
|
|
74
|
+
const parsed = parseSemanticRoute(JSON.stringify({ kind: 'compare', confidence: 0.95, reason: 'V3 PTC alternatives', candidateCallIds: ['ptc-1', 'ptc-2'], checkpointSeqs: [] }))!
|
|
75
|
+
expect(semanticDecision(parsed, value.events)).toMatchObject({ kind: 'compare', source: 'semantic' })
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('routes progress tracking based on changed team/task snapshots', () => {
|
|
79
|
+
const value = session()
|
|
80
|
+
value.append('team/task' as never, { task: { id: 'task-1', revision: 1, subject: 'Backend API', status: 'in_progress' } } as never)
|
|
81
|
+
value.append('team/task' as never, { task: { id: 'task-1', revision: 2, subject: 'Backend API', status: 'completed' } } as never)
|
|
82
|
+
expect(semanticRouteHint(value.events)).toBe(true)
|
|
83
|
+
const decision = analyzeStructuredRoute(value.events)
|
|
84
|
+
expect(decision).toMatchObject({ kind: 'track', source: 'structured', reason: 'changed durable team tasks' })
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
describe('transactional router state', () => {
|
|
89
|
+
it('commits, requires final verification, and clears it only after final commit', () => {
|
|
90
|
+
const value = session(); const agent = { id: value.id, session: value }; const router = new AutoVerifierRouter()
|
|
91
|
+
const route = router.reserve(agent, 'compare', 'route', 4, policy)!
|
|
92
|
+
expect(router.commit(agent, route, 9)).toBe(true); expect(router.finalRequired(agent)).toBe(9)
|
|
93
|
+
const final = router.reserve(agent, 'final', 'final', 4, policy)!
|
|
94
|
+
expect(router.commit(agent, final)).toBe(true); expect(router.finalRequired(agent)).toBeUndefined()
|
|
95
|
+
})
|
|
96
|
+
it('releases in-flight state after failure and strict remains blocked', () => {
|
|
97
|
+
const value = session(); const agent = { id: value.id, session: value }; const router = new AutoVerifierRouter()
|
|
98
|
+
const first = router.reserve(agent, 'compare', 'route-a', 4, { ...policy, mode: 'strict' })!
|
|
99
|
+
router.fail(agent, first, true); expect(router.strictBlocked(agent)).toBe(true)
|
|
100
|
+
expect(router.reserve(agent, 'compare', 'route-b', 4, { ...policy, mode: 'strict' })).toBeDefined()
|
|
101
|
+
})
|
|
102
|
+
it('enforces manual mode and unified model-call budgets', () => {
|
|
103
|
+
const value = session(); const agent = { id: value.id, session: value }; const router = new AutoVerifierRouter()
|
|
104
|
+
expect(router.reserve(agent, 'compare', 'manual', 1, { ...policy, mode: 'manual' })).toBeUndefined()
|
|
105
|
+
expect(router.reserve(agent, 'select', 'expensive', 49, policy)).toBeUndefined()
|
|
106
|
+
})
|
|
107
|
+
})
|