dsh-llm-verifier 0.8.0 → 0.8.2
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/README.md +2 -0
- package/lib/{auto-CGbckoaX.js → auto-DpNooy0d.js} +46 -2
- package/lib/{auto-CGbckoaX.js.map → auto-DpNooy0d.js.map} +1 -1
- package/lib/index.js +7 -2
- package/lib/index.js.map +1 -1
- package/lib/replay.js +1 -1
- package/lib/types/auto.d.ts +16 -0
- package/lib/types/auto.d.ts.map +1 -1
- package/lib/types/auto.js +37 -9
- package/lib/types/auto.js.map +1 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +16 -1
- package/lib/types/index.js.map +1 -1
- package/package.json +1 -1
- package/src/auto.test.ts +42 -1
- package/src/auto.ts +39 -10
- package/src/index.test.ts +34 -0
- package/src/index.ts +16 -1
package/src/auto.ts
CHANGED
|
@@ -33,6 +33,8 @@ export interface AutoTaskEvidence {
|
|
|
33
33
|
/** Whether background subagents started during the task remain in flight. */
|
|
34
34
|
pendingSubagents: boolean
|
|
35
35
|
pendingUserInteraction: boolean
|
|
36
|
+
/** Plan mode is active: the agent may only research and propose, never be pushed to execute. */
|
|
37
|
+
planMode: boolean
|
|
36
38
|
eligible: boolean
|
|
37
39
|
reason: string
|
|
38
40
|
}
|
|
@@ -448,9 +450,31 @@ export function inspectUserInteractionPause(
|
|
|
448
450
|
return undefined
|
|
449
451
|
}
|
|
450
452
|
|
|
453
|
+
/**
|
|
454
|
+
* Whether the session is currently in plan mode.
|
|
455
|
+
*
|
|
456
|
+
* The host logs one `plan/mode` event per committed transition and folds the log as
|
|
457
|
+
* "empty log → inactive, last event wins" (its own projection does exactly this). Hosts
|
|
458
|
+
* without plan mode never log the event, so nothing changes there. While planning, the
|
|
459
|
+
* agent is expected to research and PROPOSE: any automatic route or acceptance verdict at
|
|
460
|
+
* the turn-stopping boundary can only fail against the missing implementation and steer
|
|
461
|
+
* "actually implement it" — commanding execution the human has not approved yet. The
|
|
462
|
+
* `exit_plan_mode` pre-review is the one gate that still runs in this state.
|
|
463
|
+
* @param events - Session event log (the whole log is folded; the mode may predate the task).
|
|
464
|
+
* @returns True when the newest `plan/mode` event activated plan mode.
|
|
465
|
+
*/
|
|
466
|
+
export function planModeActive(events: readonly SessionEvent[]): boolean {
|
|
467
|
+
let active = false
|
|
468
|
+
for (const event of events) {
|
|
469
|
+
if ((event.type as string) !== 'plan/mode') continue
|
|
470
|
+
active = (event as unknown as { data?: { active?: unknown } }).data?.active === true
|
|
471
|
+
}
|
|
472
|
+
return active
|
|
473
|
+
}
|
|
474
|
+
|
|
451
475
|
export function analyzeAutoTask(events: readonly SessionEvent[], policy: AutoVerifyPolicy, sessionId?: string): AutoTaskEvidence {
|
|
452
476
|
const taskStartSeq = latestDirectUserSeq(events)
|
|
453
|
-
if (taskStartSeq === undefined) return { taskStartSeq: 0, toolCalls: 0, completedToolResults: 0, consequentialToolCalls: 0, hasManualSessionVerification: false, manualVerificationAccepted: false, pendingSubagents: false, pendingUserInteraction: false, eligible: false, reason: 'no-direct-user-task' }
|
|
477
|
+
if (taskStartSeq === undefined) return { taskStartSeq: 0, toolCalls: 0, completedToolResults: 0, consequentialToolCalls: 0, hasManualSessionVerification: false, manualVerificationAccepted: false, pendingSubagents: false, pendingUserInteraction: false, planMode: planModeActive(events), eligible: false, reason: 'no-direct-user-task' }
|
|
454
478
|
|
|
455
479
|
const relevant = events.filter(event => event.seq >= taskStartSeq)
|
|
456
480
|
const calls = relevant.filter((event): event is SessionEvent<'tool/call'> => event.type === 'tool/call')
|
|
@@ -523,15 +547,20 @@ export function analyzeAutoTask(events: readonly SessionEvent[], policy: AutoVer
|
|
|
523
547
|
const pendingSubagents = hasPendingSubagents(events, taskStartSeq)
|
|
524
548
|
const userPause = inspectUserInteractionPause(events, taskStartSeq)
|
|
525
549
|
const pendingUserInteraction = userPause !== undefined
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
if (
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
if (
|
|
533
|
-
if (
|
|
534
|
-
return { taskStartSeq, toolCalls, completedToolResults, consequentialToolCalls, hasManualSessionVerification, manualVerificationAccepted, pendingSubagents, pendingUserInteraction, eligible:
|
|
550
|
+
const planMode = planModeActive(events)
|
|
551
|
+
|
|
552
|
+
if (policy.mode === 'manual') return { taskStartSeq, toolCalls, completedToolResults, consequentialToolCalls, hasManualSessionVerification, manualVerificationAccepted, pendingSubagents, pendingUserInteraction, planMode, eligible: false, reason: 'manual-mode' }
|
|
553
|
+
// Planning is the operator's review boundary, checked before every other suppression: an
|
|
554
|
+
// acceptance verdict here could only fail against the missing implementation and command
|
|
555
|
+
// execution the human has not approved yet.
|
|
556
|
+
if (planMode) return { taskStartSeq, toolCalls, completedToolResults, consequentialToolCalls, hasManualSessionVerification, manualVerificationAccepted, pendingSubagents, pendingUserInteraction, planMode: true, eligible: false, reason: 'plan-mode-active' }
|
|
557
|
+
if (manualVerificationAccepted) return { taskStartSeq, toolCalls, completedToolResults, consequentialToolCalls, hasManualSessionVerification, manualVerificationAccepted, pendingSubagents, pendingUserInteraction, planMode, eligible: false, reason: 'already-verified' }
|
|
558
|
+
if (pendingSubagents) return { taskStartSeq, toolCalls, completedToolResults, consequentialToolCalls, hasManualSessionVerification, manualVerificationAccepted, pendingSubagents: true, pendingUserInteraction, planMode, eligible: false, reason: 'pending-subagents' }
|
|
559
|
+
if (pendingUserInteraction) return { taskStartSeq, toolCalls, completedToolResults, consequentialToolCalls, hasManualSessionVerification, manualVerificationAccepted, pendingSubagents, pendingUserInteraction: true, planMode, eligible: false, reason: 'user-interaction-paused' }
|
|
560
|
+
if (consequentialToolCalls === 0) return { taskStartSeq, toolCalls, completedToolResults, consequentialToolCalls, hasManualSessionVerification, manualVerificationAccepted, pendingSubagents, pendingUserInteraction, planMode, eligible: false, reason: 'no-consequential-work' }
|
|
561
|
+
if (completedToolResults === 0) return { taskStartSeq, toolCalls, completedToolResults, consequentialToolCalls, hasManualSessionVerification, manualVerificationAccepted, pendingSubagents, pendingUserInteraction, planMode, eligible: false, reason: 'no-completed-evidence' }
|
|
562
|
+
if (policy.mode === 'smart' && toolCalls < policy.minToolCalls) return { taskStartSeq, toolCalls, completedToolResults, consequentialToolCalls, hasManualSessionVerification, manualVerificationAccepted, pendingSubagents, pendingUserInteraction, planMode, eligible: false, reason: 'insufficient-tool-evidence' }
|
|
563
|
+
return { taskStartSeq, toolCalls, completedToolResults, consequentialToolCalls, hasManualSessionVerification, manualVerificationAccepted, pendingSubagents, pendingUserInteraction, planMode, eligible: true, reason: policy.mode + '-eligible' }
|
|
535
564
|
}
|
|
536
565
|
|
|
537
566
|
/** One criterion's outcome from a session acceptance (candidate A is the session). */
|
package/src/index.test.ts
CHANGED
|
@@ -1306,6 +1306,30 @@ describe('automatic gate lifecycle', () => {
|
|
|
1306
1306
|
})
|
|
1307
1307
|
|
|
1308
1308
|
|
|
1309
|
+
it('skips every automatic route and the final gate while plan mode is active', async () => {
|
|
1310
|
+
// The reported bug: a planning session was judged against the missing implementation and
|
|
1311
|
+
// steered to "actually implement the change" before the human approved the plan.
|
|
1312
|
+
const calls: Array<Record<string, unknown>> = []
|
|
1313
|
+
const { handlers } = assemble(JUDGE, { stream: scriptedStream(1, [], calls), sessions: [{ id: 'topic-plan', createdAt: 1 }] })
|
|
1314
|
+
const steered: unknown[] = []
|
|
1315
|
+
const planningEvents = [
|
|
1316
|
+
user(0, 'Implement it'),
|
|
1317
|
+
call(1, 'e', 'edit'), result(2, 'e', 'file edited'),
|
|
1318
|
+
{ seq: 3, type: 'plan/mode', data: { active: true } },
|
|
1319
|
+
call(4, 'r', 'read'), result(5, 'r', 'file content'),
|
|
1320
|
+
call(6, 'g', 'grep'), result(7, 'g', 'matches'),
|
|
1321
|
+
]
|
|
1322
|
+
await handlers.get('agent/turn-stopping')!({ agent: agent(planningEvents, steered), signal: new AbortController().signal })
|
|
1323
|
+
// No judge call, no routing record, no steering: the turn simply closes for plan review.
|
|
1324
|
+
expect(calls.length).toBe(0)
|
|
1325
|
+
expect(steered.length).toBe(0)
|
|
1326
|
+
|
|
1327
|
+
// Plan approved (plan/mode inactive): the same boundary gates the earlier work again.
|
|
1328
|
+
const approvedEvents = [...planningEvents, { seq: 8, type: 'plan/mode', data: { active: false } }]
|
|
1329
|
+
await handlers.get('agent/turn-stopping')!({ agent: agent(approvedEvents, steered), signal: new AbortController().signal })
|
|
1330
|
+
expect(calls.length).toBeGreaterThan(0)
|
|
1331
|
+
})
|
|
1332
|
+
|
|
1309
1333
|
it('records a low-confidence classification so "not routed" has a reason', async () => {
|
|
1310
1334
|
// Diagnostics gap named by the review: a task that was never routed showed nothing on the
|
|
1311
1335
|
// dashboard. The classification decision itself is now stored with its reason.
|
|
@@ -1803,6 +1827,16 @@ describe('early candidate review through agent/pre-step', () => {
|
|
|
1803
1827
|
expect(judgeCalls(calls)).toHaveLength(6)
|
|
1804
1828
|
})
|
|
1805
1829
|
|
|
1830
|
+
it('does not inject into a step while plan mode is active', async () => {
|
|
1831
|
+
const calls: Array<Record<string, unknown>> = []
|
|
1832
|
+
const { handlers } = assemble(JUDGE, { stream: reviewStream(calls), sessions: [{ id: 'agent-pre', createdAt: 1 }] })
|
|
1833
|
+
const events = [...workflowEvents([candidate('1', 'candidate one'), candidate('2', 'candidate two')]), { type: 'plan/mode', seq: 3, data: { active: true } }]
|
|
1834
|
+
const base = { kind: 'enter' as const, messages: [] }
|
|
1835
|
+
const decision = await handlers.get('agent/pre-step')!({ agent: agent(events), messages: [], turn: 1, step: 2, signal: signal() }, async () => base)
|
|
1836
|
+
expect(decision).toBe(base)
|
|
1837
|
+
expect(calls).toHaveLength(0)
|
|
1838
|
+
})
|
|
1839
|
+
|
|
1806
1840
|
it('does not re-buy the same pair at the following stop boundary', async () => {
|
|
1807
1841
|
const calls: Array<Record<string, unknown>> = []
|
|
1808
1842
|
const { handlers } = assemble(JUDGE, { stream: reviewStream(calls), sessions: [{ id: 'agent-pre', createdAt: 1 }] })
|
package/src/index.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { VerifierEngine, mergeRunStats, normalizeCriteria, partialStats, type Ju
|
|
|
15
15
|
import { loadVerifierImages } from './images.ts'
|
|
16
16
|
import { extractSession, sanitizeVerifierText, sessionEvents, type SessionExtraction } from './session.ts'
|
|
17
17
|
import { CriteriaResolver, type ResolvedCriteria } from './criteria.ts'
|
|
18
|
-
import { analyzeAutoTask, automaticFeedback, compareRouteFeedbackDetail, failedAcceptanceCriteria, inspectUserInteractionPause, isSubagentSession, selectRouteFeedbackDetail, sessionAccepted, MAX_ROUTE_FEEDBACK_CHARS, type AcceptanceCriterion, type RoutedCandidateRef } from './auto.ts'
|
|
18
|
+
import { analyzeAutoTask, automaticFeedback, compareRouteFeedbackDetail, failedAcceptanceCriteria, inspectUserInteractionPause, isSubagentSession, planModeActive, selectRouteFeedbackDetail, sessionAccepted, MAX_ROUTE_FEEDBACK_CHARS, type AcceptanceCriterion, type RoutedCandidateRef } from './auto.ts'
|
|
19
19
|
import { AutoVerifierRouter, analyzeStructuredRoute, boundDecision, buildSemanticRouteView, estimateRoutedCalls, inspectDeliveryPhase, inspectRecoverySignal, latestDirectUserSeq, nextDiagnosticCycleId, parseSemanticRoute, routedRepeats, semanticDecision, semanticReferencesVisible, semanticRouteHint, type CandidateArtifact, type Reservation, type RouteDecision, type RoutedAgent, type RoutedVerifierKind, type SemanticRouteView } from './router.ts'
|
|
20
20
|
import { VerifierActivities, createActivityObserver, type ActivityView } from './verifier-activity.ts'
|
|
21
21
|
import { ProcessCycleStore, ProcessSelector, resolveProcessFile, type ProcessCycleReport } from './process-selection.ts'
|
|
@@ -1337,6 +1337,10 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
1337
1337
|
if (!selected.enabled || selected.autoVerifyMode !== 'smart') return decision
|
|
1338
1338
|
if (payload.signal.aborted) return decision
|
|
1339
1339
|
if (!selected.autoVerifySubagents && isSubagentSession(payload.agent)) return decision
|
|
1340
|
+
// Planning is the operator's review boundary: injecting candidate feedback or buying a
|
|
1341
|
+
// process-selection cycle here would push the agent to execute a plan the human has not
|
|
1342
|
+
// approved yet.
|
|
1343
|
+
if (planModeActive(sessionEvents(payload.agent.session))) return decision
|
|
1340
1344
|
// A step carrying a fresh direct or team task belongs to the operator, not to us.
|
|
1341
1345
|
const carriesTask = payload.messages.some(message => {
|
|
1342
1346
|
const kind = (message as { source?: { kind?: unknown } }).source?.kind
|
|
@@ -1379,6 +1383,17 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
1379
1383
|
const userPause = inspectUserInteractionPause(snapshot, evidence.taskStartSeq, turn)
|
|
1380
1384
|
const userInteractionPending = evidence.pendingUserInteraction || userPause !== undefined
|
|
1381
1385
|
|
|
1386
|
+
// Plan mode is the operator's review boundary: the agent is supposed to research and
|
|
1387
|
+
// PROPOSE, so no automatic route, team gate or final acceptance may run here — a verdict
|
|
1388
|
+
// could only fail against the missing implementation and steer "actually implement it",
|
|
1389
|
+
// commanding execution the human has not approved yet. The exit_plan_mode pre-review is
|
|
1390
|
+
// the one gate that still runs while planning, so this boundary resumes once the mode is
|
|
1391
|
+
// exited (plan/mode active → inactive) and real work exists.
|
|
1392
|
+
if (evidence.planMode) {
|
|
1393
|
+
ctx.logger.info?.('llm-verifier automatic session verification skipped: plan mode is active')
|
|
1394
|
+
return
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1382
1397
|
// A manual `verifier_current_session` that covered the whole task and passed is the
|
|
1383
1398
|
// strongest acceptance signal available; it discharges the mandatory final gate
|
|
1384
1399
|
// BEFORE another routed decision is purchased. `analyzeAutoTask` already refused any
|