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.
Files changed (50) hide show
  1. package/LICENSE +22 -22
  2. package/README.md +301 -301
  3. package/cordis.patch.yml +4 -4
  4. package/lib/client.js +176 -14
  5. package/lib/index.js +418 -43
  6. package/lib/types/client-i18n.d.ts +27 -0
  7. package/lib/types/client-i18n.js +18 -0
  8. package/lib/types/client.d.ts +15 -2
  9. package/lib/types/client.js +134 -9
  10. package/lib/types/config.d.ts +4 -0
  11. package/lib/types/config.js +5 -3
  12. package/lib/types/index.d.ts +2 -0
  13. package/lib/types/index.js +185 -4
  14. package/lib/types/plan-gate.d.ts +14 -0
  15. package/lib/types/plan-gate.js +70 -0
  16. package/lib/types/router.d.ts +10 -1
  17. package/lib/types/router.js +37 -7
  18. package/lib/types/session.js +25 -10
  19. package/lib/types/team-gate.d.ts +11 -0
  20. package/lib/types/team-gate.js +51 -0
  21. package/package.json +112 -112
  22. package/src/auto.test.ts +86 -86
  23. package/src/auto.ts +133 -133
  24. package/src/cache.ts +115 -115
  25. package/src/caller.test.ts +34 -34
  26. package/src/caller.ts +151 -151
  27. package/src/client-i18n.ts +18 -0
  28. package/src/client.tsx +385 -213
  29. package/src/config.ts +189 -183
  30. package/src/core.test.ts +71 -71
  31. package/src/core.ts +205 -205
  32. package/src/engine.test.ts +136 -136
  33. package/src/engine.ts +140 -140
  34. package/src/images.ts +33 -33
  35. package/src/index.ts +416 -247
  36. package/src/parity.test.ts +85 -85
  37. package/src/plan-gate.test.ts +56 -0
  38. package/src/plan-gate.ts +77 -0
  39. package/src/router.test.ts +107 -89
  40. package/src/router.ts +349 -309
  41. package/src/session.test.ts +53 -36
  42. package/src/session.ts +117 -103
  43. package/src/statistics.test.ts +80 -80
  44. package/src/statistics.ts +328 -328
  45. package/src/team-gate.test.ts +56 -0
  46. package/src/team-gate.ts +63 -0
  47. package/src/top-logprobs.test.ts +84 -84
  48. package/src/top-logprobs.ts +153 -153
  49. package/src/topic-storage.test.ts +31 -31
  50. 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
+ }