dsh-auto-review 0.4.0
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/CHANGELOG.md +91 -0
- package/LICENSE +201 -0
- package/README.es.md +226 -0
- package/README.hi.md +226 -0
- package/README.md +226 -0
- package/README.pt.md +226 -0
- package/README.zh.md +226 -0
- package/cordis.patch.yml +27 -0
- package/lib/client.js +494 -0
- package/lib/client.js.map +1 -0
- package/lib/events-CFl6CHZi.js +344 -0
- package/lib/index.js +5305 -0
- package/lib/invariant.js +223 -0
- package/lib/types/client/ReviewPanel.d.ts +29 -0
- package/lib/types/client/ReviewPanel.d.ts.map +1 -0
- package/lib/types/client/ReviewPanel.js +63 -0
- package/lib/types/client/ReviewPanel.js.map +1 -0
- package/lib/types/client/index.d.ts +31 -0
- package/lib/types/client/index.d.ts.map +1 -0
- package/lib/types/client/index.js +51 -0
- package/lib/types/client/index.js.map +1 -0
- package/lib/types/client/locales.d.ts +49 -0
- package/lib/types/client/locales.d.ts.map +1 -0
- package/lib/types/client/locales.js +82 -0
- package/lib/types/client/locales.js.map +1 -0
- package/lib/types/client/present.d.ts +25 -0
- package/lib/types/client/present.d.ts.map +1 -0
- package/lib/types/client/present.js +36 -0
- package/lib/types/client/present.js.map +1 -0
- package/lib/types/client/styles.d.ts +9 -0
- package/lib/types/client/styles.d.ts.map +1 -0
- package/lib/types/client/styles.js +130 -0
- package/lib/types/client/styles.js.map +1 -0
- package/lib/types/config.d.ts +200 -0
- package/lib/types/config.d.ts.map +1 -0
- package/lib/types/config.js +154 -0
- package/lib/types/config.js.map +1 -0
- package/lib/types/events.d.ts +347 -0
- package/lib/types/events.d.ts.map +1 -0
- package/lib/types/events.js +369 -0
- package/lib/types/events.js.map +1 -0
- package/lib/types/index.d.ts +26 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/index.js +26 -0
- package/lib/types/index.js.map +1 -0
- package/lib/types/invariant.d.ts +28 -0
- package/lib/types/invariant.d.ts.map +1 -0
- package/lib/types/invariant.js +306 -0
- package/lib/types/invariant.js.map +1 -0
- package/lib/types/messages.d.ts +38 -0
- package/lib/types/messages.d.ts.map +1 -0
- package/lib/types/messages.js +65 -0
- package/lib/types/messages.js.map +1 -0
- package/lib/types/projection-types.d.ts +61 -0
- package/lib/types/projection-types.d.ts.map +1 -0
- package/lib/types/projection-types.js +8 -0
- package/lib/types/projection-types.js.map +1 -0
- package/lib/types/projection.d.ts +93 -0
- package/lib/types/projection.d.ts.map +1 -0
- package/lib/types/projection.js +197 -0
- package/lib/types/projection.js.map +1 -0
- package/lib/types/review.d.ts +141 -0
- package/lib/types/review.d.ts.map +1 -0
- package/lib/types/review.js +389 -0
- package/lib/types/review.js.map +1 -0
- package/lib/types/runtime.d.ts +160 -0
- package/lib/types/runtime.d.ts.map +1 -0
- package/lib/types/runtime.js +486 -0
- package/lib/types/runtime.js.map +1 -0
- package/package.json +132 -0
- package/src/client/ReviewPanel.tsx +164 -0
- package/src/client/index.ts +72 -0
- package/src/client/locales.ts +87 -0
- package/src/client/present.ts +58 -0
- package/src/client/styles.ts +131 -0
- package/src/config.ts +346 -0
- package/src/events.ts +553 -0
- package/src/index.ts +28 -0
- package/src/invariant.ts +326 -0
- package/src/messages.ts +91 -0
- package/src/projection-types.ts +61 -0
- package/src/projection.ts +224 -0
- package/src/review.ts +428 -0
- package/src/runtime.ts +562 -0
package/src/invariant.ts
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-owned invariant companion for `dsh-auto-review`:
|
|
3
|
+
*
|
|
4
|
+
* 1. Every `autoReview/verdict` audit event references an `approval/asked`
|
|
5
|
+
* event that precedes it in the same session, at most one verdict exists
|
|
6
|
+
* per asked event, and payload fields obey the closed vocabularies
|
|
7
|
+
* (decision/reason/riskLevel/outcome/fallback). The same chain holds for
|
|
8
|
+
* `autoReview/rejection` events (hard-disable rejections).
|
|
9
|
+
* 2. Model-visible ⟺ logged: every `tool/result` carrying the deny marker
|
|
10
|
+
* links to a recorded `deny` verdict with a matching call id, every
|
|
11
|
+
* `tool/result` carrying the fallback marker links to a recorded fallback
|
|
12
|
+
* verdict that was rejected, and every `tool/result` carrying the never
|
|
13
|
+
* marker links to a recorded rejection that settled rejected.
|
|
14
|
+
*
|
|
15
|
+
* @module dsh-auto-review/invariant
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
19
|
+
import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
|
|
20
|
+
import type { InvariantFailure, InvariantInstaller } from '@deepseek-ai/dsh-invariants'
|
|
21
|
+
import { AUTO_REVIEW_FALLBACKS, CIRCUIT_MARKER_PATTERN, DENY_MARKER_PATTERN, FALLBACK_MARKER_PATTERN, NEVER_MARKER_PATTERN } from './events.ts'
|
|
22
|
+
|
|
23
|
+
const PACKAGE_NAME = 'dsh-auto-review'
|
|
24
|
+
|
|
25
|
+
/** Cordis companion plugin name. */
|
|
26
|
+
export const name = 'auto-review-invariant'
|
|
27
|
+
/** Service required before the companion can reserve package ownership. */
|
|
28
|
+
export const inject = ['invariants']
|
|
29
|
+
|
|
30
|
+
const DECISIONS: readonly string[] = ['allow', 'deny']
|
|
31
|
+
const RISK_LEVELS: readonly string[] = ['low', 'medium', 'high']
|
|
32
|
+
const OUTCOMES: readonly string[] = ['allowed-once', 'rejected', 'cancelled', 'unavailable']
|
|
33
|
+
const TRIP_KINDS: readonly string[] = ['consecutive', 'window']
|
|
34
|
+
const CIRCUIT_ACTIONS: readonly string[] = ['delegate', 'reject', 'abort-turn']
|
|
35
|
+
|
|
36
|
+
interface VerdictRecord {
|
|
37
|
+
readonly callId: string | undefined
|
|
38
|
+
readonly decision: string | undefined
|
|
39
|
+
readonly outcome: string | undefined
|
|
40
|
+
readonly fallback: string | undefined
|
|
41
|
+
readonly escalation: string | undefined
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** One recorded hard-disable rejection, for never-marker reconstruction. */
|
|
45
|
+
interface RejectionRecord {
|
|
46
|
+
readonly callId: string | undefined
|
|
47
|
+
readonly outcome: string | undefined
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* jscpd:ignore-start -- package companions share replay and dispatch plumbing */
|
|
51
|
+
/** Install verdict-chain and deny-marker validation over loaded logs and newly appended events. */
|
|
52
|
+
const install: InvariantInstaller = Object.assign((ctx: Context, fail: InvariantFailure) => {
|
|
53
|
+
// Install-scoped so a dispose/re-register cycle re-sweeps from a clean slate.
|
|
54
|
+
const askedIds = new WeakMap<Session, Map<string, number>>()
|
|
55
|
+
const verdicts = new WeakMap<Session, Map<string, VerdictRecord>>()
|
|
56
|
+
const verdictByApproval = new WeakMap<Session, Map<string, VerdictRecord>>()
|
|
57
|
+
const circuits = new WeakMap<Session, Map<string, string>>()
|
|
58
|
+
const rejections = new WeakMap<Session, Map<string, RejectionRecord>>()
|
|
59
|
+
|
|
60
|
+
const validateEvent = (session: Session, event: SessionEvent): void => {
|
|
61
|
+
if (event.type === 'approval/asked') {
|
|
62
|
+
let ids = askedIds.get(session)
|
|
63
|
+
if (ids === undefined) {
|
|
64
|
+
ids = new Map<string, number>()
|
|
65
|
+
askedIds.set(session, ids)
|
|
66
|
+
}
|
|
67
|
+
ids.set(event.data.id, (ids.get(event.data.id) ?? 0) + 1)
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
if (event.type === 'autoReview/verdict') {
|
|
71
|
+
const data = event.data
|
|
72
|
+
const fallback = data.fallback
|
|
73
|
+
if (fallback === undefined) {
|
|
74
|
+
if (data.decision === undefined || !DECISIONS.includes(data.decision)) {
|
|
75
|
+
fail(`autoReview/verdict ${JSON.stringify(data.reviewId)} has invalid decision ${JSON.stringify(data.decision)}`)
|
|
76
|
+
}
|
|
77
|
+
if (typeof data.reason !== 'string' || data.reason.length === 0) {
|
|
78
|
+
fail(`autoReview/verdict ${JSON.stringify(data.reviewId)} has no reviewer reason`)
|
|
79
|
+
}
|
|
80
|
+
if (data.riskLevel !== undefined && !RISK_LEVELS.includes(data.riskLevel)) {
|
|
81
|
+
fail(`autoReview/verdict ${JSON.stringify(data.reviewId)} has invalid riskLevel ${JSON.stringify(data.riskLevel)}`)
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
if (!AUTO_REVIEW_FALLBACKS.includes(fallback)) {
|
|
85
|
+
fail(`autoReview/verdict ${JSON.stringify(data.reviewId)} has invalid fallback ${JSON.stringify(fallback)}`)
|
|
86
|
+
}
|
|
87
|
+
if (data.decision !== undefined || data.reason !== undefined || data.riskLevel !== undefined) {
|
|
88
|
+
fail(`autoReview/verdict ${JSON.stringify(data.reviewId)} carries verdict fields alongside fallback ${JSON.stringify(fallback)}`)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (data.outcome !== undefined && !OUTCOMES.includes(data.outcome)) {
|
|
92
|
+
fail(`autoReview/verdict ${JSON.stringify(data.reviewId)} has invalid outcome ${JSON.stringify(data.outcome)}`)
|
|
93
|
+
}
|
|
94
|
+
if (data.escalation !== undefined) {
|
|
95
|
+
// The only escalation today: the risk policy overrode an allow. It
|
|
96
|
+
// must reference an allow verdict and can never settle allowed-once.
|
|
97
|
+
if (data.escalation !== 'risk-policy') {
|
|
98
|
+
fail(`autoReview/verdict ${JSON.stringify(data.reviewId)} has invalid escalation ${JSON.stringify(data.escalation)}`)
|
|
99
|
+
}
|
|
100
|
+
if (data.decision !== 'allow') {
|
|
101
|
+
fail(`autoReview/verdict ${JSON.stringify(data.reviewId)} escalates a non-allow verdict`)
|
|
102
|
+
}
|
|
103
|
+
if (data.outcome === 'allowed-once') {
|
|
104
|
+
fail(`autoReview/verdict ${JSON.stringify(data.reviewId)} escalates yet settles allowed-once`)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (!Number.isSafeInteger(data.durationMs) || data.durationMs < 0) {
|
|
108
|
+
fail(`autoReview/verdict ${JSON.stringify(data.reviewId)} has invalid durationMs ${String(data.durationMs)}`)
|
|
109
|
+
}
|
|
110
|
+
const count = askedIds.get(session)?.get(data.approvalId) ?? 0
|
|
111
|
+
if (count < 1) {
|
|
112
|
+
fail(`autoReview/verdict ${JSON.stringify(data.reviewId)} references no prior approval/asked ${JSON.stringify(data.approvalId)}`)
|
|
113
|
+
}
|
|
114
|
+
let verdictsForSession = verdicts.get(session)
|
|
115
|
+
if (verdictsForSession === undefined) {
|
|
116
|
+
verdictsForSession = new Map<string, VerdictRecord>()
|
|
117
|
+
verdicts.set(session, verdictsForSession)
|
|
118
|
+
}
|
|
119
|
+
if (verdictsForSession.has(data.reviewId)) {
|
|
120
|
+
fail(`autoReview/verdict repeats reviewId ${JSON.stringify(data.reviewId)}`)
|
|
121
|
+
}
|
|
122
|
+
const record: VerdictRecord = {
|
|
123
|
+
callId: data.callId,
|
|
124
|
+
decision: data.decision,
|
|
125
|
+
outcome: data.outcome,
|
|
126
|
+
fallback: data.fallback,
|
|
127
|
+
escalation: data.escalation,
|
|
128
|
+
}
|
|
129
|
+
verdictsForSession.set(data.reviewId, record)
|
|
130
|
+
let byApproval = verdictByApproval.get(session)
|
|
131
|
+
if (byApproval === undefined) {
|
|
132
|
+
byApproval = new Map<string, VerdictRecord>()
|
|
133
|
+
verdictByApproval.set(session, byApproval)
|
|
134
|
+
}
|
|
135
|
+
if (byApproval.has(data.approvalId)) {
|
|
136
|
+
fail(`autoReview/verdict ${JSON.stringify(data.reviewId)} is the second verdict for approval/asked ${JSON.stringify(data.approvalId)}`)
|
|
137
|
+
}
|
|
138
|
+
byApproval.set(data.approvalId, record)
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
if (event.type === 'autoReview/circuit') {
|
|
142
|
+
const data = event.data
|
|
143
|
+
if (!TRIP_KINDS.includes(data.trip.kind)) {
|
|
144
|
+
fail(`autoReview/circuit ${JSON.stringify(data.circuitId)} has invalid trip kind ${JSON.stringify(data.trip.kind)}`)
|
|
145
|
+
}
|
|
146
|
+
if (!CIRCUIT_ACTIONS.includes(data.action)) {
|
|
147
|
+
fail(`autoReview/circuit ${JSON.stringify(data.circuitId)} has invalid action ${JSON.stringify(data.action)}`)
|
|
148
|
+
}
|
|
149
|
+
if (!Number.isSafeInteger(data.trip.count) || data.trip.count < 1) {
|
|
150
|
+
fail(`autoReview/circuit ${JSON.stringify(data.circuitId)} has invalid trip count ${String(data.trip.count)}`)
|
|
151
|
+
}
|
|
152
|
+
let circuitIds = circuits.get(session)
|
|
153
|
+
if (circuitIds === undefined) {
|
|
154
|
+
circuitIds = new Map<string, string>()
|
|
155
|
+
circuits.set(session, circuitIds)
|
|
156
|
+
}
|
|
157
|
+
if (circuitIds.has(data.circuitId)) {
|
|
158
|
+
fail(`autoReview/circuit repeats circuitId ${JSON.stringify(data.circuitId)}`)
|
|
159
|
+
}
|
|
160
|
+
circuitIds.set(data.circuitId, data.action)
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
if (event.type === 'autoReview/override') {
|
|
164
|
+
const verdict = verdicts.get(session)?.get(event.data.reviewId)
|
|
165
|
+
if (verdict === undefined || (verdict.decision !== 'deny' && verdict.escalation !== 'risk-policy')) {
|
|
166
|
+
fail(`autoReview/override references reviewId ${JSON.stringify(event.data.reviewId)} without a prior denial verdict`)
|
|
167
|
+
}
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
if (event.type === 'autoReview/rejection') {
|
|
171
|
+
const data = event.data
|
|
172
|
+
if (typeof data.reason !== 'string' || data.reason.length === 0) {
|
|
173
|
+
fail(`autoReview/rejection ${JSON.stringify(data.rejectionId)} has no hard-disable reason`)
|
|
174
|
+
}
|
|
175
|
+
if (data.outcome !== 'rejected') {
|
|
176
|
+
fail(`autoReview/rejection ${JSON.stringify(data.rejectionId)} has invalid outcome ${JSON.stringify(data.outcome)}`)
|
|
177
|
+
}
|
|
178
|
+
if (data.approvalId !== undefined) {
|
|
179
|
+
const count = askedIds.get(session)?.get(data.approvalId) ?? 0
|
|
180
|
+
if (count < 1) {
|
|
181
|
+
fail(`autoReview/rejection ${JSON.stringify(data.rejectionId)} references no prior approval/asked ${JSON.stringify(data.approvalId)}`)
|
|
182
|
+
}
|
|
183
|
+
let byApproval = verdictByApproval.get(session)
|
|
184
|
+
if (byApproval === undefined) {
|
|
185
|
+
byApproval = new Map<string, VerdictRecord>()
|
|
186
|
+
verdictByApproval.set(session, byApproval)
|
|
187
|
+
}
|
|
188
|
+
if (byApproval.has(data.approvalId)) {
|
|
189
|
+
fail(`autoReview/rejection ${JSON.stringify(data.rejectionId)} is the second decision for approval/asked ${JSON.stringify(data.approvalId)}`)
|
|
190
|
+
}
|
|
191
|
+
byApproval.set(data.approvalId, {
|
|
192
|
+
callId: data.callId,
|
|
193
|
+
decision: undefined,
|
|
194
|
+
outcome: data.outcome,
|
|
195
|
+
fallback: undefined,
|
|
196
|
+
escalation: undefined,
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
let rejectionIds = rejections.get(session)
|
|
200
|
+
if (rejectionIds === undefined) {
|
|
201
|
+
rejectionIds = new Map<string, RejectionRecord>()
|
|
202
|
+
rejections.set(session, rejectionIds)
|
|
203
|
+
}
|
|
204
|
+
if (rejectionIds.has(data.rejectionId)) {
|
|
205
|
+
fail(`autoReview/rejection repeats rejectionId ${JSON.stringify(data.rejectionId)}`)
|
|
206
|
+
}
|
|
207
|
+
rejectionIds.set(data.rejectionId, { callId: data.callId, outcome: data.outcome })
|
|
208
|
+
return
|
|
209
|
+
}
|
|
210
|
+
if (event.type === 'approval/decided') {
|
|
211
|
+
// The answerer claims a request by appending its verdict BEFORE the
|
|
212
|
+
// service settles the decided pair; a claimed verdict must therefore
|
|
213
|
+
// agree with the recorded outcome (a `delegate` fallback omits its
|
|
214
|
+
// outcome because the downstream answerer owns the decision).
|
|
215
|
+
const claimed = verdictByApproval.get(session)?.get(event.data.id)
|
|
216
|
+
if (claimed?.outcome !== undefined && claimed.outcome !== event.data.outcome) {
|
|
217
|
+
fail(`approval/decided ${JSON.stringify(event.data.id)} outcome ${JSON.stringify(event.data.outcome)} contradicts the autoReview/verdict outcome ${JSON.stringify(claimed.outcome)}`)
|
|
218
|
+
}
|
|
219
|
+
return
|
|
220
|
+
}
|
|
221
|
+
if (event.type === 'tool/result') {
|
|
222
|
+
const block = event.data.message.content[0]
|
|
223
|
+
if (block === undefined || block.type !== 'tool-result') {
|
|
224
|
+
// Not a tool-result projection this invariant owns; skip.
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
const text = block.content
|
|
228
|
+
.filter(item => item.type === 'text')
|
|
229
|
+
.map(item => (item as { text: string }).text)
|
|
230
|
+
.join('\n')
|
|
231
|
+
const denyMatch = DENY_MARKER_PATTERN.exec(text)
|
|
232
|
+
if (denyMatch !== null) {
|
|
233
|
+
const reviewId = denyMatch[1]
|
|
234
|
+
if (reviewId === undefined) {
|
|
235
|
+
fail(`tool/result carries an unparseable deny marker: ${JSON.stringify(denyMatch[0])}`)
|
|
236
|
+
return
|
|
237
|
+
}
|
|
238
|
+
const verdict = verdicts.get(session)?.get(reviewId)
|
|
239
|
+
if (verdict === undefined) {
|
|
240
|
+
fail(`tool/result deny marker references unknown reviewId ${JSON.stringify(reviewId)}`)
|
|
241
|
+
return
|
|
242
|
+
}
|
|
243
|
+
if (verdict.decision !== 'deny' && !(verdict.escalation === 'risk-policy' && verdict.outcome === 'rejected')) {
|
|
244
|
+
fail(`tool/result deny marker references a non-deny verdict ${JSON.stringify(reviewId)}`)
|
|
245
|
+
}
|
|
246
|
+
if (verdict.callId !== undefined && verdict.callId !== block.toolCallId) {
|
|
247
|
+
fail(`tool/result deny marker for review ${JSON.stringify(reviewId)} has call id ${JSON.stringify(block.toolCallId)}, expected ${JSON.stringify(verdict.callId)}`)
|
|
248
|
+
}
|
|
249
|
+
return
|
|
250
|
+
}
|
|
251
|
+
const fallbackMatch = FALLBACK_MARKER_PATTERN.exec(text)
|
|
252
|
+
if (fallbackMatch !== null) {
|
|
253
|
+
const reviewId = fallbackMatch[1]
|
|
254
|
+
if (reviewId === undefined) {
|
|
255
|
+
fail(`tool/result carries an unparseable fallback marker: ${JSON.stringify(fallbackMatch[0])}`)
|
|
256
|
+
return
|
|
257
|
+
}
|
|
258
|
+
const verdict = verdicts.get(session)?.get(reviewId)
|
|
259
|
+
if (verdict === undefined) {
|
|
260
|
+
fail(`tool/result fallback marker references unknown reviewId ${JSON.stringify(reviewId)}`)
|
|
261
|
+
return
|
|
262
|
+
}
|
|
263
|
+
if (verdict.fallback === undefined || verdict.outcome !== 'rejected') {
|
|
264
|
+
fail(`tool/result fallback marker references a verdict that was not rejected by fallback ${JSON.stringify(reviewId)}`)
|
|
265
|
+
}
|
|
266
|
+
if (verdict.callId !== undefined && verdict.callId !== block.toolCallId) {
|
|
267
|
+
fail(`tool/result fallback marker for review ${JSON.stringify(reviewId)} has call id ${JSON.stringify(block.toolCallId)}, expected ${JSON.stringify(verdict.callId)}`)
|
|
268
|
+
}
|
|
269
|
+
return
|
|
270
|
+
}
|
|
271
|
+
const circuitMatch = CIRCUIT_MARKER_PATTERN.exec(text)
|
|
272
|
+
if (circuitMatch !== null) {
|
|
273
|
+
const circuitId = circuitMatch[1]
|
|
274
|
+
const toolName = circuitMatch[2]
|
|
275
|
+
if (circuitId === undefined || toolName === undefined) {
|
|
276
|
+
fail(`tool/result carries an unparseable circuit marker: ${JSON.stringify(circuitMatch[0])}`)
|
|
277
|
+
return
|
|
278
|
+
}
|
|
279
|
+
const action = circuits.get(session)?.get(circuitId)
|
|
280
|
+
if (action === undefined) {
|
|
281
|
+
fail(`tool/result circuit marker references unknown circuitId ${JSON.stringify(circuitId)}`)
|
|
282
|
+
return
|
|
283
|
+
}
|
|
284
|
+
if (action === 'delegate') {
|
|
285
|
+
fail(`tool/result circuit marker references a delegate-action circuit ${JSON.stringify(circuitId)}`)
|
|
286
|
+
}
|
|
287
|
+
return
|
|
288
|
+
}
|
|
289
|
+
const neverMatch = NEVER_MARKER_PATTERN.exec(text)
|
|
290
|
+
if (neverMatch === null) return
|
|
291
|
+
const rejectionId = neverMatch[1]
|
|
292
|
+
if (rejectionId === undefined) {
|
|
293
|
+
fail(`tool/result carries an unparseable never marker: ${JSON.stringify(neverMatch[0])}`)
|
|
294
|
+
return
|
|
295
|
+
}
|
|
296
|
+
const rejection = rejections.get(session)?.get(rejectionId)
|
|
297
|
+
if (rejection === undefined) {
|
|
298
|
+
fail(`tool/result never marker references unknown rejectionId ${JSON.stringify(rejectionId)}`)
|
|
299
|
+
return
|
|
300
|
+
}
|
|
301
|
+
if (rejection.outcome !== 'rejected') {
|
|
302
|
+
fail(`tool/result never marker references a rejection that did not settle rejected ${JSON.stringify(rejectionId)}`)
|
|
303
|
+
}
|
|
304
|
+
if (rejection.callId !== undefined && rejection.callId !== block.toolCallId) {
|
|
305
|
+
fail(`tool/result never marker for rejection ${JSON.stringify(rejectionId)} has call id ${JSON.stringify(block.toolCallId)}, expected ${JSON.stringify(rejection.callId)}`)
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
for (const session of ctx.sessions.list()) {
|
|
310
|
+
for (const event of session.events) validateEvent(session, event)
|
|
311
|
+
}
|
|
312
|
+
ctx.on('internal/dispatch', (_mode, eventName, args) => {
|
|
313
|
+
if (eventName !== 'session/event') return
|
|
314
|
+
const [session, event] = args as [Session, SessionEvent]
|
|
315
|
+
validateEvent(session, event)
|
|
316
|
+
}, { global: true })
|
|
317
|
+
}, { inject: ['sessions'] })
|
|
318
|
+
/* jscpd:ignore-end */
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Register this package's invariant companion.
|
|
322
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
323
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
324
|
+
*/
|
|
325
|
+
export const apply = (ctx: Context): Promise<() => void> =>
|
|
326
|
+
Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
|
package/src/messages.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User-facing command strings for `dsh-auto-review` in the supported
|
|
3
|
+
* languages. The table is a pure function of the resolved `language` config;
|
|
4
|
+
* the English strings are the source of truth, the Chinese twin mirrors
|
|
5
|
+
* them. Reviewer-prompt content is NOT here (it is config-provided
|
|
6
|
+
* guidance/policy text, not command UI).
|
|
7
|
+
* @module dsh-auto-review/messages
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Supported UI languages for the `/auto-review` command output. */
|
|
11
|
+
export type UiLanguage = 'en' | 'zh'
|
|
12
|
+
|
|
13
|
+
/** All command strings of one language. */
|
|
14
|
+
export interface Messages {
|
|
15
|
+
readonly usage: string
|
|
16
|
+
readonly description: string
|
|
17
|
+
readonly statusLine: (enabled: boolean) => string
|
|
18
|
+
readonly verdictsLine: (used: number, max: number) => string
|
|
19
|
+
readonly failuresLine: (used: number, max: number) => string
|
|
20
|
+
readonly circuitLine: (kind: string, count: number, action: string) => string
|
|
21
|
+
readonly allTimeLine: (allows: number, denies: number, fallbacks: number, rejections: number, avg: number) => string
|
|
22
|
+
readonly recentLine: (labels: string) => string
|
|
23
|
+
readonly unknownArg: (arg: string) => string
|
|
24
|
+
readonly already: (state: string) => string
|
|
25
|
+
readonly switchedNotice: (enabled: boolean) => string
|
|
26
|
+
readonly switchedResult: (state: string) => string
|
|
27
|
+
readonly approveResult: (toolName: string, reviewId: string, minutes: number) => string
|
|
28
|
+
readonly approveNone: (index: number, count: number) => string
|
|
29
|
+
readonly approveInvalid: (arg: string) => string
|
|
30
|
+
readonly circuitNotice: (kind: string, count: number) => string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const EN: Messages = {
|
|
34
|
+
usage: 'Usage: /auto-review on|off|status|approve [n]',
|
|
35
|
+
description: 'enable or disable second-model AI auto-review for this session, inspect verdicts, or approve one retry',
|
|
36
|
+
statusLine: enabled => `Auto-review is ${enabled ? 'ON' : 'OFF'} for this session.`,
|
|
37
|
+
verdictsLine: (used, max) => `AI verdicts this turn: ${used}/${max}`,
|
|
38
|
+
failuresLine: (used, max) => `Reviewer failures this turn: ${used}/${max}`,
|
|
39
|
+
circuitLine: (kind, count, action) => `Rejection circuit breaker tripped (${kind}: ${count} denials); later requests in this turn follow "${action}".`,
|
|
40
|
+
allTimeLine: (allows, denies, fallbacks, rejections, avg) => `All-time: ${allows} allows, ${denies} denies, ${fallbacks} fallbacks, ${rejections} never rejects (avg ${avg} ms per verdict).`,
|
|
41
|
+
recentLine: labels => `Recent verdicts: ${labels}`,
|
|
42
|
+
unknownArg: arg => `Unknown /auto-review argument "${arg}". Usage: /auto-review on|off|status|approve [n]`,
|
|
43
|
+
already: state => `Auto-review is already ${state} for this session.`,
|
|
44
|
+
switchedNotice: enabled => `AI auto-review was switched ${enabled ? 'ON' : 'OFF'} for this session (changed by the user).`,
|
|
45
|
+
switchedResult: state => `Auto-review ${state} for this session.`,
|
|
46
|
+
approveResult: (toolName, reviewId, minutes) => [
|
|
47
|
+
`Authorized ONE retry of the denied ${toolName} action (review ${reviewId}).`,
|
|
48
|
+
`The next ${toolName} review within ${minutes} minutes carries this authorization as reviewer context — the reviewer still decides.`,
|
|
49
|
+
].join('\n'),
|
|
50
|
+
approveNone: (index, count) => `No recent denial #${index} in this session (${count} recorded).`,
|
|
51
|
+
approveInvalid: arg => `Invalid /auto-review approve index "${arg}". Usage: /auto-review approve [n]`,
|
|
52
|
+
circuitNotice: (kind, count) => `The AI auto-review rejection circuit breaker tripped (${kind}: ${count} denials). `
|
|
53
|
+
+ 'This turn is aborted because the agent kept proposing blocked actions. '
|
|
54
|
+
+ 'Adjust the sandbox boundary or the review policy, then continue.',
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const ZH: Messages = {
|
|
58
|
+
usage: '用法:/auto-review on|off|status|approve [n]',
|
|
59
|
+
description: '为本会话启用或禁用第二模型 AI 自动审查、查看裁决统计,或批准一次重试',
|
|
60
|
+
statusLine: enabled => `本会话的自动审查已${enabled ? '开启' : '关闭'}。`,
|
|
61
|
+
verdictsLine: (used, max) => `本回合 AI 裁决数:${used}/${max}`,
|
|
62
|
+
failuresLine: (used, max) => `本回合审查失败数:${used}/${max}`,
|
|
63
|
+
circuitLine: (kind, count, action) => `拒绝熔断器已触发(${kind}:${count} 次拒绝);本回合后续请求按 "${action}" 处理。`,
|
|
64
|
+
allTimeLine: (allows, denies, fallbacks, rejections, avg) => `累计:${allows} 次放行,${denies} 次拒绝,${fallbacks} 次失败,${rejections} 次硬禁用拒绝(每次裁决平均 ${avg} ms)。`,
|
|
65
|
+
recentLine: labels => `最近裁决:${labels}`,
|
|
66
|
+
unknownArg: arg => `未知的 /auto-review 参数 "${arg}"。用法:/auto-review on|off|status|approve [n]`,
|
|
67
|
+
already: state => `本会话的自动审查已处于 ${state} 状态。`,
|
|
68
|
+
switchedNotice: enabled => `AI 自动审查已为本会话${enabled ? '开启' : '关闭'}(由用户更改)。`,
|
|
69
|
+
switchedResult: state => `本会话的自动审查已${state}。`,
|
|
70
|
+
approveResult: (toolName, reviewId, minutes) => [
|
|
71
|
+
`已批准对 ${toolName} 被拒操作的一次重试(审查 ${reviewId})。`,
|
|
72
|
+
`接下来 ${minutes} 分钟内对 ${toolName} 的下一次审查将携带该授权作为审查上下文——最终仍由审查代理裁决。`,
|
|
73
|
+
].join('\n'),
|
|
74
|
+
approveNone: (index, count) => `本会话没有第 ${index} 条近期拒绝记录(共 ${count} 条)。`,
|
|
75
|
+
approveInvalid: arg => `无效的 /auto-review approve 序号 "${arg}"。用法:/auto-review approve [n]`,
|
|
76
|
+
circuitNotice: (kind, count) => `AI 自动审查拒绝熔断器触发(${kind}:${count} 次拒绝)。`
|
|
77
|
+
+ '由于代理持续提出被阻止的操作,本回合已中止。'
|
|
78
|
+
+ '请调整沙箱边界或审查策略后继续。',
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The message tables, keyed by {@link UiLanguage}. */
|
|
82
|
+
export const MESSAGES: Readonly<Record<UiLanguage, Messages>> = { en: EN, zh: ZH }
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Pick one language's message table.
|
|
86
|
+
* @param language - the resolved UI language.
|
|
87
|
+
* @returns the table (English and Chinese are the shipped languages).
|
|
88
|
+
*/
|
|
89
|
+
export function messages(language: UiLanguage): Messages {
|
|
90
|
+
return MESSAGES[language]
|
|
91
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure-type outlet of the `autoReview` session projection: the wire value
|
|
3
|
+
* the host folds and the browser review panel renders. Zero value imports —
|
|
4
|
+
* client programs pull these types without dragging the host chain.
|
|
5
|
+
* @module dsh-auto-review/projection-types
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** One verdict, presented for the panel. */
|
|
9
|
+
export interface AutoReviewVerdictView {
|
|
10
|
+
readonly reviewId: string
|
|
11
|
+
readonly toolName: string
|
|
12
|
+
readonly at: number
|
|
13
|
+
readonly durationMs: number
|
|
14
|
+
readonly decision?: 'allow' | 'deny'
|
|
15
|
+
readonly riskLevel?: 'low' | 'medium' | 'high'
|
|
16
|
+
readonly escalation?: 'risk-policy'
|
|
17
|
+
readonly fallback?: string
|
|
18
|
+
readonly outcome?: string
|
|
19
|
+
readonly reason?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** One recent denial offered for a one-shot approve (newest first). */
|
|
23
|
+
export interface AutoReviewDenyView {
|
|
24
|
+
readonly reviewId: string
|
|
25
|
+
readonly toolName: string
|
|
26
|
+
readonly reason?: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The whole wire value folded by the `autoReview` projection unit. */
|
|
30
|
+
export interface AutoReviewProjection {
|
|
31
|
+
/** The session's switch — the last `autoReview/state` event, or the `enableByDefault` default. */
|
|
32
|
+
readonly enabled: boolean
|
|
33
|
+
/** Real AI verdicts in the current open turn. */
|
|
34
|
+
readonly verdictsUsed: number
|
|
35
|
+
/** Reviewer failures in the current open turn. */
|
|
36
|
+
readonly failuresUsed: number
|
|
37
|
+
/** Cumulative counts over the whole session. */
|
|
38
|
+
readonly allows: number
|
|
39
|
+
readonly denies: number
|
|
40
|
+
readonly fallbacks: number
|
|
41
|
+
/** Hard-disable (`never` policy) rejections — no reviewer ran for these. */
|
|
42
|
+
readonly neverRejects: number
|
|
43
|
+
/** Mean duration of decision-carrying verdicts, rounded; 0 without any. */
|
|
44
|
+
readonly avgDurationMs: number
|
|
45
|
+
/** The current turn's circuit trip, or null. */
|
|
46
|
+
readonly circuit: {
|
|
47
|
+
readonly action: 'delegate' | 'reject' | 'abort-turn'
|
|
48
|
+
readonly trip: { readonly kind: 'consecutive' | 'window'; readonly count: number }
|
|
49
|
+
readonly toolName: string
|
|
50
|
+
} | null
|
|
51
|
+
/** The last 8 verdicts, newest first. */
|
|
52
|
+
readonly recent: readonly AutoReviewVerdictView[]
|
|
53
|
+
/** The last 5 denials, newest first (approve candidates; index 0 = approve 1). */
|
|
54
|
+
readonly recentDenies: readonly AutoReviewDenyView[]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
declare module '@deepseek-ai/dsh-session-projection/types' {
|
|
58
|
+
interface SessionProjectionMap {
|
|
59
|
+
autoReview: AutoReviewProjection
|
|
60
|
+
}
|
|
61
|
+
}
|