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.
Files changed (90) hide show
  1. package/LICENSE +22 -22
  2. package/README.md +328 -301
  3. package/cordis.patch.yml +4 -4
  4. package/lib/caller-BWwSVNZf.js.map +1 -0
  5. package/lib/client.js +176 -14
  6. package/lib/client.js.map +1 -0
  7. package/lib/core.js.map +1 -0
  8. package/lib/index.js +400 -51
  9. package/lib/index.js.map +1 -0
  10. package/lib/types/auto.d.ts.map +1 -0
  11. package/lib/types/auto.js +8 -2
  12. package/lib/types/auto.js.map +1 -0
  13. package/lib/types/cache.d.ts.map +1 -0
  14. package/lib/types/cache.js.map +1 -0
  15. package/lib/types/caller.d.ts.map +1 -0
  16. package/lib/types/caller.js.map +1 -0
  17. package/lib/types/client-i18n.d.ts +27 -0
  18. package/lib/types/client-i18n.d.ts.map +1 -0
  19. package/lib/types/client-i18n.js +18 -0
  20. package/lib/types/client-i18n.js.map +1 -0
  21. package/lib/types/client.d.ts +15 -2
  22. package/lib/types/client.d.ts.map +1 -0
  23. package/lib/types/client.js +137 -10
  24. package/lib/types/client.js.map +1 -0
  25. package/lib/types/config.d.ts +4 -0
  26. package/lib/types/config.d.ts.map +1 -0
  27. package/lib/types/config.js +5 -3
  28. package/lib/types/config.js.map +1 -0
  29. package/lib/types/core.d.ts.map +1 -0
  30. package/lib/types/core.js.map +1 -0
  31. package/lib/types/engine.d.ts.map +1 -0
  32. package/lib/types/engine.js.map +1 -0
  33. package/lib/types/images.d.ts.map +1 -0
  34. package/lib/types/images.js.map +1 -0
  35. package/lib/types/index.d.ts +2 -0
  36. package/lib/types/index.d.ts.map +1 -0
  37. package/lib/types/index.js +162 -9
  38. package/lib/types/index.js.map +1 -0
  39. package/lib/types/plan-gate.d.ts +27 -0
  40. package/lib/types/plan-gate.d.ts.map +1 -0
  41. package/lib/types/plan-gate.js +60 -0
  42. package/lib/types/plan-gate.js.map +1 -0
  43. package/lib/types/router.d.ts +28 -32
  44. package/lib/types/router.d.ts.map +1 -0
  45. package/lib/types/router.js +41 -9
  46. package/lib/types/router.js.map +1 -0
  47. package/lib/types/session.d.ts +11 -0
  48. package/lib/types/session.d.ts.map +1 -0
  49. package/lib/types/session.js +41 -11
  50. package/lib/types/session.js.map +1 -0
  51. package/lib/types/statistics.d.ts.map +1 -0
  52. package/lib/types/statistics.js.map +1 -0
  53. package/lib/types/team-gate.d.ts +20 -0
  54. package/lib/types/team-gate.d.ts.map +1 -0
  55. package/lib/types/team-gate.js +59 -0
  56. package/lib/types/team-gate.js.map +1 -0
  57. package/lib/types/top-logprobs.d.ts.map +1 -0
  58. package/lib/types/top-logprobs.js.map +1 -0
  59. package/lib/types/topic-storage.d.ts.map +1 -0
  60. package/lib/types/topic-storage.js.map +1 -0
  61. package/package.json +114 -112
  62. package/src/auto.test.ts +95 -86
  63. package/src/auto.ts +140 -133
  64. package/src/cache.ts +115 -115
  65. package/src/caller.test.ts +34 -34
  66. package/src/caller.ts +151 -151
  67. package/src/client-i18n.ts +18 -0
  68. package/src/client.tsx +383 -213
  69. package/src/config.ts +189 -183
  70. package/src/core.test.ts +71 -71
  71. package/src/core.ts +205 -205
  72. package/src/engine.test.ts +136 -136
  73. package/src/engine.ts +140 -140
  74. package/src/images.ts +33 -33
  75. package/src/index.ts +394 -247
  76. package/src/parity.test.ts +85 -85
  77. package/src/plan-gate.test.ts +56 -0
  78. package/src/plan-gate.ts +66 -0
  79. package/src/router.test.ts +107 -89
  80. package/src/router.ts +361 -309
  81. package/src/session.test.ts +71 -36
  82. package/src/session.ts +132 -103
  83. package/src/statistics.test.ts +80 -80
  84. package/src/statistics.ts +328 -328
  85. package/src/team-gate.test.ts +92 -0
  86. package/src/team-gate.ts +80 -0
  87. package/src/top-logprobs.test.ts +84 -84
  88. package/src/top-logprobs.ts +153 -153
  89. package/src/topic-storage.test.ts +31 -31
  90. package/src/topic-storage.ts +42 -42
