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/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 {
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
export
|
|
23
|
-
export
|
|
24
|
-
export
|
|
25
|
-
export * from './
|
|
26
|
-
export * from './
|
|
27
|
-
export * from './
|
|
28
|
-
export * from './
|
|
29
|
-
export * from './
|
|
30
|
-
export
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
function
|
|
41
|
-
function
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
-
topics.
|
|
63
|
-
return
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
await statistics.record({ toolName, sessionId: String(agent.id), startedAt, success:
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
return {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const {
|
|
104
|
-
return { result
|
|
105
|
-
})
|
|
106
|
-
const
|
|
107
|
-
const { verifier, selected } = await engine(agent)
|
|
108
|
-
return { result: await verifier.
|
|
109
|
-
})
|
|
110
|
-
const
|
|
111
|
-
const { verifier, selected } = await engine(agent)
|
|
112
|
-
return { result: await verifier.
|
|
113
|
-
})
|
|
114
|
-
const
|
|
115
|
-
const { verifier, selected } = await engine(agent)
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
services.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
return
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
+
}
|