dsh-llm-verifier 0.1.13 → 0.1.14
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 +301 -301
- package/cordis.patch.yml +4 -4
- package/lib/client.js +176 -14
- package/lib/index.js +418 -43
- package/lib/types/client-i18n.d.ts +27 -0
- package/lib/types/client-i18n.js +18 -0
- package/lib/types/client.d.ts +15 -2
- package/lib/types/client.js +134 -9
- package/lib/types/config.d.ts +4 -0
- package/lib/types/config.js +5 -3
- package/lib/types/index.d.ts +2 -0
- package/lib/types/index.js +185 -4
- package/lib/types/plan-gate.d.ts +14 -0
- package/lib/types/plan-gate.js +70 -0
- package/lib/types/router.d.ts +10 -1
- package/lib/types/router.js +37 -7
- package/lib/types/session.js +25 -10
- package/lib/types/team-gate.d.ts +11 -0
- package/lib/types/team-gate.js +51 -0
- package/package.json +112 -112
- package/src/auto.test.ts +86 -86
- package/src/auto.ts +133 -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 +385 -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 +416 -247
- package/src/parity.test.ts +85 -85
- package/src/plan-gate.test.ts +56 -0
- package/src/plan-gate.ts +77 -0
- package/src/router.test.ts +107 -89
- package/src/router.ts +349 -309
- package/src/session.test.ts +53 -36
- package/src/session.ts +117 -103
- package/src/statistics.test.ts +80 -80
- package/src/statistics.ts +328 -328
- package/src/team-gate.test.ts +56 -0
- package/src/team-gate.ts +63 -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/auto.test.ts
CHANGED
|
@@ -1,86 +1,86 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import { Session } from '@deepseek-ai/dsh-session'
|
|
3
|
-
import { createToolResultMessage, createUserMessage } from '@deepseek-ai/dsh-llm'
|
|
4
|
-
import { AutoVerificationBudget, analyzeAutoTask, automaticFeedback } from './auto.ts'
|
|
5
|
-
|
|
6
|
-
function taskSession() {
|
|
7
|
-
const session = Session.create('session-00000000-0000-4000-8000-000000000009' as never)
|
|
8
|
-
session.append('user/message', createUserMessage({ content: [{ type: 'text', text: 'Implement and test the feature' }], source: { kind: 'user' } }), { surfaceOp: 'append' })
|
|
9
|
-
return session
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function call(session: ReturnType<typeof taskSession>, name: string, id: string) {
|
|
13
|
-
session.append('tool/call', { turn: 1, step: 1, callId: id as never, name, arguments: '{}' })
|
|
14
|
-
session.append('tool/result', { turn: 1, step: 1, message: createToolResultMessage({ callId: id as never, content: [{ type: 'text', text: 'ok' }], isError: false }) }, { surfaceOp: 'append' })
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const smart = { mode: 'smart' as const, minToolCalls: 3, maxPerTask: 2, maxPerSession: 8 }
|
|
18
|
-
|
|
19
|
-
describe('automatic verification policy', () => {
|
|
20
|
-
it('requires consequential work and enough evidence in smart mode', () => {
|
|
21
|
-
const session = taskSession()
|
|
22
|
-
call(session, 'read', 'one')
|
|
23
|
-
call(session, 'grep', 'two')
|
|
24
|
-
expect(analyzeAutoTask(session.events, smart)).toMatchObject({ eligible: false, reason: 'no-consequential-work' })
|
|
25
|
-
call(session, 'edit', 'three')
|
|
26
|
-
expect(analyzeAutoTask(session.events, smart)).toMatchObject({ eligible: true, toolCalls: 3, consequentialToolCalls: 1 })
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
it('does not count a failed consequential call paired with an unrelated success', () => {
|
|
30
|
-
const session = taskSession()
|
|
31
|
-
session.append('tool/call', { turn: 1, step: 1, callId: 'bad' as never, name: 'edit', arguments: '{}' })
|
|
32
|
-
session.append('tool/result', { turn: 1, step: 1, message: createToolResultMessage({ callId: 'bad' as never, content: [{ type: 'text', text: 'failed' }], isError: true }), error: { name: 'Error', code: 'FAILED' } }, { surfaceOp: 'append' })
|
|
33
|
-
call(session, 'read', 'good')
|
|
34
|
-
expect(analyzeAutoTask(session.events, smart)).toMatchObject({ consequentialToolCalls: 0, eligible: false, reason: 'no-consequential-work' })
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
it('strict mode accepts one completed consequential call', () => {
|
|
38
|
-
const session = taskSession()
|
|
39
|
-
call(session, 'ssh_exec', 'one')
|
|
40
|
-
expect(analyzeAutoTask(session.events, { ...smart, mode: 'strict' })).toMatchObject({ eligible: true, reason: 'strict-eligible' })
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
it('manual verification suppresses the automatic gate', () => {
|
|
44
|
-
const session = taskSession()
|
|
45
|
-
call(session, 'edit', 'one')
|
|
46
|
-
call(session, 'pwsh', 'two')
|
|
47
|
-
call(session, 'verifier_current_session', 'three')
|
|
48
|
-
expect(analyzeAutoTask(session.events, smart)).toMatchObject({ eligible: false, reason: 'already-verified', hasManualSessionVerification: true })
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
it('resets per-task attempts for a new direct user message and enforces the session cap', () => {
|
|
52
|
-
const session = taskSession()
|
|
53
|
-
call(session, 'edit', 'one')
|
|
54
|
-
call(session, 'read', 'two')
|
|
55
|
-
call(session, 'pwsh', 'three')
|
|
56
|
-
const agent = { id: session.id, session } as never
|
|
57
|
-
const budget = new AutoVerificationBudget()
|
|
58
|
-
const policy = { ...smart, maxPerTask: 1, maxPerSession: 2 }
|
|
59
|
-
expect(budget.claim(agent, analyzeAutoTask(session.events, policy), policy)).toBe(true)
|
|
60
|
-
expect(budget.claim(agent, analyzeAutoTask(session.events, policy), policy)).toBe(false)
|
|
61
|
-
session.append('user/message', createUserMessage({ content: [{ type: 'text', text: 'Do another task' }], source: { kind: 'user' } }), { surfaceOp: 'append' })
|
|
62
|
-
call(session, 'write', 'four')
|
|
63
|
-
call(session, 'read', 'five')
|
|
64
|
-
call(session, 'pwsh', 'six')
|
|
65
|
-
expect(budget.claim(agent, analyzeAutoTask(session.events, policy), policy)).toBe(true)
|
|
66
|
-
session.append('user/message', createUserMessage({ content: [{ type: 'text', text: 'Third task' }], source: { kind: 'user' } }), { surfaceOp: 'append' })
|
|
67
|
-
call(session, 'edit', 'seven')
|
|
68
|
-
call(session, 'read', 'eight')
|
|
69
|
-
call(session, 'pwsh', 'nine')
|
|
70
|
-
expect(budget.claim(agent, analyzeAutoTask(session.events, policy), policy)).toBe(false)
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
it('builds actionable low-score feedback', () => {
|
|
74
|
-
expect(automaticFeedback(0.42, 0.31, 'A', 0.65)).toContain('42.0%')
|
|
75
|
-
expect(automaticFeedback(0.42, 0.31, 'A', 0.65)).toContain('verification command')
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
it('recognizes PTC mode tool/code-dispatch events for eligibility', () => {
|
|
79
|
-
const session = taskSession()
|
|
80
|
-
session.append('tool/code-dispatch', { subCallId: 'c1' as never, name: 'read', arguments: '{}', isError: false, content: [{ type: 'text', text: 'data' }] })
|
|
81
|
-
session.append('tool/code-dispatch', { subCallId: 'c2' as never, name: 'grep', arguments: '{}', isError: false, content: [{ type: 'text', text: 'match' }] })
|
|
82
|
-
expect(analyzeAutoTask(session.events, smart)).toMatchObject({ eligible: false, reason: 'no-consequential-work' })
|
|
83
|
-
session.append('tool/code-dispatch', { subCallId: 'c3' as never, name: 'edit', arguments: '{}', isError: false, content: [{ type: 'text', text: 'done' }] })
|
|
84
|
-
expect(analyzeAutoTask(session.events, smart)).toMatchObject({ eligible: true, toolCalls: 3, consequentialToolCalls: 1 })
|
|
85
|
-
})
|
|
86
|
-
})
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { Session } from '@deepseek-ai/dsh-session'
|
|
3
|
+
import { createToolResultMessage, createUserMessage } from '@deepseek-ai/dsh-llm'
|
|
4
|
+
import { AutoVerificationBudget, analyzeAutoTask, automaticFeedback } from './auto.ts'
|
|
5
|
+
|
|
6
|
+
function taskSession() {
|
|
7
|
+
const session = Session.create('session-00000000-0000-4000-8000-000000000009' as never)
|
|
8
|
+
session.append('user/message', createUserMessage({ content: [{ type: 'text', text: 'Implement and test the feature' }], source: { kind: 'user' } }), { surfaceOp: 'append' })
|
|
9
|
+
return session
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function call(session: ReturnType<typeof taskSession>, name: string, id: string) {
|
|
13
|
+
session.append('tool/call', { turn: 1, step: 1, callId: id as never, name, arguments: '{}' })
|
|
14
|
+
session.append('tool/result', { turn: 1, step: 1, message: createToolResultMessage({ callId: id as never, content: [{ type: 'text', text: 'ok' }], isError: false }) }, { surfaceOp: 'append' })
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const smart = { mode: 'smart' as const, minToolCalls: 3, maxPerTask: 2, maxPerSession: 8 }
|
|
18
|
+
|
|
19
|
+
describe('automatic verification policy', () => {
|
|
20
|
+
it('requires consequential work and enough evidence in smart mode', () => {
|
|
21
|
+
const session = taskSession()
|
|
22
|
+
call(session, 'read', 'one')
|
|
23
|
+
call(session, 'grep', 'two')
|
|
24
|
+
expect(analyzeAutoTask(session.events, smart)).toMatchObject({ eligible: false, reason: 'no-consequential-work' })
|
|
25
|
+
call(session, 'edit', 'three')
|
|
26
|
+
expect(analyzeAutoTask(session.events, smart)).toMatchObject({ eligible: true, toolCalls: 3, consequentialToolCalls: 1 })
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('does not count a failed consequential call paired with an unrelated success', () => {
|
|
30
|
+
const session = taskSession()
|
|
31
|
+
session.append('tool/call', { turn: 1, step: 1, callId: 'bad' as never, name: 'edit', arguments: '{}' })
|
|
32
|
+
session.append('tool/result', { turn: 1, step: 1, message: createToolResultMessage({ callId: 'bad' as never, content: [{ type: 'text', text: 'failed' }], isError: true }), error: { name: 'Error', code: 'FAILED' } }, { surfaceOp: 'append' })
|
|
33
|
+
call(session, 'read', 'good')
|
|
34
|
+
expect(analyzeAutoTask(session.events, smart)).toMatchObject({ consequentialToolCalls: 0, eligible: false, reason: 'no-consequential-work' })
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('strict mode accepts one completed consequential call', () => {
|
|
38
|
+
const session = taskSession()
|
|
39
|
+
call(session, 'ssh_exec', 'one')
|
|
40
|
+
expect(analyzeAutoTask(session.events, { ...smart, mode: 'strict' })).toMatchObject({ eligible: true, reason: 'strict-eligible' })
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('manual verification suppresses the automatic gate', () => {
|
|
44
|
+
const session = taskSession()
|
|
45
|
+
call(session, 'edit', 'one')
|
|
46
|
+
call(session, 'pwsh', 'two')
|
|
47
|
+
call(session, 'verifier_current_session', 'three')
|
|
48
|
+
expect(analyzeAutoTask(session.events, smart)).toMatchObject({ eligible: false, reason: 'already-verified', hasManualSessionVerification: true })
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('resets per-task attempts for a new direct user message and enforces the session cap', () => {
|
|
52
|
+
const session = taskSession()
|
|
53
|
+
call(session, 'edit', 'one')
|
|
54
|
+
call(session, 'read', 'two')
|
|
55
|
+
call(session, 'pwsh', 'three')
|
|
56
|
+
const agent = { id: session.id, session } as never
|
|
57
|
+
const budget = new AutoVerificationBudget()
|
|
58
|
+
const policy = { ...smart, maxPerTask: 1, maxPerSession: 2 }
|
|
59
|
+
expect(budget.claim(agent, analyzeAutoTask(session.events, policy), policy)).toBe(true)
|
|
60
|
+
expect(budget.claim(agent, analyzeAutoTask(session.events, policy), policy)).toBe(false)
|
|
61
|
+
session.append('user/message', createUserMessage({ content: [{ type: 'text', text: 'Do another task' }], source: { kind: 'user' } }), { surfaceOp: 'append' })
|
|
62
|
+
call(session, 'write', 'four')
|
|
63
|
+
call(session, 'read', 'five')
|
|
64
|
+
call(session, 'pwsh', 'six')
|
|
65
|
+
expect(budget.claim(agent, analyzeAutoTask(session.events, policy), policy)).toBe(true)
|
|
66
|
+
session.append('user/message', createUserMessage({ content: [{ type: 'text', text: 'Third task' }], source: { kind: 'user' } }), { surfaceOp: 'append' })
|
|
67
|
+
call(session, 'edit', 'seven')
|
|
68
|
+
call(session, 'read', 'eight')
|
|
69
|
+
call(session, 'pwsh', 'nine')
|
|
70
|
+
expect(budget.claim(agent, analyzeAutoTask(session.events, policy), policy)).toBe(false)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('builds actionable low-score feedback', () => {
|
|
74
|
+
expect(automaticFeedback(0.42, 0.31, 'A', 0.65)).toContain('42.0%')
|
|
75
|
+
expect(automaticFeedback(0.42, 0.31, 'A', 0.65)).toContain('verification command')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('recognizes PTC mode tool/code-dispatch events for eligibility', () => {
|
|
79
|
+
const session = taskSession()
|
|
80
|
+
session.append('tool/code-dispatch', { subCallId: 'c1' as never, name: 'read', arguments: '{}', isError: false, content: [{ type: 'text', text: 'data' }] })
|
|
81
|
+
session.append('tool/code-dispatch', { subCallId: 'c2' as never, name: 'grep', arguments: '{}', isError: false, content: [{ type: 'text', text: 'match' }] })
|
|
82
|
+
expect(analyzeAutoTask(session.events, smart)).toMatchObject({ eligible: false, reason: 'no-consequential-work' })
|
|
83
|
+
session.append('tool/code-dispatch', { subCallId: 'c3' as never, name: 'edit', arguments: '{}', isError: false, content: [{ type: 'text', text: 'done' }] })
|
|
84
|
+
expect(analyzeAutoTask(session.events, smart)).toMatchObject({ eligible: true, toolCalls: 3, consequentialToolCalls: 1 })
|
|
85
|
+
})
|
|
86
|
+
})
|
package/src/auto.ts
CHANGED
|
@@ -1,133 +1,133 @@
|
|
|
1
|
-
import type { Agent } from '@deepseek-ai/dsh-agent'
|
|
2
|
-
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
|
3
|
-
|
|
4
|
-
export type AutoVerifyMode = 'manual' | 'smart' | 'strict'
|
|
5
|
-
|
|
6
|
-
export interface AutoVerifyPolicy {
|
|
7
|
-
mode: AutoVerifyMode
|
|
8
|
-
minToolCalls: number
|
|
9
|
-
maxPerTask: number
|
|
10
|
-
maxPerSession: number
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface AutoTaskEvidence {
|
|
14
|
-
taskStartSeq: number
|
|
15
|
-
toolCalls: number
|
|
16
|
-
completedToolResults: number
|
|
17
|
-
consequentialToolCalls: number
|
|
18
|
-
hasManualSessionVerification: boolean
|
|
19
|
-
eligible: boolean
|
|
20
|
-
reason: string
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const PASSIVE_TOOLS = new Set([
|
|
24
|
-
'read', 'read_image', 'glob', 'grep', 'web_search', 'ssh_list', 'job_list',
|
|
25
|
-
'job_output', 'list_agents', 'get_goal', 'skill', 'mcp__codegraph__codegraph_explore',
|
|
26
|
-
])
|
|
27
|
-
const VERIFIER_TOOLS = new Set([
|
|
28
|
-
'verifier_compare', 'verifier_select', 'verifier_track', 'verifier_current_session',
|
|
29
|
-
])
|
|
30
|
-
const CONSEQUENTIAL_TOOLS = new Set([
|
|
31
|
-
'edit', 'write', 'pwsh', 'bash', 'run_code', 'codex_image_generate',
|
|
32
|
-
'ssh_exec', 'ssh_upload', 'ssh_download', 'ssh_tunnel', 'ssh_cluster',
|
|
33
|
-
'job_kill', 'workbench_session_delete', 'create_goal', 'update_goal',
|
|
34
|
-
])
|
|
35
|
-
|
|
36
|
-
function latestDirectUserSeq(events: readonly SessionEvent[]): number | undefined {
|
|
37
|
-
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
38
|
-
const event = events[index]
|
|
39
|
-
if (event?.type === 'user/message' && event.data.source.kind === 'user') return event.seq
|
|
40
|
-
}
|
|
41
|
-
return undefined
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function isConsequential(name: string): boolean {
|
|
45
|
-
if (CONSEQUENTIAL_TOOLS.has(name)) return true
|
|
46
|
-
if (PASSIVE_TOOLS.has(name) || VERIFIER_TOOLS.has(name)) return false
|
|
47
|
-
return /(?:edit|write|patch|apply|deploy|upload|delete|remove|kill|exec|shell|command|migration|database|tunnel|cluster)/iu.test(name)
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
interface CodeDispatchData {
|
|
51
|
-
subCallId?: string
|
|
52
|
-
name: string
|
|
53
|
-
arguments?: unknown
|
|
54
|
-
isError?: boolean
|
|
55
|
-
content?: readonly { isError?: boolean }[]
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function isSuccessfulCodeDispatch(data: CodeDispatchData): boolean {
|
|
59
|
-
if (data.isError === true) return false
|
|
60
|
-
if (Array.isArray(data.content) && data.content.some(b => b.isError === true)) return false
|
|
61
|
-
return true
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function analyzeAutoTask(events: readonly SessionEvent[], policy: AutoVerifyPolicy): AutoTaskEvidence {
|
|
65
|
-
const taskStartSeq = latestDirectUserSeq(events)
|
|
66
|
-
if (taskStartSeq === undefined) return { taskStartSeq: 0, toolCalls: 0, completedToolResults: 0, consequentialToolCalls: 0, hasManualSessionVerification: false, eligible: false, reason: 'no-direct-user-task' }
|
|
67
|
-
|
|
68
|
-
const relevant = events.filter(event => event.seq >= taskStartSeq)
|
|
69
|
-
const calls = relevant.filter((event): event is SessionEvent<'tool/call'> => event.type === 'tool/call')
|
|
70
|
-
const successfulResults = new Set(relevant.filter((event): event is SessionEvent<'tool/result'> => event.type === 'tool/result' && event.data.error === undefined && event.data.message.content.every(block => block.isError !== true)).map(event => String(event.data.message.source.callId)))
|
|
71
|
-
const pairedCalls = calls.filter(event => successfulResults.has(String(event.data.callId)))
|
|
72
|
-
|
|
73
|
-
const codeDispatches = relevant.filter(event => event.type === 'tool/code-dispatch').map(event => event.data as CodeDispatchData)
|
|
74
|
-
const successfulDispatches = codeDispatches.filter(isSuccessfulCodeDispatch)
|
|
75
|
-
|
|
76
|
-
const toolCalls = calls.filter(event => !VERIFIER_TOOLS.has(event.data.name)).length + codeDispatches.filter(d => !VERIFIER_TOOLS.has(d.name)).length
|
|
77
|
-
const completedToolResults = pairedCalls.length + successfulDispatches.length
|
|
78
|
-
const consequentialToolCalls = pairedCalls.filter(event => isConsequential(event.data.name)).length + successfulDispatches.filter(d => isConsequential(d.name)).length
|
|
79
|
-
const hasManualSessionVerification = pairedCalls.some(event => event.data.name === 'verifier_current_session') || successfulDispatches.some(d => d.name === 'verifier_current_session')
|
|
80
|
-
|
|
81
|
-
if (policy.mode === 'manual') return { taskStartSeq, toolCalls, completedToolResults, consequentialToolCalls, hasManualSessionVerification, eligible: false, reason: 'manual-mode' }
|
|
82
|
-
if (hasManualSessionVerification) return { taskStartSeq, toolCalls, completedToolResults, consequentialToolCalls, hasManualSessionVerification, eligible: false, reason: 'already-verified' }
|
|
83
|
-
if (consequentialToolCalls === 0) return { taskStartSeq, toolCalls, completedToolResults, consequentialToolCalls, hasManualSessionVerification, eligible: false, reason: 'no-consequential-work' }
|
|
84
|
-
if (completedToolResults === 0) return { taskStartSeq, toolCalls, completedToolResults, consequentialToolCalls, hasManualSessionVerification, eligible: false, reason: 'no-completed-evidence' }
|
|
85
|
-
if (policy.mode === 'smart' && toolCalls < policy.minToolCalls) return { taskStartSeq, toolCalls, completedToolResults, consequentialToolCalls, hasManualSessionVerification, eligible: false, reason: 'insufficient-tool-evidence' }
|
|
86
|
-
return { taskStartSeq, toolCalls, completedToolResults, consequentialToolCalls, hasManualSessionVerification, eligible: true, reason: policy.mode + '-eligible' }
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
interface SessionBudget {
|
|
90
|
-
taskStartSeq: number
|
|
91
|
-
taskAttempts: number
|
|
92
|
-
sessionAttempts: number
|
|
93
|
-
lastEvaluatedSeq: number
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export class AutoVerificationBudget {
|
|
97
|
-
private readonly states = new Map<string, SessionBudget>()
|
|
98
|
-
|
|
99
|
-
claim(agent: Agent, evidence: AutoTaskEvidence, policy: AutoVerifyPolicy): boolean {
|
|
100
|
-
if (!evidence.eligible) return false
|
|
101
|
-
const id = String(agent.id)
|
|
102
|
-
const previous = this.states.get(id)
|
|
103
|
-
const state: SessionBudget = previous ?? { taskStartSeq: evidence.taskStartSeq, taskAttempts: 0, sessionAttempts: 0, lastEvaluatedSeq: -1 }
|
|
104
|
-
if (state.taskStartSeq !== evidence.taskStartSeq) {
|
|
105
|
-
state.taskStartSeq = evidence.taskStartSeq
|
|
106
|
-
state.taskAttempts = 0
|
|
107
|
-
state.lastEvaluatedSeq = -1
|
|
108
|
-
}
|
|
109
|
-
const lastSeq = agent.session.events.at(-1)?.seq ?? -1
|
|
110
|
-
if (lastSeq <= state.lastEvaluatedSeq || state.taskAttempts >= policy.maxPerTask || state.sessionAttempts >= policy.maxPerSession) {
|
|
111
|
-
this.states.set(id, state)
|
|
112
|
-
return false
|
|
113
|
-
}
|
|
114
|
-
state.taskAttempts += 1
|
|
115
|
-
state.sessionAttempts += 1
|
|
116
|
-
state.lastEvaluatedSeq = lastSeq
|
|
117
|
-
this.states.set(id, state)
|
|
118
|
-
return true
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
release(agent: Agent): void {
|
|
122
|
-
this.states.delete(String(agent.id))
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
export function automaticFeedback(score: number, baselineScore: number, winner: 'A' | 'B' | 'tie', threshold: number): string {
|
|
127
|
-
const percent = (value: number) => (value * 100).toFixed(1) + '%'
|
|
128
|
-
return [
|
|
129
|
-
'[Automatic verifier gate]',
|
|
130
|
-
`The independent verifier did not clear this task for completion: evidence score ${percent(score)}, baseline ${percent(baselineScore)}, verdict ${winner}, required ${percent(threshold)}.`,
|
|
131
|
-
'Re-open the task requirements, inspect the actual tool outputs for unresolved errors or missing proof, make any necessary corrections, and run a directly relevant verification command before concluding. Do not merely restate that the task is complete.',
|
|
132
|
-
].join('\n')
|
|
133
|
-
}
|
|
1
|
+
import type { Agent } from '@deepseek-ai/dsh-agent'
|
|
2
|
+
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
|
3
|
+
|
|
4
|
+
export type AutoVerifyMode = 'manual' | 'smart' | 'strict'
|
|
5
|
+
|
|
6
|
+
export interface AutoVerifyPolicy {
|
|
7
|
+
mode: AutoVerifyMode
|
|
8
|
+
minToolCalls: number
|
|
9
|
+
maxPerTask: number
|
|
10
|
+
maxPerSession: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface AutoTaskEvidence {
|
|
14
|
+
taskStartSeq: number
|
|
15
|
+
toolCalls: number
|
|
16
|
+
completedToolResults: number
|
|
17
|
+
consequentialToolCalls: number
|
|
18
|
+
hasManualSessionVerification: boolean
|
|
19
|
+
eligible: boolean
|
|
20
|
+
reason: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const PASSIVE_TOOLS = new Set([
|
|
24
|
+
'read', 'read_image', 'glob', 'grep', 'web_search', 'ssh_list', 'job_list',
|
|
25
|
+
'job_output', 'list_agents', 'get_goal', 'skill', 'mcp__codegraph__codegraph_explore',
|
|
26
|
+
])
|
|
27
|
+
const VERIFIER_TOOLS = new Set([
|
|
28
|
+
'verifier_compare', 'verifier_select', 'verifier_track', 'verifier_current_session',
|
|
29
|
+
])
|
|
30
|
+
const CONSEQUENTIAL_TOOLS = new Set([
|
|
31
|
+
'edit', 'write', 'pwsh', 'bash', 'run_code', 'codex_image_generate',
|
|
32
|
+
'ssh_exec', 'ssh_upload', 'ssh_download', 'ssh_tunnel', 'ssh_cluster',
|
|
33
|
+
'job_kill', 'workbench_session_delete', 'create_goal', 'update_goal',
|
|
34
|
+
])
|
|
35
|
+
|
|
36
|
+
function latestDirectUserSeq(events: readonly SessionEvent[]): number | undefined {
|
|
37
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
38
|
+
const event = events[index]
|
|
39
|
+
if (event?.type === 'user/message' && event.data.source.kind === 'user') return event.seq
|
|
40
|
+
}
|
|
41
|
+
return undefined
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isConsequential(name: string): boolean {
|
|
45
|
+
if (CONSEQUENTIAL_TOOLS.has(name)) return true
|
|
46
|
+
if (PASSIVE_TOOLS.has(name) || VERIFIER_TOOLS.has(name)) return false
|
|
47
|
+
return /(?:edit|write|patch|apply|deploy|upload|delete|remove|kill|exec|shell|command|migration|database|tunnel|cluster)/iu.test(name)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface CodeDispatchData {
|
|
51
|
+
subCallId?: string
|
|
52
|
+
name: string
|
|
53
|
+
arguments?: unknown
|
|
54
|
+
isError?: boolean
|
|
55
|
+
content?: readonly { isError?: boolean }[]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isSuccessfulCodeDispatch(data: CodeDispatchData): boolean {
|
|
59
|
+
if (data.isError === true) return false
|
|
60
|
+
if (Array.isArray(data.content) && data.content.some(b => b.isError === true)) return false
|
|
61
|
+
return true
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function analyzeAutoTask(events: readonly SessionEvent[], policy: AutoVerifyPolicy): AutoTaskEvidence {
|
|
65
|
+
const taskStartSeq = latestDirectUserSeq(events)
|
|
66
|
+
if (taskStartSeq === undefined) return { taskStartSeq: 0, toolCalls: 0, completedToolResults: 0, consequentialToolCalls: 0, hasManualSessionVerification: false, eligible: false, reason: 'no-direct-user-task' }
|
|
67
|
+
|
|
68
|
+
const relevant = events.filter(event => event.seq >= taskStartSeq)
|
|
69
|
+
const calls = relevant.filter((event): event is SessionEvent<'tool/call'> => event.type === 'tool/call')
|
|
70
|
+
const successfulResults = new Set(relevant.filter((event): event is SessionEvent<'tool/result'> => event.type === 'tool/result' && event.data.error === undefined && event.data.message.content.every(block => block.isError !== true)).map(event => String(event.data.message.source.callId)))
|
|
71
|
+
const pairedCalls = calls.filter(event => successfulResults.has(String(event.data.callId)))
|
|
72
|
+
|
|
73
|
+
const codeDispatches = relevant.filter(event => event.type === 'tool/code-dispatch').map(event => event.data as CodeDispatchData)
|
|
74
|
+
const successfulDispatches = codeDispatches.filter(isSuccessfulCodeDispatch)
|
|
75
|
+
|
|
76
|
+
const toolCalls = calls.filter(event => !VERIFIER_TOOLS.has(event.data.name)).length + codeDispatches.filter(d => !VERIFIER_TOOLS.has(d.name)).length
|
|
77
|
+
const completedToolResults = pairedCalls.length + successfulDispatches.length
|
|
78
|
+
const consequentialToolCalls = pairedCalls.filter(event => isConsequential(event.data.name)).length + successfulDispatches.filter(d => isConsequential(d.name)).length
|
|
79
|
+
const hasManualSessionVerification = pairedCalls.some(event => event.data.name === 'verifier_current_session') || successfulDispatches.some(d => d.name === 'verifier_current_session')
|
|
80
|
+
|
|
81
|
+
if (policy.mode === 'manual') return { taskStartSeq, toolCalls, completedToolResults, consequentialToolCalls, hasManualSessionVerification, eligible: false, reason: 'manual-mode' }
|
|
82
|
+
if (hasManualSessionVerification) return { taskStartSeq, toolCalls, completedToolResults, consequentialToolCalls, hasManualSessionVerification, eligible: false, reason: 'already-verified' }
|
|
83
|
+
if (consequentialToolCalls === 0) return { taskStartSeq, toolCalls, completedToolResults, consequentialToolCalls, hasManualSessionVerification, eligible: false, reason: 'no-consequential-work' }
|
|
84
|
+
if (completedToolResults === 0) return { taskStartSeq, toolCalls, completedToolResults, consequentialToolCalls, hasManualSessionVerification, eligible: false, reason: 'no-completed-evidence' }
|
|
85
|
+
if (policy.mode === 'smart' && toolCalls < policy.minToolCalls) return { taskStartSeq, toolCalls, completedToolResults, consequentialToolCalls, hasManualSessionVerification, eligible: false, reason: 'insufficient-tool-evidence' }
|
|
86
|
+
return { taskStartSeq, toolCalls, completedToolResults, consequentialToolCalls, hasManualSessionVerification, eligible: true, reason: policy.mode + '-eligible' }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
interface SessionBudget {
|
|
90
|
+
taskStartSeq: number
|
|
91
|
+
taskAttempts: number
|
|
92
|
+
sessionAttempts: number
|
|
93
|
+
lastEvaluatedSeq: number
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export class AutoVerificationBudget {
|
|
97
|
+
private readonly states = new Map<string, SessionBudget>()
|
|
98
|
+
|
|
99
|
+
claim(agent: Agent, evidence: AutoTaskEvidence, policy: AutoVerifyPolicy): boolean {
|
|
100
|
+
if (!evidence.eligible) return false
|
|
101
|
+
const id = String(agent.id)
|
|
102
|
+
const previous = this.states.get(id)
|
|
103
|
+
const state: SessionBudget = previous ?? { taskStartSeq: evidence.taskStartSeq, taskAttempts: 0, sessionAttempts: 0, lastEvaluatedSeq: -1 }
|
|
104
|
+
if (state.taskStartSeq !== evidence.taskStartSeq) {
|
|
105
|
+
state.taskStartSeq = evidence.taskStartSeq
|
|
106
|
+
state.taskAttempts = 0
|
|
107
|
+
state.lastEvaluatedSeq = -1
|
|
108
|
+
}
|
|
109
|
+
const lastSeq = agent.session.events.at(-1)?.seq ?? -1
|
|
110
|
+
if (lastSeq <= state.lastEvaluatedSeq || state.taskAttempts >= policy.maxPerTask || state.sessionAttempts >= policy.maxPerSession) {
|
|
111
|
+
this.states.set(id, state)
|
|
112
|
+
return false
|
|
113
|
+
}
|
|
114
|
+
state.taskAttempts += 1
|
|
115
|
+
state.sessionAttempts += 1
|
|
116
|
+
state.lastEvaluatedSeq = lastSeq
|
|
117
|
+
this.states.set(id, state)
|
|
118
|
+
return true
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
release(agent: Agent): void {
|
|
122
|
+
this.states.delete(String(agent.id))
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function automaticFeedback(score: number, baselineScore: number, winner: 'A' | 'B' | 'tie', threshold: number): string {
|
|
127
|
+
const percent = (value: number) => (value * 100).toFixed(1) + '%'
|
|
128
|
+
return [
|
|
129
|
+
'[Automatic verifier gate]',
|
|
130
|
+
`The independent verifier did not clear this task for completion: evidence score ${percent(score)}, baseline ${percent(baselineScore)}, verdict ${winner}, required ${percent(threshold)}.`,
|
|
131
|
+
'Re-open the task requirements, inspect the actual tool outputs for unresolved errors or missing proof, make any necessary corrections, and run a directly relevant verification command before concluding. Do not merely restate that the task is complete.',
|
|
132
|
+
].join('\n')
|
|
133
|
+
}
|