package/src/index.ts CHANGED
@@ -1,247 +1,394 @@
1
- import type { Context } from '@deepseek-ai/cordis'
2
- import type { HostConnectionHandle } from '@deepseek-ai/dsh-client-connection'
3
- import { defineTool } from '@deepseek-ai/dsh-tools'
4
- import { createUserMessage } from '@deepseek-ai/dsh-llm'
5
- import type { Agent } from '@deepseek-ai/dsh-agent'
6
- import type { AttachmentStore, ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
7
- import type { SessionHeader } from '@deepseek-ai/dsh-session'
8
- import { Config, installVerifierSettings, resolveConfig } from './config.ts'
9
- import { RequestLimiter, callVerifierText } from './caller.ts'
10
- import { TopLogprobCapabilityCache, resolveCapabilityFile } from './top-logprobs.ts'
11
- import { ScoreCache, SingleFlight, resolveCacheFile, stableHash, type CachedPairScore } from './cache.ts'
12
- import { VerifierEngine, normalizeCriteria, type RunStats } from './engine.ts'
13
- import { loadVerifierImages } from './images.ts'
14
- import { extractSession } from './session.ts'
15
- import { AutoVerificationBudget, analyzeAutoTask, automaticFeedback } from './auto.ts'
16
- import { AutoVerifierRouter, analyzeStructuredRoute, boundDecision, buildSemanticRoutePrompt, parseSemanticRoute, semanticDecision, semanticRouteHint, type RouteDecision } from './router.ts'
17
- import { StatisticsStore, emptyRunStats, errorDetails, mergeStatisticsOverviews, resolveStatisticsFile, type StatisticsOverview, type StatisticsQuery, type VerifierToolName } from './statistics.ts'
18
- import { resolveTopicDataDir, type SessionArtifactLocator } from './topic-storage.ts'
19
-
20
- export const name = 'llm-verifier'
21
- export const inject = ['tools', 'agents', 'attachments', 'llm', 'connection', 'sessionPersistence']
22
- export { Config }
23
- export * from './core.ts'
24
- export * from './engine.ts'
25
- export * from './cache.ts'
26
- export * from './statistics.ts'
27
- export * from './topic-storage.ts'
28
- export * from './auto.ts'
29
- export * from './router.ts'
30
- export { callVerifier, RequestLimiter, type VerifierClientConfig, type VerifierImage, type UsageStats, type VerifierCompletion } from './caller.ts'
31
-
32
- const criterionSchema = { type: 'object' as const, additionalProperties: false, properties: { id: { type: 'string' as const, required: true as const }, name: { type: 'string' as const, required: true as const }, description: { type: 'string' as const, required: true as const } } }
33
- const statsSchema = { type: 'object' as const, additionalProperties: false, properties: { calls: { type: 'integer' as const, required: true as const }, attempts: { type: 'integer' as const, required: true as const }, retries: { type: 'integer' as const, required: true as const }, inputTokens: { type: 'integer' as const, required: true as const }, cachedInputTokens: { type: 'integer' as const, required: true as const }, outputTokens: { type: 'integer' as const, required: true as const }, reasoningTokens: { type: 'integer' as const, required: true as const }, cacheHits: { type: 'integer' as const, required: true as const }, cacheMisses: { type: 'integer' as const, required: true as const }, estimatedCostUsd: { type: 'number' as const, required: true as const }, topLogprobScores: { type: 'integer' as const, required: true as const }, explicitTagScores: { type: 'integer' as const, required: true as const } } }
34
- const criterionResultSchema = { type: 'object' as const, additionalProperties: false, properties: { id: { type: 'string' as const, required: true as const }, name: { type: 'string' as const, required: true as const }, scoreA: { type: 'number' as const, required: true as const }, scoreB: { type: 'number' as const, required: true as const } } }
35
- const commonParams = { criteria: { type: 'array' as const, items: criterionSchema }, repeats: { type: 'integer' as const }, images: { type: 'array' as const, items: { type: 'string' as const }, description: 'Optional HTTPS or data:image/...;base64 images. The selected DSH model must accept image input.' } }
36
- function renderJson(value: unknown) { return [{ type: 'text' as const, text: JSON.stringify(value, null, 2) }] }
37
- function positive(value: number | undefined, fallback: number, field: string): number { const result = value ?? fallback; if (!Number.isSafeInteger(result) || result <= 0) throw new Error('llm-verifier: ' + field + ' must be a positive integer'); return result }
38
- function numberField(value: unknown, fallback: number): number { return typeof value === 'number' && Number.isFinite(value) ? value : fallback }
39
- function statsFrom(value: unknown): RunStats { if (typeof value !== 'object' || value === null || !('stats' in value)) return emptyRunStats(); const source = (value as { stats?: unknown }).stats; if (typeof source !== 'object' || source === null) return emptyRunStats(); const row = source as Record<string, unknown>; return { calls: numberField(row.calls, 0), attempts: numberField(row.attempts, 0), retries: numberField(row.retries, 0), inputTokens: numberField(row.inputTokens, 0), cachedInputTokens: numberField(row.cachedInputTokens, 0), outputTokens: numberField(row.outputTokens, 0), reasoningTokens: numberField(row.reasoningTokens, 0), cacheHits: numberField(row.cacheHits, 0), cacheMisses: numberField(row.cacheMisses, 0), estimatedCostUsd: numberField(row.estimatedCostUsd, 0), topLogprobScores: numberField(row.topLogprobScores, 0), explicitTagScores: numberField(row.explicitTagScores, 0) } }
40
- function rpcSuccess<T>(value: T) { return { ok: true as const, value } }
41
- function rpcFailure(message: string) { return { ok: false as const, error: { code: 'bad-request' as const, message, details: { issues: [] } } } }
42
-
43
- interface SessionVerificationOptions { fromSeq?: number; toSeq?: number; includeAssistantText?: boolean; redactPatterns?: readonly string[]; maxChars?: number; repeats?: number }
44
- interface SessionVerificationResult { sessionId: string; problem: string; score: number; baselineScore: number; winner: 'A' | 'B' | 'tie'; fromSeq: number; toSeq: number; omittedCharacters: number; calls: number; stats: RunStats }
45
-
46
- export function apply(ctx: Context, config: Config = {}): void {
47
- const services = ctx as Context & { attachments: AttachmentStore; connection: HostConnectionHandle; sessionPersistence: SessionArtifactLocator & { list(signal?: AbortSignal): Promise<readonly unknown[]> } }
48
- const entry = resolveConfig(config)
49
- let limiter = new RequestLimiter(entry.maxConcurrency)
50
- const current = installVerifierSettings(ctx, entry, () => { limiter = new RequestLimiter(current().maxConcurrency) })
51
- const autoBudget = new AutoVerificationBudget()
52
- const autoRouter = new AutoVerifierRouter()
53
- const topics = new Map<string, { dataDir: string; cache: ScoreCache; capabilities: TopLogprobCapabilityCache; flights: SingleFlight<{ value: CachedPairScore; hit: boolean }>; statistics: StatisticsStore }>()
54
- const topic = (header: SessionHeader) => {
55
- const selected = current()
56
- const dataDir = resolveTopicDataDir(services.sessionPersistence, header, selected.cacheDir)
57
- const id = String(header.id)
58
- const existing = topics.get(id)
59
- if (existing?.dataDir === dataDir) return existing
60
- const cacheFile = resolveCacheFile(dataDir)
61
- const created = { dataDir, cache: new ScoreCache(cacheFile, selected.cacheMaxEntries), capabilities: new TopLogprobCapabilityCache(resolveCapabilityFile(dataDir)), flights: new SingleFlight<{ value: CachedPairScore; hit: boolean }>(), statistics: new StatisticsStore(resolveStatisticsFile(cacheFile)) }
62
- topics.set(id, created)
63
- return created
64
- }
65
- const requireAgent = (agent: Agent | undefined): Agent => {
66
- const selected = agent ?? ctx.agents.currentInitiator()
67
- if (selected === undefined) throw new Error('llm-verifier: verifier tools require an agent-owned topic so their data can follow topic deletion')
68
- return selected
69
- }
70
- const engine = async (agent: Agent) => {
71
- const selected = current()
72
- await ctx.llm.resolveCallConfig({ provider: selected.provider, model: selected.model, ...(selected.reasoningEffort ? { reasoningEffort: selected.reasoningEffort as never } : {}), maxTokens: selected.maxTokens })
73
- const topicEntry = topic(agent.session.header)
74
- return { verifier: new VerifierEngine({ ...selected, ctx, llm: ctx.llm, attachments: services.attachments, topLogprobCapabilities: topicEntry.capabilities, limiter }, selected.maxConcurrency, topicEntry.cache, { input: selected.estimatedInputUsdPerMillion, output: selected.estimatedOutputUsdPerMillion }, topicEntry.flights), selected }
75
- }
76
- const images = (values: readonly string[] | undefined, signal: AbortSignal) => loadVerifierImages(values, signal)
77
- const route = (selected: { provider: string; model: string }) => ({ provider: selected.provider, model: selected.model })
78
- const requireEnabled = () => { if (!current().enabled) throw new Error('llm-verifier: verifier tools are disabled — enable them in Settings → LLM Verifier') }
79
- const record = async <T>(toolName: VerifierToolName, agent: Agent, operation: () => Promise<{ result: T; selected: { provider: string; model: string } }>): Promise<T & { provider: string; model: string }> => {
80
- const startedAt = Date.now()
81
- let selected: { provider: string; model: string } = current()
82
- const statistics = topic(agent.session.header).statistics
83
- try {
84
- const completed = await operation()
85
- selected = completed.selected
86
- const value = { ...completed.result as T & object, ...route(selected) } as T & { provider: string; model: string }
87
- await statistics.record({ toolName, sessionId: String(agent.id), startedAt, success: true, provider: selected.provider, model: selected.model, stats: statsFrom(value) }).catch(() => {})
88
- return value
89
- } catch (error) {
90
- const details = errorDetails(error)
91
- await statistics.record({ toolName, sessionId: String(agent.id), startedAt, success: false, ...details, provider: selected.provider, model: selected.model, stats: emptyRunStats() }).catch(() => {})
92
- throw error
93
- }
94
- }
95
- const verifySession = async (agent: Agent, options: SessionVerificationOptions, signal: AbortSignal): Promise<SessionVerificationResult & { provider: string; model: string }> => record('verifier_current_session', agent, async () => {
96
- const extracted = await extractSession(agent, async (ref: ImageAttachmentRef) => { const stored = await services.attachments.readImage(ref, signal); return { data: stored.data, mediaType: stored.ref.mediaType } }, { fromSeq: options.fromSeq, toSeq: options.toSeq, includeAssistantText: options.includeAssistantText, redactPatterns: options.redactPatterns, maxChars: options.maxChars })
97
- const { verifier, selected } = await engine(agent)
98
- const compared = await verifier.compare({ problem: extracted.problem, candidateA: extracted.trace, candidateB: '(No useful work or verification was performed.)', repeats: positive(options.repeats, 2, 'repeats'), images: extracted.images }, signal)
99
- const result: SessionVerificationResult = { sessionId: extracted.sessionId, problem: extracted.problem, score: compared.scoreA, baselineScore: compared.scoreB, winner: compared.winner, fromSeq: extracted.fromSeq, toSeq: extracted.toSeq, omittedCharacters: extracted.omittedCharacters, calls: compared.calls, stats: compared.stats }
100
- return { result, selected }
101
- })
102
- const compareCandidates = async (agent: Agent, problem: string, candidateA: string, candidateB: string, repeats: number, signal: AbortSignal, routedImages: readonly import('./caller.ts').VerifierImage[] = []) => record('verifier_compare', agent, async () => {
103
- const { verifier, selected } = await engine(agent)
104
- return { result: await verifier.compare({ problem, candidateA, candidateB, repeats, images: routedImages }, signal), selected }
105
- })
106
- const selectCandidates = async (agent: Agent, problem: string, candidates: readonly string[], repeats: number, signal: AbortSignal, routedImages: readonly import('./caller.ts').VerifierImage[] = []) => record('verifier_select', agent, async () => {
107
- const { verifier, selected } = await engine(agent)
108
- return { result: await verifier.select({ problem, candidates, repeats, pivots: Math.min(2, candidates.length), seed: 0, images: routedImages }, signal), selected }
109
- })
110
- const trackProgress = async (agent: Agent, problem: string, steps: readonly string[], checkpoints: readonly number[], repeats: number, signal: AbortSignal, routedImages: readonly import('./caller.ts').VerifierImage[] = []) => record('verifier_track', agent, async () => {
111
- const { verifier, selected } = await engine(agent)
112
- return { result: await verifier.track(problem, steps, checkpoints, repeats, signal, routedImages), selected }
113
- })
114
- const classifyRoute = async (agent: Agent, prompt: string, signal: AbortSignal) => record('verifier_route_classify', agent, async () => {
115
- const { verifier, selected } = await engine(agent)
116
- const completion = await callVerifierText(verifier.client, prompt, signal)
117
- const stats: RunStats = { ...completion.usage, cacheHits: 0, cacheMisses: 0, estimatedCostUsd: ((completion.usage.inputTokens + completion.usage.cachedInputTokens) * selected.estimatedInputUsdPerMillion + completion.usage.outputTokens * selected.estimatedOutputUsdPerMillion) / 1_000_000, topLogprobScores: 0, explicitTagScores: 1 }
118
- return { result: { text: completion.text, stats }, selected }
119
- })
120
- const extractTask = async (agent: Agent, fromSeq: number, toSeq: number, maxChars: number, signal: AbortSignal) => extractSession(agent, async (ref: ImageAttachmentRef) => { const stored = await services.attachments.readImage(ref, signal); return { data: stored.data, mediaType: stored.ref.mediaType } }, { fromSeq, toSeq, includeAssistantText: true, maxChars })
121
- const routePolicy = (selected: ReturnType<typeof current>) => ({ mode: selected.autoVerifyMode, minConfidence: selected.autoRouteMinConfidence, maxCandidates: selected.autoRouteMaxCandidates, maxPerTask: selected.autoRouteMaxPerTask + selected.autoVerifyMaxPerTask, maxPerSession: selected.autoRouteMaxPerSession + selected.autoVerifyMaxPerSession, maxModelCallsPerTask: selected.autoMaxModelCallsPerTask, maxModelCallsPerSession: selected.autoMaxModelCallsPerSession, maxInputChars: selected.autoRouteMaxInputChars, maxItemChars: selected.autoRouteMaxItemChars })
122
- const routeFeedback = (decision: RouteDecision, detail: string) => createUserMessage({ content: [{ type: 'text' as const, text: '[Automatic verifier routing: ' + decision.kind + ']\n' + detail + '\nUse this independent result to continue the actual task. Do not merely restate the ranking or progress score; implement, correct, and verify the required work.' }], source: { kind: 'plugin' as const, plugin: 'dsh-llm-verifier', form: 'notice' as const, summary: 'Automatic verifier routed ' + decision.kind } })
123
-
124
- services.connection.rpc.handle('/llm-verifier', async (endpoint: string, payload: unknown) => {
125
- if (endpoint !== 'statistics') return rpcFailure('unknown llm-verifier endpoint')
126
- if (typeof payload !== 'object' || payload === null) return rpcFailure('statistics payload must be an object')
127
- const row = payload as Record<string, unknown>
128
- const sessionId = typeof row.sessionId === 'string' && row.sessionId.length > 0 ? row.sessionId : undefined
129
- const query: StatisticsQuery = { fromMs: numberField(row.fromMs, Number.NaN), toMs: numberField(row.toMs, Number.NaN), timezoneOffsetMinutes: numberField(row.timezoneOffsetMinutes, 0), recentLimit: numberField(row.recentLimit, 40), ...(sessionId ? { sessionId } : {}) }
130
- try {
131
- const items = await services.sessionPersistence.list()
132
- const headers: SessionHeader[] = items
133
- .map(item => item && typeof item === 'object' && 'header' in item ? (item as { header: SessionHeader }).header : item as SessionHeader)
134
- .filter(header => header !== undefined && (sessionId === undefined || String(header.id) === sessionId))
135
- const overviews = await Promise.all(headers.map(header => topic(header).statistics.overview(query)))
136
- const value: StatisticsOverview = mergeStatisticsOverviews(overviews, query)
137
- return rpcSuccess(value)
138
- } catch (error) { return rpcFailure(error instanceof Error ? error.message : String(error)) }
139
- }, { authority: 'loopback' } as never)
140
-
141
- ctx.on('agent/disposed', ({ agent }) => { autoBudget.release(agent); autoRouter.release(agent); topics.delete(String(agent.id)) })
142
- ctx.on('agent/turn-stopping', async ({ agent, signal }) => {
143
- const selected = current()
144
- if (!selected.enabled || selected.autoVerifyMode === 'manual' || signal.aborted) return
145
- const policy = routePolicy(selected)
146
- const evidence = analyzeAutoTask(agent.session.events, { mode: selected.autoVerifyMode, minToolCalls: selected.autoVerifyMinToolCalls, maxPerTask: selected.autoVerifyMaxPerTask, maxPerSession: selected.autoVerifyMaxPerSession })
147
- const admittedLastSeq = agent.session.events.at(-1)?.seq ?? -1
148
- const snapshot = agent.session.events.filter(event => event.seq <= admittedLastSeq)
149
- const stillCurrent = () => !signal.aborted && (agent.session.events.at(-1)?.seq ?? -1) === admittedLastSeq
150
- let decision = boundDecision(analyzeStructuredRoute(snapshot, selected.autoRouteMaxCandidates, selected.autoRouteMaxItemChars) as RouteDecision, policy)
151
-
152
- if (decision === undefined && selected.autoRouteSemantic && (selected.autoVerifyMode === 'strict' || semanticRouteHint(snapshot))) {
153
- const fingerprint = stableHash({ phase: 'semantic', from: evidence.taskStartSeq, to: admittedLastSeq, model: selected.provider + '/' + selected.model })
154
- const reservation = autoRouter.reserve(agent, 'semantic', fingerprint, 1, policy)
155
- if (reservation) {
156
- try {
157
- const extracted = await extractTask(agent, evidence.taskStartSeq, admittedLastSeq, selected.autoVerifyMaxChars, signal)
158
- const classified = await classifyRoute(agent, buildSemanticRoutePrompt(extracted.problem, snapshot, selected.autoRouteMaxCandidates, selected.autoRouteMaxItemChars), signal)
159
- if (!stillCurrent()) { autoRouter.fail(agent, reservation, false); return }
160
- const parsed = parseSemanticRoute(classified.text, selected.autoRouteMaxCandidates)
161
- if (!parsed) throw new Error('semantic router returned invalid strict JSON')
162
- decision = parsed.confidence >= selected.autoRouteMinConfidence ? boundDecision(semanticDecision(parsed, snapshot, selected.autoRouteMaxItemChars) as RouteDecision, policy) : undefined
163
- autoRouter.commit(agent, reservation)
164
- } catch (error) {
165
- autoRouter.fail(agent, reservation, selected.autoVerifyMode === 'strict')
166
- ctx.logger.warn('llm-verifier automatic classification failed: ' + (error instanceof Error ? error.message : String(error)))
167
- if (selected.autoVerifyMode === 'strict' && !signal.aborted) agent.steer(createUserMessage({ content: [{ type: 'text', text: '[Automatic verifier routing]\nStrict route classification failed: ' + (error instanceof Error ? error.message : String(error)) + '\nDo not conclude until directly relevant verification succeeds.' }], source: { kind: 'plugin', plugin: 'dsh-llm-verifier' } }))
168
- return
169
- }
170
- }
171
- }
172
-
173
- if (!stillCurrent()) return
174
- if (decision) {
175
- const expectedCalls = decision.kind === 'select' ? Math.max(1, decision.candidates.length * 8 * selected.autoVerifyRepeats) : Math.max(1, 4 * selected.autoVerifyRepeats)
176
- const reservation = autoRouter.reserve(agent, decision.kind, decision.fingerprint, expectedCalls, policy)
177
- if (reservation) {
178
- try {
179
- const extracted = await extractTask(agent, evidence.taskStartSeq, admittedLastSeq, selected.autoVerifyMaxChars, signal)
180
- if (decision.kind === 'compare') {
181
- const result = await compareCandidates(agent, extracted.problem, decision.candidates[0].content, decision.candidates[1].content, selected.autoVerifyRepeats, signal, extracted.images)
182
- if (!stillCurrent()) { autoRouter.fail(agent, reservation, false); return }
183
- if (!autoRouter.commit(agent, reservation, admittedLastSeq)) return
184
- const winner = result.winner === 'A' ? decision.candidates[0].label : result.winner === 'B' ? decision.candidates[1].label : 'tie'
185
- agent.steer(routeFeedback(decision, 'Winner: ' + winner + '. Scores: ' + (result.scoreA * 100).toFixed(1) + '% / ' + (result.scoreB * 100).toFixed(1) + '%.'))
186
- return
187
- }
188
- if (decision.kind === 'select') {
189
- const result = await selectCandidates(agent, extracted.problem, decision.candidates.map(candidate => candidate.content), selected.autoVerifyRepeats, signal, extracted.images)
190
- if (!stillCurrent()) { autoRouter.fail(agent, reservation, false); return }
191
- if (!autoRouter.commit(agent, reservation, admittedLastSeq)) return
192
- const ranking = result.ranking.map((index, rank) => (rank + 1) + '. ' + decision.candidates[index]!.label).join('\n')
193
- agent.steer(routeFeedback(decision, 'Ranking:\n' + ranking + '\nProceed with ' + decision.candidates[result.index]!.label + '.'))
194
- return
195
- }
196
- const result = await trackProgress(agent, extracted.problem, decision.steps, decision.checkpoints, selected.autoVerifyRepeats, signal, extracted.images)
197
- if (!stillCurrent()) { autoRouter.fail(agent, reservation, false); return }
198
- if (!autoRouter.commit(agent, reservation, admittedLastSeq)) return
199
- const detail = result.scores.map((score, index) => 'Checkpoint step ' + decision.checkpoints[index] + ': ' + (score * 100).toFixed(1) + '%').join('\n')
200
- const continuation = result.scores.some(score => score < selected.autoTrackCompletionThreshold) ? '\nContinue the unfinished work.' : '\nPrepare final delivery evidence; final session verification is mandatory.'
201
- agent.steer(routeFeedback(decision, detail + continuation))
202
- return
203
- } catch (error) {
204
- autoRouter.fail(agent, reservation, selected.autoVerifyMode === 'strict')
205
- ctx.logger.warn('llm-verifier automatic route failed: ' + (error instanceof Error ? error.message : String(error)))
206
- if (selected.autoVerifyMode === 'strict' && !signal.aborted) agent.steer(createUserMessage({ content: [{ type: 'text', text: '[Automatic verifier routing]\nStrict routed verification failed: ' + (error instanceof Error ? error.message : String(error)) + '\nDo not conclude until it succeeds.' }], source: { kind: 'plugin', plugin: 'dsh-llm-verifier' } }))
207
- return
208
- }
209
- }
210
- }
211
-
212
- const forcedFromSeq = autoRouter.finalRequired(agent)
213
- if (forcedFromSeq === undefined && !evidence.eligible) {
214
- if (selected.autoVerifyMode === 'strict' && autoRouter.strictBlocked(agent)) agent.steer(createUserMessage({ content: [{ type: 'text', text: '[Automatic verifier gate]\nStrict verification remains blocked. Produce new evidence or run a directly relevant verifier.' }], source: { kind: 'plugin', plugin: 'dsh-llm-verifier' } }))
215
- return
216
- }
217
- const finalFromSeq = forcedFromSeq === undefined ? evidence.taskStartSeq : Math.min(evidence.taskStartSeq, forcedFromSeq)
218
- const finalFingerprint = stableHash({ phase: 'final', from: finalFromSeq, to: admittedLastSeq })
219
- const finalReservation = autoRouter.reserve(agent, 'final', finalFingerprint, Math.max(1, 4 * selected.autoVerifyRepeats), policy)
220
- if (!finalReservation) {
221
- if (selected.autoVerifyMode === 'strict' && (forcedFromSeq !== undefined || autoRouter.strictBlocked(agent))) agent.steer(createUserMessage({ content: [{ type: 'text', text: '[Automatic verifier gate]\nStrict final verification is required but its safety budget is exhausted or another verifier is active. Do not conclude; request operator review.' }], source: { kind: 'plugin', plugin: 'dsh-llm-verifier' } }))
222
- return
223
- }
224
- try {
225
- const result = await verifySession(agent, { fromSeq: finalFromSeq, toSeq: admittedLastSeq, includeAssistantText: true, maxChars: selected.autoVerifyMaxChars, repeats: selected.autoVerifyRepeats }, signal)
226
- if (!stillCurrent()) { autoRouter.fail(agent, finalReservation, false); return }
227
- const passed = result.winner === 'A' && result.score >= selected.autoVerifyThreshold
228
- if (passed) autoRouter.commit(agent, finalReservation)
229
- else {
230
- autoRouter.fail(agent, finalReservation, selected.autoVerifyMode === 'strict')
231
- agent.steer(createUserMessage({ content: [{ type: 'text', text: automaticFeedback(result.score, result.baselineScore, result.winner, selected.autoVerifyThreshold) }], source: { kind: 'plugin', plugin: 'dsh-llm-verifier' } }))
232
- }
233
- } catch (error) {
234
- autoRouter.fail(agent, finalReservation, selected.autoVerifyMode === 'strict')
235
- ctx.logger.warn('llm-verifier automatic final verification failed: ' + (error instanceof Error ? error.message : String(error)))
236
- if (selected.autoVerifyMode === 'strict' && !signal.aborted) agent.steer(createUserMessage({ content: [{ type: 'text', text: '[Automatic verifier gate]\nStrict final verification failed: ' + (error instanceof Error ? error.message : String(error)) + '\nDo not conclude until verification succeeds.' }], source: { kind: 'plugin', plugin: 'dsh-llm-verifier' } }))
237
- }
238
- })
239
-
240
- ctx.tools.register(defineTool({ name: 'verifier_compare', description: 'Use autonomously when exactly two substantive answers, patches, plans, or execution trajectories need an independent evidence-based comparison and the choice is consequential or uncertain. Do not use for trivial deterministic questions or when there is only one candidate. Uses the verifier model selected in DSH Settings, with top-logprob A–T expectations when supported and explicit-tag fallback otherwise.', parameters: { problem: { type: 'string', required: true }, candidate_a: { type: 'string', required: true }, candidate_b: { type: 'string', required: true }, ...commonParams }, output: { schema: { type: 'object', additionalProperties: false, properties: { scoreA: { type: 'number', required: true }, scoreB: { type: 'number', required: true }, winner: { type: 'string', enum: ['A', 'B', 'tie'], required: true }, criteria: { type: 'array', items: criterionResultSchema, required: true }, calls: { type: 'integer', required: true }, stats: { ...statsSchema, required: true }, provider: { type: 'string', required: true }, model: { type: 'string', required: true } } }, render: (_args, value) => renderJson(value) }, timeoutMs: entry.timeoutMs * 20, async execute(args, exec) { requireEnabled(); const agent = requireAgent(exec.agent); return record('verifier_compare', agent, async () => { const { verifier, selected } = await engine(agent); const result = await verifier.compare({ problem: args.problem, candidateA: args.candidate_a, candidateB: args.candidate_b, criteria: normalizeCriteria(args.criteria), repeats: positive(args.repeats, 2, 'repeats'), images: await images(args.images, exec.signal) }, exec.signal); return { result, selected } }) } }))
241
-
242
- ctx.tools.register(defineTool({ name: 'verifier_select', description: 'Use autonomously when three or more substantive candidate answers, patches, plans, or trajectories must be ranked and an independent choice is valuable. Use verifier_compare for exactly two candidates; do not generate extra candidates merely to invoke this tool. Deterministic orchestrators should call this directly once they have three or more real candidates.', parameters: { problem: { type: 'string', required: true }, candidates: { type: 'array', items: { type: 'string' }, required: true }, ...commonParams, pivots: { type: 'integer' }, seed: { type: 'integer' } }, output: { schema: { type: 'object', additionalProperties: false, properties: { index: { type: 'integer', required: true }, best: { type: 'string', required: true }, scores: { type: 'array', items: { type: 'number' }, required: true }, ranking: { type: 'array', items: { type: 'integer' }, required: true }, pivots: { type: 'array', items: { type: 'integer' }, required: true }, comparisons: { type: 'integer', required: true }, calls: { type: 'integer', required: true }, stats: { ...statsSchema, required: true }, provider: { type: 'string', required: true }, model: { type: 'string', required: true } } }, render: (_args, value) => renderJson(value) }, timeoutMs: entry.timeoutMs * 100, async execute(args, exec) { requireEnabled(); const agent = requireAgent(exec.agent); return record('verifier_select', agent, async () => { const { verifier, selected } = await engine(agent); const result = await verifier.select({ problem: args.problem, candidates: args.candidates, criteria: normalizeCriteria(args.criteria), repeats: positive(args.repeats, 2, 'repeats'), pivots: positive(args.pivots, 2, 'pivots'), seed: args.seed ?? 0, images: await images(args.images, exec.signal) }, exec.signal); return { result, selected } }) } }))
243
-
244
- ctx.tools.register(defineTool({ name: 'verifier_track', description: 'Use autonomously for a genuinely multi-step task when progress at explicit checkpoints is uncertain or needs evidence-based measurement. Deterministic goal/workflow orchestrators should call this directly when real checkpoints already exist. Do not use for a single completed answer or invent checkpoints.', parameters: { problem: { type: 'string', required: true }, steps: { type: 'array', items: { type: 'string' }, required: true }, checkpoints: { type: 'array', items: { type: 'integer' }, required: true }, repeats: commonParams.repeats, images: commonParams.images }, output: { schema: { type: 'object', additionalProperties: false, properties: { scores: { type: 'array', items: { type: 'number' }, required: true }, perRepeat: { type: 'array', items: { type: 'array', items: { type: 'number' } }, required: true }, calls: { type: 'integer', required: true }, stats: { ...statsSchema, required: true }, provider: { type: 'string', required: true }, model: { type: 'string', required: true } } }, render: (_args, value) => renderJson(value) }, timeoutMs: entry.timeoutMs * 20, async execute(args, exec) { requireEnabled(); const agent = requireAgent(exec.agent); return record('verifier_track', agent, async () => { const { verifier, selected } = await engine(agent); const result = await verifier.track(args.problem, args.steps, args.checkpoints, positive(args.repeats, 2, 'repeats'), exec.signal, await images(args.images, exec.signal)); return { result, selected } }) } }))
245
-
246
- ctx.tools.register(defineTool({ name: 'verifier_current_session', description: 'Explicitly verify the current DSH session. Smart/strict policy can also invoke this gate automatically at the turn-stopping lifecycle boundary after consequential work with real tool evidence. Extracts the session, applies redaction and bounds, then sends the evidence to the configured verifier model.', parameters: { from_seq: { type: 'integer' }, to_seq: { type: 'integer' }, include_assistant_text: { type: 'boolean' }, redact_patterns: { type: 'array', items: { type: 'string' } }, max_chars: { type: 'integer' }, repeats: { type: 'integer' } }, output: { schema: { type: 'object', additionalProperties: false, properties: { sessionId: { type: 'string', required: true }, problem: { type: 'string', required: true }, score: { type: 'number', required: true }, baselineScore: { type: 'number', required: true }, winner: { type: 'string', enum: ['A', 'B', 'tie'], required: true }, fromSeq: { type: 'integer', required: true }, toSeq: { type: 'integer', required: true }, omittedCharacters: { type: 'integer', required: true }, calls: { type: 'integer', required: true }, stats: { ...statsSchema, required: true }, provider: { type: 'string', required: true }, model: { type: 'string', required: true } } }, render: (_args, value) => renderJson(value) }, timeoutMs: entry.timeoutMs * 20, async execute(args, exec) { requireEnabled(); const agent = requireAgent(exec.agent); return verifySession(agent, { fromSeq: args.from_seq, toSeq: args.to_seq, includeAssistantText: args.include_assistant_text, redactPatterns: args.redact_patterns, maxChars: args.max_chars, repeats: args.repeats }, exec.signal) } }))
247
- }
1
+ import type { Context } from '@deepseek-ai/cordis'
2
+ import type { HostConnectionHandle } from '@deepseek-ai/dsh-client-connection'
3
+ import { defineTool } from '@deepseek-ai/dsh-tools'
4
+ import { createUserMessage } from '@deepseek-ai/dsh-llm'
5
+ import type { Agent } from '@deepseek-ai/dsh-agent'
6
+ import type { AttachmentStore, ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
7
+ import type { SessionHeader } from '@deepseek-ai/dsh-session'
8
+ import { Config, installVerifierSettings, resolveConfig } from './config.ts'
9
+ import { RequestLimiter, callVerifierText } from './caller.ts'
10
+ import { TopLogprobCapabilityCache, resolveCapabilityFile } from './top-logprobs.ts'
11
+ import { ScoreCache, SingleFlight, resolveCacheFile, stableHash, type CachedPairScore } from './cache.ts'
12
+ import { VerifierEngine, normalizeCriteria, type RunStats } from './engine.ts'
13
+ import { loadVerifierImages } from './images.ts'
14
+ import { extractSession, sessionEvents } from './session.ts'
15
+ import { AutoVerificationBudget, analyzeAutoTask, automaticFeedback } from './auto.ts'
16
+ import { AutoVerifierRouter, analyzeStructuredRoute, boundDecision, buildSemanticRoutePrompt, parseSemanticRoute, semanticDecision, semanticRouteHint, type RouteDecision } from './router.ts'
17
+ import { buildPlanPreReviewPrompt, parseVerdictLetter, planFromArguments } from './plan-gate.ts'
18
+ import { inspectTeamTasks, buildTeamTaskVerificationPrompt } from './team-gate.ts'
19
+ import { StatisticsStore, emptyRunStats, errorDetails, mergeStatisticsOverviews, resolveStatisticsFile, type StatisticsOverview, type StatisticsQuery, type VerifierToolName } from './statistics.ts'
20
+ import { resolveTopicDataDir, type SessionArtifactLocator } from './topic-storage.ts'
21
+
22
+ export const name = 'llm-verifier'
23
+ export const inject = ['tools', 'agents', 'attachments', 'llm', 'connection', 'sessionPersistence']
24
+ export { Config }
25
+ export * from './core.ts'
26
+ export * from './engine.ts'
27
+ export * from './cache.ts'
28
+ export * from './statistics.ts'
29
+ export * from './topic-storage.ts'
30
+ export * from './auto.ts'
31
+ export * from './router.ts'
32
+ export * from './plan-gate.ts'
33
+ export * from './team-gate.ts'
34
+ export { callVerifier, RequestLimiter, type VerifierClientConfig, type VerifierImage, type UsageStats, type VerifierCompletion } from './caller.ts'
35
+
36
+ const criterionSchema = { type: 'object' as const, additionalProperties: false, properties: { id: { type: 'string' as const, required: true as const }, name: { type: 'string' as const, required: true as const }, description: { type: 'string' as const, required: true as const } } }
37
+ const statsSchema = { type: 'object' as const, additionalProperties: false, properties: { calls: { type: 'integer' as const, required: true as const }, attempts: { type: 'integer' as const, required: true as const }, retries: { type: 'integer' as const, required: true as const }, inputTokens: { type: 'integer' as const, required: true as const }, cachedInputTokens: { type: 'integer' as const, required: true as const }, outputTokens: { type: 'integer' as const, required: true as const }, reasoningTokens: { type: 'integer' as const, required: true as const }, cacheHits: { type: 'integer' as const, required: true as const }, cacheMisses: { type: 'integer' as const, required: true as const }, estimatedCostUsd: { type: 'number' as const, required: true as const }, topLogprobScores: { type: 'integer' as const, required: true as const }, explicitTagScores: { type: 'integer' as const, required: true as const } } }
38
+ const criterionResultSchema = { type: 'object' as const, additionalProperties: false, properties: { id: { type: 'string' as const, required: true as const }, name: { type: 'string' as const, required: true as const }, scoreA: { type: 'number' as const, required: true as const }, scoreB: { type: 'number' as const, required: true as const } } }
39
+ const commonParams = { criteria: { type: 'array' as const, items: criterionSchema }, repeats: { type: 'integer' as const }, images: { type: 'array' as const, items: { type: 'string' as const }, description: 'Optional HTTPS or data:image/...;base64 images. The selected DSH model must accept image input.' } }
40
+ function renderJson(value: unknown) { return [{ type: 'text' as const, text: JSON.stringify(value, null, 2) }] }
41
+ function positive(value: number | undefined, fallback: number, field: string): number { const result = value ?? fallback; if (!Number.isSafeInteger(result) || result <= 0) throw new Error('llm-verifier: ' + field + ' must be a positive integer'); return result }
42
+ function numberField(value: unknown, fallback: number): number { return typeof value === 'number' && Number.isFinite(value) ? value : fallback }
43
+ function statsFrom(value: unknown): RunStats { if (typeof value !== 'object' || value === null || !('stats' in value)) return emptyRunStats(); const source = (value as { stats?: unknown }).stats; if (typeof source !== 'object' || source === null) return emptyRunStats(); const row = source as Record<string, unknown>; return { calls: numberField(row.calls, 0), attempts: numberField(row.attempts, 0), retries: numberField(row.retries, 0), inputTokens: numberField(row.inputTokens, 0), cachedInputTokens: numberField(row.cachedInputTokens, 0), outputTokens: numberField(row.outputTokens, 0), reasoningTokens: numberField(row.reasoningTokens, 0), cacheHits: numberField(row.cacheHits, 0), cacheMisses: numberField(row.cacheMisses, 0), estimatedCostUsd: numberField(row.estimatedCostUsd, 0), topLogprobScores: numberField(row.topLogprobScores, 0), explicitTagScores: numberField(row.explicitTagScores, 0) } }
44
+ function rpcSuccess<T>(value: T) { return { ok: true as const, value } }
45
+ function rpcFailure(message: string) { return { ok: false as const, error: { code: 'bad-request' as const, message, details: { issues: [] } } } }
46
+
47
+ interface SessionVerificationOptions { fromSeq?: number; toSeq?: number; includeAssistantText?: boolean; redactPatterns?: readonly string[]; maxChars?: number; repeats?: number }
48
+ interface SessionVerificationResult { sessionId: string; problem: string; score: number; baselineScore: number; winner: 'A' | 'B' | 'tie'; fromSeq: number; toSeq: number; omittedCharacters: number; calls: number; stats: RunStats }
49
+
50
+ export function apply(ctx: Context, config: Config = {}): void {
51
+ const services = ctx as Context & { attachments: AttachmentStore; connection: HostConnectionHandle; sessionPersistence: SessionArtifactLocator & { list(signal?: AbortSignal): Promise<readonly unknown[]> } }
52
+ const entry = resolveConfig(config)
53
+ let limiter = new RequestLimiter(entry.maxConcurrency)
54
+ const current = installVerifierSettings(ctx, entry, () => { limiter = new RequestLimiter(current().maxConcurrency) })
55
+ const autoBudget = new AutoVerificationBudget()
56
+ const autoRouter = new AutoVerifierRouter()
57
+ const topics = new Map<string, { dataDir: string; cache: ScoreCache; capabilities: TopLogprobCapabilityCache; flights: SingleFlight<{ value: CachedPairScore; hit: boolean }>; statistics: StatisticsStore }>()
58
+ const topic = (header: SessionHeader) => {
59
+ const selected = current()
60
+ const dataDir = resolveTopicDataDir(services.sessionPersistence, header, selected.cacheDir)
61
+ const id = String(header.id)
62
+ const existing = topics.get(id)
63
+ if (existing?.dataDir === dataDir) return existing
64
+ const cacheFile = resolveCacheFile(dataDir)
65
+ const created = { dataDir, cache: new ScoreCache(cacheFile, selected.cacheMaxEntries), capabilities: new TopLogprobCapabilityCache(resolveCapabilityFile(dataDir)), flights: new SingleFlight<{ value: CachedPairScore; hit: boolean }>(), statistics: new StatisticsStore(resolveStatisticsFile(cacheFile)) }
66
+ topics.set(id, created)
67
+ return created
68
+ }
69
+ const requireAgent = (agent: Agent | undefined): Agent => {
70
+ const selected = agent ?? ctx.agents.currentInitiator()
71
+ if (selected === undefined) throw new Error('llm-verifier: verifier tools require an agent-owned topic so their data can follow topic deletion')
72
+ return selected
73
+ }
74
+ const engine = async (agent: Agent) => {
75
+ const selected = current()
76
+ await ctx.llm.resolveCallConfig({ provider: selected.provider, model: selected.model, ...(selected.reasoningEffort ? { reasoningEffort: selected.reasoningEffort as never } : {}), maxTokens: selected.maxTokens })
77
+ const topicEntry = topic(agent.session.header)
78
+ return { verifier: new VerifierEngine({ ...selected, ctx, llm: ctx.llm, attachments: services.attachments, topLogprobCapabilities: topicEntry.capabilities, limiter }, selected.maxConcurrency, topicEntry.cache, { input: selected.estimatedInputUsdPerMillion, output: selected.estimatedOutputUsdPerMillion }, topicEntry.flights), selected }
79
+ }
80
+ const images = (values: readonly string[] | undefined, signal: AbortSignal) => loadVerifierImages(values, signal)
81
+ const route = (selected: { provider: string; model: string }) => ({ provider: selected.provider, model: selected.model })
82
+ const requireEnabled = () => { if (!current().enabled) throw new Error('llm-verifier: verifier tools are disabled — enable them in Settings → LLM Verifier') }
83
+ const record = async <T>(toolName: VerifierToolName, agent: Agent, operation: () => Promise<{ result: T; selected: { provider: string; model: string } }>): Promise<T & { provider: string; model: string }> => {
84
+ const startedAt = Date.now()
85
+ let selected: { provider: string; model: string } = current()
86
+ const statistics = topic(agent.session.header).statistics
87
+ try {
88
+ const completed = await operation()
89
+ selected = completed.selected
90
+ const value = { ...completed.result as T & object, ...route(selected) } as T & { provider: string; model: string }
91
+ await statistics.record({ toolName, sessionId: String(agent.id), startedAt, success: true, provider: selected.provider, model: selected.model, stats: statsFrom(value) }).catch(() => {})
92
+ return value
93
+ } catch (error) {
94
+ const details = errorDetails(error)
95
+ await statistics.record({ toolName, sessionId: String(agent.id), startedAt, success: false, ...details, provider: selected.provider, model: selected.model, stats: emptyRunStats() }).catch(() => {})
96
+ throw error
97
+ }
98
+ }
99
+ const verifySession = async (agent: Agent, options: SessionVerificationOptions, signal: AbortSignal): Promise<SessionVerificationResult & { provider: string; model: string }> => record('verifier_current_session', agent, async () => {
100
+ const extracted = await extractSession(agent, async (ref: ImageAttachmentRef) => { const stored = await services.attachments.readImage(ref, signal); return { data: stored.data, mediaType: stored.ref.mediaType } }, { fromSeq: options.fromSeq, toSeq: options.toSeq, includeAssistantText: options.includeAssistantText, redactPatterns: options.redactPatterns, maxChars: options.maxChars })
101
+ const { verifier, selected } = await engine(agent)
102
+ const compared = await verifier.compare({ problem: extracted.problem, candidateA: extracted.trace, candidateB: '(No useful work or verification was performed.)', repeats: positive(options.repeats, 2, 'repeats'), images: extracted.images }, signal)
103
+ const result: SessionVerificationResult = { sessionId: extracted.sessionId, problem: extracted.problem, score: compared.scoreA, baselineScore: compared.scoreB, winner: compared.winner, fromSeq: extracted.fromSeq, toSeq: extracted.toSeq, omittedCharacters: extracted.omittedCharacters, calls: compared.calls, stats: compared.stats }
104
+ return { result, selected }
105
+ })
106
+ const compareCandidates = async (agent: Agent, problem: string, candidateA: string, candidateB: string, repeats: number, signal: AbortSignal, routedImages: readonly import('./caller.ts').VerifierImage[] = []) => record('verifier_compare', agent, async () => {
107
+ const { verifier, selected } = await engine(agent)
108
+ return { result: await verifier.compare({ problem, candidateA, candidateB, repeats, images: routedImages }, signal), selected }
109
+ })
110
+ const selectCandidates = async (agent: Agent, problem: string, candidates: readonly string[], repeats: number, signal: AbortSignal, routedImages: readonly import('./caller.ts').VerifierImage[] = []) => record('verifier_select', agent, async () => {
111
+ const { verifier, selected } = await engine(agent)
112
+ return { result: await verifier.select({ problem, candidates, repeats, pivots: Math.min(2, candidates.length), seed: 0, images: routedImages }, signal), selected }
113
+ })
114
+ const trackProgress = async (agent: Agent, problem: string, steps: readonly string[], checkpoints: readonly number[], repeats: number, signal: AbortSignal, routedImages: readonly import('./caller.ts').VerifierImage[] = []) => record('verifier_track', agent, async () => {
115
+ const { verifier, selected } = await engine(agent)
116
+ return { result: await verifier.track(problem, steps, checkpoints, repeats, signal, routedImages), selected }
117
+ })
118
+ const classifyRoute = async (agent: Agent, prompt: string, signal: AbortSignal) => record('verifier_route_classify', agent, async () => {
119
+ const { verifier, selected } = await engine(agent)
120
+ const completion = await callVerifierText(verifier.client, prompt, signal)
121
+ const stats: RunStats = { ...completion.usage, cacheHits: 0, cacheMisses: 0, estimatedCostUsd: ((completion.usage.inputTokens + completion.usage.cachedInputTokens) * selected.estimatedInputUsdPerMillion + completion.usage.outputTokens * selected.estimatedOutputUsdPerMillion) / 1_000_000, topLogprobScores: 0, explicitTagScores: 1 }
122
+ return { result: { text: completion.text, stats }, selected }
123
+ })
124
+ const extractTask = async (agent: Agent, fromSeq: number, toSeq: number, maxChars: number, signal: AbortSignal) => extractSession(agent, async (ref: ImageAttachmentRef) => { const stored = await services.attachments.readImage(ref, signal); return { data: stored.data, mediaType: stored.ref.mediaType } }, { fromSeq, toSeq, includeAssistantText: true, maxChars })
125
+ const routePolicy = (selected: ReturnType<typeof current>) => ({ mode: selected.autoVerifyMode, minConfidence: selected.autoRouteMinConfidence, maxCandidates: selected.autoRouteMaxCandidates, maxPerTask: selected.autoRouteMaxPerTask + selected.autoVerifyMaxPerTask, maxPerSession: selected.autoRouteMaxPerSession + selected.autoVerifyMaxPerSession, maxModelCallsPerTask: selected.autoMaxModelCallsPerTask, maxModelCallsPerSession: selected.autoMaxModelCallsPerSession, maxInputChars: selected.autoRouteMaxInputChars, maxItemChars: selected.autoRouteMaxItemChars })
126
+ const routeFeedback = (decision: RouteDecision, detail: string) => createUserMessage({ content: [{ type: 'text' as const, text: '[Automatic verifier routing: ' + decision.kind + ']\n' + detail + '\nUse this independent result to continue the actual task. Do not merely restate the ranking or progress score; implement, correct, and verify the required work.' }], source: { kind: 'plugin' as const, plugin: 'dsh-llm-verifier', form: 'notice' as const, summary: 'Automatic verifier routed ' + decision.kind } })
127
+
128
+ const handleStatisticsQuery = async (payload: unknown): Promise<{ ok: true; value: StatisticsOverview } | { ok: false; error: { code: 'bad-request'; message: string; details: { issues: never[] } } }> => {
129
+ if (typeof payload !== 'object' || payload === null) return rpcFailure('statistics payload must be an object')
130
+ const row = payload as Record<string, unknown>
131
+ const sessionId = typeof row.sessionId === 'string' && row.sessionId.length > 0 ? row.sessionId : undefined
132
+ const query: StatisticsQuery = { fromMs: numberField(row.fromMs, Number.NaN), toMs: numberField(row.toMs, Number.NaN), timezoneOffsetMinutes: numberField(row.timezoneOffsetMinutes, 0), recentLimit: numberField(row.recentLimit, 40), ...(sessionId ? { sessionId } : {}) }
133
+ try {
134
+ const items = await services.sessionPersistence.list()
135
+ const headers: SessionHeader[] = items
136
+ .map(item => item && typeof item === 'object' && 'header' in item ? (item as { header: SessionHeader }).header : item as SessionHeader)
137
+ .filter(header => header !== undefined && (sessionId === undefined || String(header.id) === sessionId))
138
+ const overviews = await Promise.all(headers.map(header => topic(header).statistics.overview(query)))
139
+ const value: StatisticsOverview = mergeStatisticsOverviews(overviews, query)
140
+ return rpcSuccess(value)
141
+ } catch (error) { return rpcFailure(error instanceof Error ? error.message : String(error)) }
142
+ }
143
+
144
+ // 1. 优先注册到官方 /api 共享通道 (connection.fetch.register)
145
+ const anyConn = services.connection as unknown as { fetch?: { register: (route: unknown) => () => Promise<void> } }
146
+ if (anyConn && anyConn.fetch && typeof anyConn.fetch.register === 'function') {
147
+ try {
148
+ anyConn.fetch.register({
149
+ path: '/api/llm-verifier/statistics',
150
+ methods: ['POST'],
151
+ requestBody: 'buffered',
152
+ fetch: async (request: Request): Promise<Response> => {
153
+ let body: unknown
154
+ try {
155
+ body = await request.json()
156
+ } catch {
157
+ return new Response('body is not JSON', { status: 400 })
158
+ }
159
+ const isRpcEnvelope = typeof body === 'object' && body !== null && (body as { type?: unknown }).type === 'client-request' && typeof (body as { rpcId?: unknown }).rpcId === 'string'
160
+ const rpcId = isRpcEnvelope ? (body as { rpcId: string }).rpcId : 'direct'
161
+ const payload = isRpcEnvelope ? (body as { payload?: unknown }).payload : body
162
+ const outcome = await handleStatisticsQuery(payload)
163
+ if (isRpcEnvelope) {
164
+ return Response.json({
165
+ type: 'server-response',
166
+ rpcId,
167
+ result: outcome,
168
+ })
169
+ }
170
+ return Response.json(outcome)
171
+ },
172
+ })
173
+ } catch (e) {
174
+ ctx.logger.warn('failed to register /api/llm-verifier/statistics fetch route: ' + String(e))
175
+ }
176
+ }
177
+
178
+ // 2. Fallback for hosts predating the exact Fetch route API: the same query over
179
+ // the plugin's own RPC channel, which Connection guards with the Host/Origin
180
+ // fence and browser authentication. Modern hosts answer on /api first.
181
+ const legacyRpc = services.connection as unknown as { rpc?: { handle?: (channel: string, handler: (endpoint: string, payload: unknown) => unknown) => unknown } }
182
+ if (typeof legacyRpc.rpc?.handle === 'function') {
183
+ try {
184
+ legacyRpc.rpc.handle('/llm-verifier', async (endpoint: string, payload: unknown) => {
185
+ if (endpoint !== 'statistics') return rpcFailure('unknown llm-verifier endpoint')
186
+ return handleStatisticsQuery(payload)
187
+ })
188
+ } catch (e) {
189
+ ctx.logger.warn('failed to register /llm-verifier rpc fallback: ' + String(e))
190
+ }
191
+ }
192
+
193
+ ctx.on('agent/disposed', ({ agent }) => { autoBudget.release(agent); autoRouter.release(agent); topics.delete(String(agent.id)) })
194
+
195
+ // Plan pre-review: judge exit_plan_mode BEFORE the tool asks the human, so a
196
+ // weak plan is sent back to the model instead of reaching the review dialog.
197
+ ctx.on('tools/pre-execute', async (exec, next) => {
198
+ const selected = current()
199
+ if (!selected.enabled || selected.autoVerifyMode === 'manual' || !selected.autoVerifyPlanMode) return next()
200
+ if (exec.name !== 'exit_plan_mode' || exec.agent === undefined || exec.signal.aborted) return next()
201
+ const plan = planFromArguments(exec.arguments)
202
+ if (!plan) return next()
203
+ const agent = exec.agent
204
+ const policy = routePolicy(selected)
205
+ const evidence = analyzeAutoTask(sessionEvents(agent.session), { mode: selected.autoVerifyMode, minToolCalls: selected.autoVerifyMinToolCalls, maxPerTask: selected.autoVerifyMaxPerTask, maxPerSession: selected.autoVerifyMaxPerSession })
206
+ const planFingerprint = stableHash({ phase: 'plan_review', plan })
207
+ const reservation = autoRouter.reserve(agent, 'plan_review', planFingerprint, 1, policy)
208
+ if (reservation === undefined) {
209
+ // Already reviewed, another verification is in flight, or the auto-verification
210
+ // budget is spent. The plan still reaches the human: denying here would let an
211
+ // exhausted budget deadlock plan mode, whose only exit is this very tool call.
212
+ if (!autoRouter.completedFingerprint(agent, planFingerprint)) {
213
+ ctx.logger.warn('llm-verifier plan pre-review skipped (verification in flight or auto-verification budget exhausted)')
214
+ }
215
+ return next()
216
+ }
217
+ try {
218
+ const toSeq = sessionEvents(agent.session).at(-1)?.seq ?? -1
219
+ const extracted = await extractTask(agent, evidence.taskStartSeq, toSeq, selected.autoVerifyMaxChars, exec.signal)
220
+ const classified = await classifyRoute(agent, buildPlanPreReviewPrompt(extracted.problem, plan, selected.autoRouteMaxInputChars), exec.signal)
221
+ const verdict = parseVerdictLetter(classified.text)
222
+ if (verdict === undefined) {
223
+ autoRouter.fail(agent, reservation, false)
224
+ ctx.logger.warn('llm-verifier plan pre-review produced no verdict line; the plan was allowed through')
225
+ return next()
226
+ }
227
+ if (verdict.score >= selected.autoVerifyThreshold) {
228
+ autoRouter.commit(agent, reservation)
229
+ return next()
230
+ }
231
+ autoRouter.fail(agent, reservation, selected.autoVerifyMode === 'strict')
232
+ return {
233
+ kind: 'deny',
234
+ reason: '[Automatic Verifier Plan Pre-review] scored ' + (verdict.score * 100).toFixed(1) + '% against a ' + (selected.autoVerifyThreshold * 100).toFixed(0) + '% threshold.\n' + verdict.feedback.slice(0, 4000) + '\nRevise the plan to address these findings, then call exit_plan_mode again.',
235
+ }
236
+ } catch (error) {
237
+ autoRouter.fail(agent, reservation, false)
238
+ ctx.logger.warn('llm-verifier plan pre-review failed: ' + (error instanceof Error ? error.message : String(error)))
239
+ return next()
240
+ }
241
+ })
242
+
243
+ ctx.on('agent/turn-stopping', async ({ agent, signal }) => {
244
+ const selected = current()
245
+ if (!selected.enabled || selected.autoVerifyMode === 'manual' || signal.aborted) return
246
+ const policy = routePolicy(selected)
247
+ const evidence = analyzeAutoTask(sessionEvents(agent.session), { mode: selected.autoVerifyMode, minToolCalls: selected.autoVerifyMinToolCalls, maxPerTask: selected.autoVerifyMaxPerTask, maxPerSession: selected.autoVerifyMaxPerSession })
248
+ const admittedLastSeq = sessionEvents(agent.session).at(-1)?.seq ?? -1
249
+ const snapshot = sessionEvents(agent.session).filter(event => event.seq <= admittedLastSeq)
250
+ const stillCurrent = () => !signal.aborted && (sessionEvents(agent.session).at(-1)?.seq ?? -1) === admittedLastSeq
251
+
252
+ if (selected.autoVerifyTeamTasks) {
253
+ const teamInspection = inspectTeamTasks(snapshot, evidence.taskStartSeq)
254
+ // Every completion still pending in this turn, oldest first. A pass does not
255
+ // keep the turn alive, so a task deferred to "the next turn-stopping" could
256
+ // wait forever: verify the whole batch, with the router budget as the cap.
257
+ for (const completed of teamInspection.completedTasks) {
258
+ const task = completed.task
259
+ const taskReservation = autoRouter.reserve(agent, 'team_task', stableHash({ phase: 'team_task', taskId: task.id, status: task.status, seq: completed.seq }), 1, policy)
260
+ if (taskReservation === undefined) continue
261
+ try {
262
+ const extracted = await extractTask(agent, evidence.taskStartSeq, admittedLastSeq, selected.autoVerifyMaxChars, signal)
263
+ const prompt = buildTeamTaskVerificationPrompt(task, extracted.trace, selected.autoRouteMaxInputChars)
264
+ const classified = await classifyRoute(agent, prompt, signal)
265
+ if (!stillCurrent()) { autoRouter.fail(agent, taskReservation, false); return }
266
+ const verdict = parseVerdictLetter(classified.text)
267
+ if (verdict === undefined) {
268
+ autoRouter.fail(agent, taskReservation, false)
269
+ ctx.logger.warn('llm-verifier team task verification produced no verdict line; task ' + task.id + ' was not gated')
270
+ continue
271
+ }
272
+ if (verdict.score >= selected.autoVerifyThreshold) {
273
+ autoRouter.commit(agent, taskReservation, admittedLastSeq)
274
+ continue
275
+ }
276
+ autoRouter.fail(agent, taskReservation, selected.autoVerifyMode === 'strict')
277
+ agent.steer(createUserMessage({
278
+ content: [{
279
+ type: 'text',
280
+ text: '[Automatic Verifier Team Task Gate]\nTask "' + task.subject + '" (#' + task.id + ') verification scored ' + (verdict.score * 100).toFixed(1) + '% (Threshold: ' + (selected.autoVerifyThreshold * 100).toFixed(0) + '%).\n' + verdict.feedback + '\nProvide verified execution evidence or resolve remaining issues before completing the task.'
281
+ }],
282
+ source: { kind: 'plugin', plugin: 'dsh-llm-verifier', form: 'notice', summary: 'Team Task Gate Feedback' }
283
+ }))
284
+ return
285
+ } catch (error) {
286
+ autoRouter.fail(agent, taskReservation, selected.autoVerifyMode === 'strict')
287
+ ctx.logger.warn('llm-verifier team task verification failed: ' + (error instanceof Error ? error.message : String(error)))
288
+ if (selected.autoVerifyMode === 'strict' && !signal.aborted) {
289
+ agent.steer(createUserMessage({ content: [{ type: 'text', text: '[Automatic Verifier Team Task Gate]\nTask verification failed: ' + (error instanceof Error ? error.message : String(error)) }], source: { kind: 'plugin', plugin: 'dsh-llm-verifier' } }))
290
+ return
291
+ }
292
+ continue
293
+ }
294
+ }
295
+ }
296
+
297
+ let decision = boundDecision(analyzeStructuredRoute(snapshot, selected.autoRouteMaxCandidates, selected.autoRouteMaxItemChars) as RouteDecision, policy)
298
+
299
+ if (decision === undefined && selected.autoRouteSemantic && (selected.autoVerifyMode === 'strict' || semanticRouteHint(snapshot))) {
300
+ const fingerprint = stableHash({ phase: 'semantic', from: evidence.taskStartSeq, to: admittedLastSeq, model: selected.provider + '/' + selected.model })
301
+ const reservation = autoRouter.reserve(agent, 'semantic', fingerprint, 1, policy)
302
+ if (reservation) {
303
+ try {
304
+ const extracted = await extractTask(agent, evidence.taskStartSeq, admittedLastSeq, selected.autoVerifyMaxChars, signal)
305
+ const classified = await classifyRoute(agent, buildSemanticRoutePrompt(extracted.problem, snapshot, selected.autoRouteMaxCandidates, selected.autoRouteMaxItemChars), signal)
306
+ if (!stillCurrent()) { autoRouter.fail(agent, reservation, false); return }
307
+ const parsed = parseSemanticRoute(classified.text, selected.autoRouteMaxCandidates)
308
+ if (!parsed) throw new Error('semantic router returned invalid strict JSON')
309
+ decision = parsed.confidence >= selected.autoRouteMinConfidence ? boundDecision(semanticDecision(parsed, snapshot, selected.autoRouteMaxItemChars) as RouteDecision, policy) : undefined
310
+ autoRouter.commit(agent, reservation)
311
+ } catch (error) {
312
+ autoRouter.fail(agent, reservation, selected.autoVerifyMode === 'strict')
313
+ ctx.logger.warn('llm-verifier automatic classification failed: ' + (error instanceof Error ? error.message : String(error)))
314
+ if (selected.autoVerifyMode === 'strict' && !signal.aborted) agent.steer(createUserMessage({ content: [{ type: 'text', text: '[Automatic verifier routing]\nStrict route classification failed: ' + (error instanceof Error ? error.message : String(error)) + '\nDo not conclude until directly relevant verification succeeds.' }], source: { kind: 'plugin', plugin: 'dsh-llm-verifier' } }))
315
+ return
316
+ }
317
+ }
318
+ }
319
+
320
+ if (!stillCurrent()) return
321
+ if (decision) {
322
+ const expectedCalls = decision.kind === 'select' ? Math.max(1, decision.candidates.length * 8 * selected.autoVerifyRepeats) : Math.max(1, 4 * selected.autoVerifyRepeats)
323
+ const reservation = autoRouter.reserve(agent, decision.kind, decision.fingerprint, expectedCalls, policy)
324
+ if (reservation) {
325
+ try {
326
+ const extracted = await extractTask(agent, evidence.taskStartSeq, admittedLastSeq, selected.autoVerifyMaxChars, signal)
327
+ if (decision.kind === 'compare') {
328
+ const result = await compareCandidates(agent, extracted.problem, decision.candidates[0].content, decision.candidates[1].content, selected.autoVerifyRepeats, signal, extracted.images)
329
+ if (!stillCurrent()) { autoRouter.fail(agent, reservation, false); return }
330
+ if (!autoRouter.commit(agent, reservation, admittedLastSeq)) return
331
+ const winner = result.winner === 'A' ? decision.candidates[0].label : result.winner === 'B' ? decision.candidates[1].label : 'tie'
332
+ agent.steer(routeFeedback(decision, 'Winner: ' + winner + '. Scores: ' + (result.scoreA * 100).toFixed(1) + '% / ' + (result.scoreB * 100).toFixed(1) + '%.'))
333
+ return
334
+ }
335
+ if (decision.kind === 'select') {
336
+ const result = await selectCandidates(agent, extracted.problem, decision.candidates.map(candidate => candidate.content), selected.autoVerifyRepeats, signal, extracted.images)
337
+ if (!stillCurrent()) { autoRouter.fail(agent, reservation, false); return }
338
+ if (!autoRouter.commit(agent, reservation, admittedLastSeq)) return
339
+ const ranking = result.ranking.map((index, rank) => (rank + 1) + '. ' + decision.candidates[index]!.label).join('\n')
340
+ agent.steer(routeFeedback(decision, 'Ranking:\n' + ranking + '\nProceed with ' + decision.candidates[result.index]!.label + '.'))
341
+ return
342
+ }
343
+ const result = await trackProgress(agent, extracted.problem, decision.steps, decision.checkpoints, selected.autoVerifyRepeats, signal, extracted.images)
344
+ if (!stillCurrent()) { autoRouter.fail(agent, reservation, false); return }
345
+ if (!autoRouter.commit(agent, reservation, admittedLastSeq)) return
346
+ const detail = result.scores.map((score, index) => 'Checkpoint step ' + decision.checkpoints[index] + ': ' + (score * 100).toFixed(1) + '%').join('\n')
347
+ const continuation = result.scores.some(score => score < selected.autoTrackCompletionThreshold) ? '\nContinue the unfinished work.' : '\nPrepare final delivery evidence; final session verification is mandatory.'
348
+ agent.steer(routeFeedback(decision, detail + continuation))
349
+ return
350
+ } catch (error) {
351
+ autoRouter.fail(agent, reservation, selected.autoVerifyMode === 'strict')
352
+ ctx.logger.warn('llm-verifier automatic route failed: ' + (error instanceof Error ? error.message : String(error)))
353
+ if (selected.autoVerifyMode === 'strict' && !signal.aborted) agent.steer(createUserMessage({ content: [{ type: 'text', text: '[Automatic verifier routing]\nStrict routed verification failed: ' + (error instanceof Error ? error.message : String(error)) + '\nDo not conclude until it succeeds.' }], source: { kind: 'plugin', plugin: 'dsh-llm-verifier' } }))
354
+ return
355
+ }
356
+ }
357
+ }
358
+
359
+ const forcedFromSeq = autoRouter.finalRequired(agent)
360
+ if (forcedFromSeq === undefined && !evidence.eligible) {
361
+ if (selected.autoVerifyMode === 'strict' && autoRouter.strictBlocked(agent)) agent.steer(createUserMessage({ content: [{ type: 'text', text: '[Automatic verifier gate]\nStrict verification remains blocked. Produce new evidence or run a directly relevant verifier.' }], source: { kind: 'plugin', plugin: 'dsh-llm-verifier' } }))
362
+ return
363
+ }
364
+ const finalFromSeq = forcedFromSeq === undefined ? evidence.taskStartSeq : Math.min(evidence.taskStartSeq, forcedFromSeq)
365
+ const finalFingerprint = stableHash({ phase: 'final', from: finalFromSeq, to: admittedLastSeq })
366
+ const finalReservation = autoRouter.reserve(agent, 'final', finalFingerprint, Math.max(1, 4 * selected.autoVerifyRepeats), policy)
367
+ if (!finalReservation) {
368
+ if (selected.autoVerifyMode === 'strict' && (forcedFromSeq !== undefined || autoRouter.strictBlocked(agent))) agent.steer(createUserMessage({ content: [{ type: 'text', text: '[Automatic verifier gate]\nStrict final verification is required but its safety budget is exhausted or another verifier is active. Do not conclude; request operator review.' }], source: { kind: 'plugin', plugin: 'dsh-llm-verifier' } }))
369
+ return
370
+ }
371
+ try {
372
+ const result = await verifySession(agent, { fromSeq: finalFromSeq, toSeq: admittedLastSeq, includeAssistantText: true, maxChars: selected.autoVerifyMaxChars, repeats: selected.autoVerifyRepeats }, signal)
373
+ if (!stillCurrent()) { autoRouter.fail(agent, finalReservation, false); return }
374
+ const passed = result.winner === 'A' && result.score >= selected.autoVerifyThreshold
375
+ if (passed) autoRouter.commit(agent, finalReservation)
376
+ else {
377
+ autoRouter.fail(agent, finalReservation, selected.autoVerifyMode === 'strict')
378
+ agent.steer(createUserMessage({ content: [{ type: 'text', text: automaticFeedback(result.score, result.baselineScore, result.winner, selected.autoVerifyThreshold) }], source: { kind: 'plugin', plugin: 'dsh-llm-verifier' } }))
379
+ }
380
+ } catch (error) {
381
+ autoRouter.fail(agent, finalReservation, selected.autoVerifyMode === 'strict')
382
+ ctx.logger.warn('llm-verifier automatic final verification failed: ' + (error instanceof Error ? error.message : String(error)))
383
+ if (selected.autoVerifyMode === 'strict' && !signal.aborted) agent.steer(createUserMessage({ content: [{ type: 'text', text: '[Automatic verifier gate]\nStrict final verification failed: ' + (error instanceof Error ? error.message : String(error)) + '\nDo not conclude until verification succeeds.' }], source: { kind: 'plugin', plugin: 'dsh-llm-verifier' } }))
384
+ }
385
+ })
386
+
387
+ ctx.tools.register(defineTool({ name: 'verifier_compare', description: 'Use autonomously when exactly two substantive answers, patches, plans, or execution trajectories need an independent evidence-based comparison and the choice is consequential or uncertain. Do not use for trivial deterministic questions or when there is only one candidate. Uses the verifier model selected in DSH Settings, with top-logprob A–T expectations when supported and explicit-tag fallback otherwise.', parameters: { problem: { type: 'string', required: true }, candidate_a: { type: 'string', required: true }, candidate_b: { type: 'string', required: true }, ...commonParams }, output: { schema: { type: 'object', additionalProperties: false, properties: { scoreA: { type: 'number', required: true }, scoreB: { type: 'number', required: true }, winner: { type: 'string', enum: ['A', 'B', 'tie'], required: true }, criteria: { type: 'array', items: criterionResultSchema, required: true }, calls: { type: 'integer', required: true }, stats: { ...statsSchema, required: true }, provider: { type: 'string', required: true }, model: { type: 'string', required: true } } }, render: (_args, value) => renderJson(value) }, timeoutMs: entry.timeoutMs * 20, async execute(args, exec) { requireEnabled(); const agent = requireAgent(exec.agent); return record('verifier_compare', agent, async () => { const { verifier, selected } = await engine(agent); const result = await verifier.compare({ problem: args.problem, candidateA: args.candidate_a, candidateB: args.candidate_b, criteria: normalizeCriteria(args.criteria), repeats: positive(args.repeats, 2, 'repeats'), images: await images(args.images, exec.signal) }, exec.signal); return { result, selected } }) } }))
388
+
389
+ ctx.tools.register(defineTool({ name: 'verifier_select', description: 'Use autonomously when three or more substantive candidate answers, patches, plans, or trajectories must be ranked and an independent choice is valuable. Use verifier_compare for exactly two candidates; do not generate extra candidates merely to invoke this tool. Deterministic orchestrators should call this directly once they have three or more real candidates.', parameters: { problem: { type: 'string', required: true }, candidates: { type: 'array', items: { type: 'string' }, required: true }, ...commonParams, pivots: { type: 'integer' }, seed: { type: 'integer' } }, output: { schema: { type: 'object', additionalProperties: false, properties: { index: { type: 'integer', required: true }, best: { type: 'string', required: true }, scores: { type: 'array', items: { type: 'number' }, required: true }, ranking: { type: 'array', items: { type: 'integer' }, required: true }, pivots: { type: 'array', items: { type: 'integer' }, required: true }, comparisons: { type: 'integer', required: true }, calls: { type: 'integer', required: true }, stats: { ...statsSchema, required: true }, provider: { type: 'string', required: true }, model: { type: 'string', required: true } } }, render: (_args, value) => renderJson(value) }, timeoutMs: entry.timeoutMs * 100, async execute(args, exec) { requireEnabled(); const agent = requireAgent(exec.agent); return record('verifier_select', agent, async () => { const { verifier, selected } = await engine(agent); const result = await verifier.select({ problem: args.problem, candidates: args.candidates, criteria: normalizeCriteria(args.criteria), repeats: positive(args.repeats, 2, 'repeats'), pivots: positive(args.pivots, 2, 'pivots'), seed: args.seed ?? 0, images: await images(args.images, exec.signal) }, exec.signal); return { result, selected } }) } }))
390
+
391
+ ctx.tools.register(defineTool({ name: 'verifier_track', description: 'Use autonomously for a genuinely multi-step task when progress at explicit checkpoints is uncertain or needs evidence-based measurement. Deterministic goal/workflow orchestrators should call this directly when real checkpoints already exist. Do not use for a single completed answer or invent checkpoints.', parameters: { problem: { type: 'string', required: true }, steps: { type: 'array', items: { type: 'string' }, required: true }, checkpoints: { type: 'array', items: { type: 'integer' }, required: true }, repeats: commonParams.repeats, images: commonParams.images }, output: { schema: { type: 'object', additionalProperties: false, properties: { scores: { type: 'array', items: { type: 'number' }, required: true }, perRepeat: { type: 'array', items: { type: 'array', items: { type: 'number' } }, required: true }, calls: { type: 'integer', required: true }, stats: { ...statsSchema, required: true }, provider: { type: 'string', required: true }, model: { type: 'string', required: true } } }, render: (_args, value) => renderJson(value) }, timeoutMs: entry.timeoutMs * 20, async execute(args, exec) { requireEnabled(); const agent = requireAgent(exec.agent); return record('verifier_track', agent, async () => { const { verifier, selected } = await engine(agent); const result = await verifier.track(args.problem, args.steps, args.checkpoints, positive(args.repeats, 2, 'repeats'), exec.signal, await images(args.images, exec.signal)); return { result, selected } }) } }))
392
+
393
+ ctx.tools.register(defineTool({ name: 'verifier_current_session', description: 'Explicitly verify the current DSH session. Smart/strict policy can also invoke this gate automatically at the turn-stopping lifecycle boundary after consequential work with real tool evidence. Extracts the session, applies redaction and bounds, then sends the evidence to the configured verifier model.', parameters: { from_seq: { type: 'integer' }, to_seq: { type: 'integer' }, include_assistant_text: { type: 'boolean' }, redact_patterns: { type: 'array', items: { type: 'string' } }, max_chars: { type: 'integer' }, repeats: { type: 'integer' } }, output: { schema: { type: 'object', additionalProperties: false, properties: { sessionId: { type: 'string', required: true }, problem: { type: 'string', required: true }, score: { type: 'number', required: true }, baselineScore: { type: 'number', required: true }, winner: { type: 'string', enum: ['A', 'B', 'tie'], required: true }, fromSeq: { type: 'integer', required: true }, toSeq: { type: 'integer', required: true }, omittedCharacters: { type: 'integer', required: true }, calls: { type: 'integer', required: true }, stats: { ...statsSchema, required: true }, provider: { type: 'string', required: true }, model: { type: 'string', required: true } } }, render: (_args, value) => renderJson(value) }, timeoutMs: entry.timeoutMs * 20, async execute(args, exec) { requireEnabled(); const agent = requireAgent(exec.agent); return verifySession(agent, { fromSeq: args.from_seq, toSeq: args.to_seq, includeAssistantText: args.include_assistant_text, redactPatterns: args.redact_patterns, maxChars: args.max_chars, repeats: args.repeats }, exec.signal) } }))
394
+ }