dsh-llm-verifier 0.6.0 → 0.6.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.
Files changed (61) hide show
  1. package/README.md +3 -0
  2. package/lib/{auto-DAMeKRkK.js → auto-caXDU2pw.js} +19 -1
  3. package/lib/{auto-DAMeKRkK.js.map → auto-caXDU2pw.js.map} +1 -1
  4. package/lib/client.js +493 -331
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +201 -52
  7. package/lib/index.js.map +1 -1
  8. package/lib/replay.js +1 -1
  9. package/lib/types/client-i18n.d.ts +36 -11
  10. package/lib/types/client-i18n.d.ts.map +1 -1
  11. package/lib/types/client-i18n.js +50 -9
  12. package/lib/types/client-i18n.js.map +1 -1
  13. package/lib/types/client.d.ts +17 -7
  14. package/lib/types/client.d.ts.map +1 -1
  15. package/lib/types/client.js +165 -44
  16. package/lib/types/client.js.map +1 -1
  17. package/lib/types/decisions.d.ts +8 -0
  18. package/lib/types/decisions.d.ts.map +1 -1
  19. package/lib/types/decisions.js +13 -1
  20. package/lib/types/decisions.js.map +1 -1
  21. package/lib/types/index.d.ts.map +1 -1
  22. package/lib/types/index.js +47 -31
  23. package/lib/types/index.js.map +1 -1
  24. package/lib/types/process-selection.d.ts +27 -5
  25. package/lib/types/process-selection.d.ts.map +1 -1
  26. package/lib/types/process-selection.js +17 -8
  27. package/lib/types/process-selection.js.map +1 -1
  28. package/lib/types/router.d.ts +22 -0
  29. package/lib/types/router.d.ts.map +1 -1
  30. package/lib/types/router.js +20 -0
  31. package/lib/types/router.js.map +1 -1
  32. package/lib/types/statistics.d.ts +8 -0
  33. package/lib/types/statistics.d.ts.map +1 -1
  34. package/lib/types/statistics.js +1 -1
  35. package/lib/types/statistics.js.map +1 -1
  36. package/lib/types/verifier-activity.d.ts +189 -0
  37. package/lib/types/verifier-activity.d.ts.map +1 -0
  38. package/lib/types/verifier-activity.js +223 -0
  39. package/lib/types/verifier-activity.js.map +1 -0
  40. package/package.json +1 -1
  41. package/src/client-i18n.ts +55 -16
  42. package/src/client.test.ts +101 -16
  43. package/src/client.tsx +238 -103
  44. package/src/decisions.test.ts +16 -0
  45. package/src/decisions.ts +22 -1
  46. package/src/index.test.ts +32 -0
  47. package/src/index.ts +48 -33
  48. package/src/process-selection.test.ts +13 -0
  49. package/src/process-selection.ts +39 -14
  50. package/src/router.test.ts +46 -0
  51. package/src/router.ts +31 -0
  52. package/src/statistics.test.ts +16 -0
  53. package/src/statistics.ts +9 -1
  54. package/src/verifier-activity.test.ts +164 -0
  55. package/src/verifier-activity.ts +321 -0
  56. package/lib/types/process-activity.d.ts +0 -109
  57. package/lib/types/process-activity.d.ts.map +0 -1
  58. package/lib/types/process-activity.js +0 -135
  59. package/lib/types/process-activity.js.map +0 -1
  60. package/src/process-activity.test.ts +0 -91
  61. package/src/process-activity.ts +0 -177
@@ -0,0 +1,189 @@
1
+ /**
2
+ * In-memory view of the verifier's in-flight cycles, for the chat chip.
3
+ *
4
+ * WHY THIS EXISTS
5
+ * ---------------
6
+ * Every automatic stage runs judge calls while the chat shows nothing about it. Two of them are
7
+ * silent windows rather than "no output at all":
8
+ *
9
+ * - P06 buffers the main reply, so while a cycle runs there is no streaming text whatsoever —
10
+ * indistinguishable from a hang;
11
+ * - the routed reviews (compare/select/track) and the final acceptance run inside
12
+ * `agent/turn-stopping`, where the reply is already on screen but the turn stays open for as long
13
+ * as the judges take (a bounded but real pause, and a passing acceptance says nothing at all).
14
+ *
15
+ * The harness renders the host's own inline indicator ("Compacting context…") from a *session event*
16
+ * pair, and that route is closed to an out-of-repo plugin: the persistence read path
17
+ * (`validateStoredEvents` in `@deepseek-ai/dsh-session-persistence`) refuses to interpret a log
18
+ * containing an event type outside the repository-generated `KNOWN_SESSION_EVENT_TYPES` unless the
19
+ * stored envelope carries `ignorable: true`, and `Session.append` cannot set that marker — only a
20
+ * seed/restore envelope can. Appending our own type would therefore make the topic unloadable.
21
+ *
22
+ * So the indicator is deliberately host-only state: the router (the one place that knows a cycle was
23
+ * granted) and the P06 selector publish what they are doing here, the plugin's existing RPC answers
24
+ * a read of it, and the client polls that read only while a turn runs. Nothing here is durable (a
25
+ * reload shows nothing, which is correct: the cycle it described is gone) and nothing here touches
26
+ * the model history, the session log, the statistics or any verdict.
27
+ *
28
+ * Pure bookkeeping: no I/O, no clock of its own (callers pass `now`), so it is directly testable.
29
+ */
30
+ import type { Reservation, RouterCycleObserver } from './router.ts';
31
+ /** Which part of the plugin owns a cycle. */
32
+ export type ActivityStage = 'process' | 'route' | 'final';
33
+ /** What a cycle is doing right now, as far as the UI can tell it apart. */
34
+ export type ActivityPhase = 'generating' | 'comparing' | 'classifying' | 'reviewing' | 'accepting';
35
+ /** The routed decision a `route` cycle resolved to, once classification promoted it. */
36
+ export type ActivityDestination = 'compare' | 'select' | 'track';
37
+ /** How a finished cycle resolved, from the user's point of view. */
38
+ export type ActivityOutcome = 'replaced' | 'kept' | 'same' | 'failed' | 'accepted' | 'rejected';
39
+ /** One cycle in flight. */
40
+ export interface ActivityActive {
41
+ cycleId: string;
42
+ stage: ActivityStage;
43
+ phase: ActivityPhase;
44
+ /** Routing target of a `route` cycle; absent while it is still classifying and for the others. */
45
+ destination?: ActivityDestination;
46
+ /** Candidates in the P06 cycle, the original reply included (2..4). */
47
+ candidates?: number;
48
+ /** Model calls this cycle reserved, for the copy that quotes a size. */
49
+ expectedCalls?: number;
50
+ /** `provider/model` of P06's second generator, when `autoProcessAlternativeModel` is set. */
51
+ alternativeModel?: string;
52
+ startedAt: number;
53
+ }
54
+ /** One cycle that just finished; kept only long enough for the chip to explain the pause. */
55
+ export interface ActivitySettled {
56
+ cycleId: string;
57
+ stage: ActivityStage;
58
+ outcome: ActivityOutcome;
59
+ candidates?: number;
60
+ /** Wall clock the cycle settled at. */
61
+ at: number;
62
+ }
63
+ /** What one session currently has to show. Both fields absent means "nothing to say". */
64
+ export interface ActivityView {
65
+ active?: ActivityActive;
66
+ settled?: ActivitySettled;
67
+ }
68
+ /**
69
+ * How long a settled cycle keeps explaining itself.
70
+ *
71
+ * Long enough to be read after the reply lands (the chat scrolls as soon as the winner replays),
72
+ * short enough that a stale line never becomes part of the transcript.
73
+ */
74
+ export declare const ACTIVITY_SETTLED_TTL_MS = 20000;
75
+ /**
76
+ * Safety bound on an in-flight record.
77
+ *
78
+ * Every cycle is bounded by the configured phase timeout and settles on every path we know of, so
79
+ * this only ever fires for a cycle that leaked (a bug, or a host that never calls back). Without it
80
+ * a leaked record would pin the chip in "working…" forever, because the client keeps polling while
81
+ * it has something to render.
82
+ */
83
+ export declare const ACTIVITY_ACTIVE_TTL_MS = 600000;
84
+ /**
85
+ * Map one finished P06 cycle onto the outcome the chip distinguishes.
86
+ *
87
+ * `replayed` is the host-facing fact ("which stream did the host actually receive") and wins over
88
+ * the outcome label: a cycle recorded as `candidate-selected` whose winner was withheld before the
89
+ * first chunk is corrected to `replayed: 'original'` and must read as "kept", not as a replacement.
90
+ * @param outcome - the durable outcome string the cycle was recorded with.
91
+ * @param replayed - which stream the host actually received.
92
+ * @returns The chip-level outcome.
93
+ */
94
+ export declare function classifyProcessOutcome(outcome: string, replayed: 'original' | 'candidate' | 'none'): ActivityOutcome;
95
+ /**
96
+ * Map one granted reservation onto the chip's active record.
97
+ *
98
+ * `process` is deliberately NOT mapped: P06 publishes its own richer record (candidate count,
99
+ * generation vs comparison), and two writers for one cycle id would race. `plan_review` and
100
+ * `team_task` are not announced either — the plan gate already shows a pending tool row in the
101
+ * chat, so a chip line would only repeat it.
102
+ * @param reservation - the reservation the router just granted.
103
+ * @param startedAt - wall clock the cycle began at.
104
+ * @returns The active record, or undefined for a phase the chip stays silent about.
105
+ */
106
+ export declare function reservationActivity(reservation: Pick<Reservation, 'id' | 'phase' | 'expectedCalls'>, startedAt: number): ActivityActive | undefined;
107
+ /**
108
+ * Map a promotion onto the update the chip needs.
109
+ *
110
+ * A classification cycle that resolved to a decision stays the SAME reservation (that is what
111
+ * promotion is for), so the chip must be moved from "classifying" to "reviewing" instead of leaving
112
+ * a second cycle behind.
113
+ * @param reservation - the promoted reservation, already carrying its execution phase.
114
+ * @returns The patch, or undefined when the phase is not one the chip announces.
115
+ */
116
+ export declare function reservationPromotion(reservation: Pick<Reservation, 'phase' | 'expectedCalls'>): Pick<ActivityActive, 'phase' | 'expectedCalls'> & {
117
+ destination?: ActivityDestination;
118
+ } | undefined;
119
+ /**
120
+ * Map a router settlement onto the chip's settled row.
121
+ *
122
+ * A routed verdict is steered into the chat by the caller right after the cycle settles, so a second
123
+ * transient line would only repeat it; the final gate is the opposite (a PASSING acceptance steers
124
+ * nothing), and that is exactly the case worth showing. `undefined` means "clear the active row and
125
+ * leave nothing behind".
126
+ * @param reservation - the reservation being settled.
127
+ * @param outcome - whether the router committed or failed it.
128
+ * @returns The stage and outcome to keep, or undefined to clear silently.
129
+ */
130
+ export declare function reservationSettled(reservation: Pick<Reservation, 'id' | 'phase'>, outcome: 'committed' | 'failed'): {
131
+ stage: ActivityStage;
132
+ outcome: ActivityOutcome;
133
+ } | undefined;
134
+ /**
135
+ * Build the router's advisory observer over one activity table.
136
+ *
137
+ * The router supplies the reservation (it is the only component that knows a cycle was granted and
138
+ * when it settled); this module owns what the chip says about it.
139
+ * @param activities - the table the plugin's RPC reads.
140
+ * @param now - wall clock used for the records and their TTLs.
141
+ * @returns The observer to hand to `new AutoVerifierRouter(...)`.
142
+ */
143
+ export declare function createActivityObserver(activities: VerifierActivities, now?: () => number): RouterCycleObserver;
144
+ /**
145
+ * Per-session activity table.
146
+ *
147
+ * One active cycle per session, because the router owns one in-flight reservation per agent at a
148
+ * time; a settled cycle replaces the previous settled one (the chip is a status line, not a history
149
+ * — the statistics sidecar is the history).
150
+ */
151
+ export declare class VerifierActivities {
152
+ private readonly active;
153
+ private readonly settled;
154
+ /** Publish one cycle as in flight. */
155
+ begin(sessionId: string, activity: ActivityActive): void;
156
+ /**
157
+ * Move one in-flight cycle to a later phase.
158
+ *
159
+ * Guarded by cycle id: a cycle that already settled (or was replaced by a newer one) must not be
160
+ * moved back into "in flight" by a late async step.
161
+ * @returns whether the exact cycle was the active one.
162
+ */
163
+ update(sessionId: string, cycleId: string, patch: Partial<Pick<ActivityActive, 'phase' | 'destination' | 'candidates' | 'expectedCalls'>>): boolean;
164
+ /**
165
+ * Retire one cycle.
166
+ *
167
+ * A cycle nobody observed (the purchase itself failed, so `begin` never ran) leaves nothing
168
+ * behind: the chip must not claim a cycle that never started. A cycle that WAS observed stays
169
+ * correctable while its own settled record is the current one, which is what P06's late
170
+ * "the winner was not delivered" correction needs.
171
+ * @param sessionId - the session the cycle belongs to.
172
+ * @param cycleId - the cycle to retire.
173
+ * @param outcome - the settled row to keep, or undefined to clear without leaving one.
174
+ * @param now - wall clock used for the settled record.
175
+ * @returns whether the cycle was known and retired.
176
+ */
177
+ finish(sessionId: string, cycleId: string, outcome: ActivityOutcome | undefined, now: number): boolean;
178
+ /**
179
+ * What one session has to show right now, with expired records pruned in place.
180
+ * @param sessionId - the session to read.
181
+ * @param now - wall clock used for both TTLs.
182
+ */
183
+ read(sessionId: string, now: number): ActivityView;
184
+ /** Forget one session entirely (settings changed, task replaced, agent disposed). */
185
+ clear(sessionId: string): void;
186
+ /** Forget every session (settings change, shutdown). */
187
+ clearAll(): void;
188
+ }
189
+ //# sourceMappingURL=verifier-activity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verifier-activity.d.ts","sourceRoot":"","sources":["../../src/verifier-activity.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,223 @@
1
+ /**
2
+ * How long a settled cycle keeps explaining itself.
3
+ *
4
+ * Long enough to be read after the reply lands (the chat scrolls as soon as the winner replays),
5
+ * short enough that a stale line never becomes part of the transcript.
6
+ */
7
+ export const ACTIVITY_SETTLED_TTL_MS = 20_000;
8
+ /**
9
+ * Safety bound on an in-flight record.
10
+ *
11
+ * Every cycle is bounded by the configured phase timeout and settles on every path we know of, so
12
+ * this only ever fires for a cycle that leaked (a bug, or a host that never calls back). Without it
13
+ * a leaked record would pin the chip in "working…" forever, because the client keeps polling while
14
+ * it has something to render.
15
+ */
16
+ export const ACTIVITY_ACTIVE_TTL_MS = 600_000;
17
+ /**
18
+ * Map one finished P06 cycle onto the outcome the chip distinguishes.
19
+ *
20
+ * `replayed` is the host-facing fact ("which stream did the host actually receive") and wins over
21
+ * the outcome label: a cycle recorded as `candidate-selected` whose winner was withheld before the
22
+ * first chunk is corrected to `replayed: 'original'` and must read as "kept", not as a replacement.
23
+ * @param outcome - the durable outcome string the cycle was recorded with.
24
+ * @param replayed - which stream the host actually received.
25
+ * @returns The chip-level outcome.
26
+ */
27
+ export function classifyProcessOutcome(outcome, replayed) {
28
+ if (replayed === 'candidate')
29
+ return 'replaced';
30
+ if (outcome === 'identical-candidate')
31
+ return 'same';
32
+ if (outcome === 'tie' || outcome === 'original-selected')
33
+ return 'kept';
34
+ if (outcome.startsWith('candidate-not-delivered'))
35
+ return 'kept';
36
+ return 'failed';
37
+ }
38
+ /**
39
+ * Map one granted reservation onto the chip's active record.
40
+ *
41
+ * `process` is deliberately NOT mapped: P06 publishes its own richer record (candidate count,
42
+ * generation vs comparison), and two writers for one cycle id would race. `plan_review` and
43
+ * `team_task` are not announced either — the plan gate already shows a pending tool row in the
44
+ * chat, so a chip line would only repeat it.
45
+ * @param reservation - the reservation the router just granted.
46
+ * @param startedAt - wall clock the cycle began at.
47
+ * @returns The active record, or undefined for a phase the chip stays silent about.
48
+ */
49
+ export function reservationActivity(reservation, startedAt) {
50
+ switch (reservation.phase) {
51
+ case 'semantic':
52
+ return { cycleId: reservation.id, stage: 'route', phase: 'classifying', expectedCalls: reservation.expectedCalls, startedAt };
53
+ case 'compare':
54
+ case 'select':
55
+ case 'track':
56
+ return { cycleId: reservation.id, stage: 'route', phase: 'reviewing', destination: reservation.phase, expectedCalls: reservation.expectedCalls, startedAt };
57
+ case 'final':
58
+ return { cycleId: reservation.id, stage: 'final', phase: 'accepting', expectedCalls: reservation.expectedCalls, startedAt };
59
+ default:
60
+ return undefined;
61
+ }
62
+ }
63
+ /**
64
+ * Map a promotion onto the update the chip needs.
65
+ *
66
+ * A classification cycle that resolved to a decision stays the SAME reservation (that is what
67
+ * promotion is for), so the chip must be moved from "classifying" to "reviewing" instead of leaving
68
+ * a second cycle behind.
69
+ * @param reservation - the promoted reservation, already carrying its execution phase.
70
+ * @returns The patch, or undefined when the phase is not one the chip announces.
71
+ */
72
+ export function reservationPromotion(reservation) {
73
+ switch (reservation.phase) {
74
+ case 'compare':
75
+ case 'select':
76
+ case 'track':
77
+ return { phase: 'reviewing', destination: reservation.phase, expectedCalls: reservation.expectedCalls };
78
+ default:
79
+ return undefined;
80
+ }
81
+ }
82
+ /**
83
+ * Map a router settlement onto the chip's settled row.
84
+ *
85
+ * A routed verdict is steered into the chat by the caller right after the cycle settles, so a second
86
+ * transient line would only repeat it; the final gate is the opposite (a PASSING acceptance steers
87
+ * nothing), and that is exactly the case worth showing. `undefined` means "clear the active row and
88
+ * leave nothing behind".
89
+ * @param reservation - the reservation being settled.
90
+ * @param outcome - whether the router committed or failed it.
91
+ * @returns The stage and outcome to keep, or undefined to clear silently.
92
+ */
93
+ export function reservationSettled(reservation, outcome) {
94
+ return reservation.phase === 'final'
95
+ ? { stage: 'final', outcome: outcome === 'committed' ? 'accepted' : 'rejected' }
96
+ : undefined;
97
+ }
98
+ /**
99
+ * Build the router's advisory observer over one activity table.
100
+ *
101
+ * The router supplies the reservation (it is the only component that knows a cycle was granted and
102
+ * when it settled); this module owns what the chip says about it.
103
+ * @param activities - the table the plugin's RPC reads.
104
+ * @param now - wall clock used for the records and their TTLs.
105
+ * @returns The observer to hand to `new AutoVerifierRouter(...)`.
106
+ */
107
+ export function createActivityObserver(activities, now = Date.now) {
108
+ const key = (agent) => String(agent.id);
109
+ return {
110
+ begin: (agent, reservation) => {
111
+ const activity = reservationActivity(reservation, now());
112
+ if (activity !== undefined)
113
+ activities.begin(key(agent), activity);
114
+ },
115
+ promoted: (agent, reservation) => {
116
+ const patch = reservationPromotion(reservation);
117
+ if (patch !== undefined)
118
+ activities.update(key(agent), reservation.id, patch);
119
+ },
120
+ settled: (agent, reservation, outcome) => {
121
+ const settled = reservationSettled(reservation, outcome);
122
+ activities.finish(key(agent), reservation.id, settled?.outcome, now());
123
+ },
124
+ };
125
+ }
126
+ /**
127
+ * Per-session activity table.
128
+ *
129
+ * One active cycle per session, because the router owns one in-flight reservation per agent at a
130
+ * time; a settled cycle replaces the previous settled one (the chip is a status line, not a history
131
+ * — the statistics sidecar is the history).
132
+ */
133
+ export class VerifierActivities {
134
+ active = new Map();
135
+ settled = new Map();
136
+ /** Publish one cycle as in flight. */
137
+ begin(sessionId, activity) {
138
+ this.active.set(sessionId, activity);
139
+ }
140
+ /**
141
+ * Move one in-flight cycle to a later phase.
142
+ *
143
+ * Guarded by cycle id: a cycle that already settled (or was replaced by a newer one) must not be
144
+ * moved back into "in flight" by a late async step.
145
+ * @returns whether the exact cycle was the active one.
146
+ */
147
+ update(sessionId, cycleId, patch) {
148
+ const current = this.active.get(sessionId);
149
+ if (current === undefined || current.cycleId !== cycleId)
150
+ return false;
151
+ this.active.set(sessionId, { ...current, ...patch });
152
+ return true;
153
+ }
154
+ /**
155
+ * Retire one cycle.
156
+ *
157
+ * A cycle nobody observed (the purchase itself failed, so `begin` never ran) leaves nothing
158
+ * behind: the chip must not claim a cycle that never started. A cycle that WAS observed stays
159
+ * correctable while its own settled record is the current one, which is what P06's late
160
+ * "the winner was not delivered" correction needs.
161
+ * @param sessionId - the session the cycle belongs to.
162
+ * @param cycleId - the cycle to retire.
163
+ * @param outcome - the settled row to keep, or undefined to clear without leaving one.
164
+ * @param now - wall clock used for the settled record.
165
+ * @returns whether the cycle was known and retired.
166
+ */
167
+ finish(sessionId, cycleId, outcome, now) {
168
+ const active = this.active.get(sessionId);
169
+ const settled = this.settled.get(sessionId);
170
+ const known = active?.cycleId === cycleId
171
+ ? active
172
+ : settled?.cycleId === cycleId
173
+ ? settled
174
+ : undefined;
175
+ if (known === undefined)
176
+ return false;
177
+ if (active?.cycleId === cycleId)
178
+ this.active.delete(sessionId);
179
+ if (outcome === undefined) {
180
+ if (settled?.cycleId === cycleId)
181
+ this.settled.delete(sessionId);
182
+ return true;
183
+ }
184
+ this.settled.set(sessionId, {
185
+ cycleId,
186
+ stage: known.stage,
187
+ outcome,
188
+ ...(known.candidates === undefined ? {} : { candidates: known.candidates }),
189
+ at: now,
190
+ });
191
+ return true;
192
+ }
193
+ /**
194
+ * What one session has to show right now, with expired records pruned in place.
195
+ * @param sessionId - the session to read.
196
+ * @param now - wall clock used for both TTLs.
197
+ */
198
+ read(sessionId, now) {
199
+ const active = this.active.get(sessionId);
200
+ if (active !== undefined && now - active.startedAt >= ACTIVITY_ACTIVE_TTL_MS)
201
+ this.active.delete(sessionId);
202
+ const settled = this.settled.get(sessionId);
203
+ if (settled !== undefined && now - settled.at >= ACTIVITY_SETTLED_TTL_MS)
204
+ this.settled.delete(sessionId);
205
+ const currentActive = this.active.get(sessionId);
206
+ const currentSettled = this.settled.get(sessionId);
207
+ return {
208
+ ...(currentActive === undefined ? {} : { active: currentActive }),
209
+ ...(currentSettled === undefined ? {} : { settled: currentSettled }),
210
+ };
211
+ }
212
+ /** Forget one session entirely (settings changed, task replaced, agent disposed). */
213
+ clear(sessionId) {
214
+ this.active.delete(sessionId);
215
+ this.settled.delete(sessionId);
216
+ }
217
+ /** Forget every session (settings change, shutdown). */
218
+ clearAll() {
219
+ this.active.clear();
220
+ this.settled.clear();
221
+ }
222
+ }
223
+ //# sourceMappingURL=verifier-activity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verifier-activity.js","sourceRoot":"","sources":["../../src/verifier-activity.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-llm-verifier",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "Configurable DSH-native LLM verifier with a Web settings page",
5
5
  "repository": {
6
6
  "type": "git",
@@ -228,6 +228,7 @@ export const zh = {
228
228
  'recent.decisionPrompt': '提示词',
229
229
  'recent.decisionOutput': '原始回答',
230
230
  'recent.details': '查看详情',
231
+ 'recent.detailsHide': '收起详情',
231
232
  'recent.detail.criterion': '判据',
232
233
  'recent.detail.score': '分数',
233
234
  'recent.detail.threshold': '阈值',
@@ -256,6 +257,15 @@ export const zh = {
256
257
  'models.tokens': '{tokens} Token',
257
258
  'models.empty': '暂无模型调用',
258
259
 
260
+ // Chat chip for the automatic stages that give no other sign of life
261
+ 'activity.route.classifying': '正在识别需要独立复核的对象…',
262
+ 'activity.route.compare': '正在比较候选方案(约 {n} 次调用)…',
263
+ 'activity.route.select': '正在为多个候选排名(约 {n} 次调用)…',
264
+ 'activity.route.track': '正在复核任务进度(约 {n} 次调用)…',
265
+ 'activity.final.accepting': '正在最终验收(约 {n} 次调用)…',
266
+ 'activity.final.accepted': '最终验收通过',
267
+ 'activity.final.rejected': '最终验收未通过',
268
+
259
269
  // Process selection (P06) chat chip: the reply is being buffered, so this is the only sign of life
260
270
  'process.generating': '正在过程选优:生成 {n} 份候选…',
261
271
  'process.comparing': '正在过程选优:裁判比较 {n} 份候选…',
@@ -563,6 +573,7 @@ export const en: I18nDict = {
563
573
  'recent.decisionPrompt': 'Prompt',
564
574
  'recent.decisionOutput': 'Raw answer',
565
575
  'recent.details': 'Details',
576
+ 'recent.detailsHide': 'Hide details',
566
577
  'recent.detail.criterion': 'Criterion',
567
578
  'recent.detail.score': 'Score',
568
579
  'recent.detail.threshold': 'Threshold',
@@ -591,6 +602,15 @@ export const en: I18nDict = {
591
602
  'models.tokens': '{tokens} Tokens',
592
603
  'models.empty': 'No model calls',
593
604
 
605
+ // Chat chip for the automatic stages that give no other sign of life
606
+ 'activity.route.classifying': 'Identifying what needs independent review…',
607
+ 'activity.route.compare': 'Comparing the candidate replies (about {n} calls)…',
608
+ 'activity.route.select': 'Ranking the candidate set (about {n} calls)…',
609
+ 'activity.route.track': 'Reviewing task progress (about {n} calls)…',
610
+ 'activity.final.accepting': 'Running the final acceptance (about {n} calls)…',
611
+ 'activity.final.accepted': 'Final acceptance passed',
612
+ 'activity.final.rejected': 'Final acceptance did not pass',
613
+
594
614
  // Process selection (P06) chat chip: the reply is being buffered, so this is the only sign of life
595
615
  'process.generating': 'Selecting the best reply: generating {n} candidates…',
596
616
  'process.comparing': 'Selecting the best reply: judging {n} candidates…',
@@ -694,40 +714,59 @@ export function tFormat(template: string, params?: Record<string, string | numbe
694
714
  return template.replace(/\{(\w+)\}/g, (match, key) => (key in params ? String(params[key]) : match))
695
715
  }
696
716
 
697
- /** The process-selection chip's wire shape (a subset of the server's `ProcessActivityView`). */
698
- export interface ProcessActivityChipView {
699
- active?: { phase?: unknown; candidates?: unknown; alternativeModel?: unknown }
700
- settled?: { outcome?: unknown; candidates?: unknown; at?: unknown }
717
+ /** The chat chip's wire shape (a subset of the server's `ActivityView`). */
718
+ export interface ActivityChipView {
719
+ active?: { stage?: unknown; phase?: unknown; destination?: unknown; candidates?: unknown; expectedCalls?: unknown }
720
+ settled?: { stage?: unknown; outcome?: unknown }
701
721
  }
702
722
 
703
723
  /** How the chip renders: a busy accent, a settled confirmation, or a failure note. */
704
- export type ProcessActivityTone = 'busy' | 'ok' | 'error'
724
+ export type ActivityTone = 'busy' | 'ok' | 'error'
725
+
726
+ /** A count that is safe to print, falling back to the given minimum for a missing/garbled value. */
727
+ function activityCount(value: unknown, minimum: number): number {
728
+ return typeof value === 'number' && Number.isFinite(value) ? Math.max(minimum, Math.floor(value)) : minimum
729
+ }
705
730
 
706
731
  /**
707
- * Render the process-selection chip, or nothing when there is nothing to say.
732
+ * Render the chat chip, or nothing when there is nothing to say.
708
733
  *
709
734
  * Pure on purpose (no React, no locale detection): both the chip and its regression tests read the
710
- * same mapping. An unknown phase counts as generating — a newer host describing a phase this build
711
- * does not know must still show that work is happening, not disappear.
735
+ * same mapping. Unknown enum values degrade to the least surprising copy — a newer host describing a
736
+ * phase or a stage this build does not know must still show that work is happening, not disappear.
712
737
  * @param view - the server's activity view, or nothing.
713
738
  * @param t - the active dictionary.
714
739
  * @returns The tone and copy, or null to render no row at all.
715
740
  */
716
- export function processActivityText(
717
- view: ProcessActivityChipView | null | undefined,
741
+ export function verifierActivityText(
742
+ view: ActivityChipView | null | undefined,
718
743
  t: I18nDict,
719
- ): { tone: ProcessActivityTone; text: string } | null {
744
+ ): { tone: ActivityTone; text: string } | null {
720
745
  const active = view?.active
721
746
  if (active !== undefined && active !== null) {
722
- const candidates = typeof active.candidates === 'number' && Number.isFinite(active.candidates)
723
- ? Math.max(2, Math.floor(active.candidates))
724
- : 2
747
+ if (active.stage === 'route') {
748
+ if (active.phase !== 'reviewing') return { tone: 'busy', text: t['activity.route.classifying'] }
749
+ const calls = activityCount(active.expectedCalls, 1)
750
+ if (active.destination === 'select') return { tone: 'busy', text: tFormat(t['activity.route.select'], { n: calls }) }
751
+ if (active.destination === 'track') return { tone: 'busy', text: tFormat(t['activity.route.track'], { n: calls }) }
752
+ return { tone: 'busy', text: tFormat(t['activity.route.compare'], { n: calls }) }
753
+ }
754
+ if (active.stage === 'final') {
755
+ return { tone: 'busy', text: tFormat(t['activity.final.accepting'], { n: activityCount(active.expectedCalls, 1) }) }
756
+ }
757
+ // Process selection: the reply is buffered, so this is the only sign of life it has.
758
+ const candidates = activityCount(active.candidates, 2)
725
759
  return active.phase === 'comparing'
726
760
  ? { tone: 'busy', text: tFormat(t['process.comparing'], { n: candidates }) }
727
761
  : { tone: 'busy', text: tFormat(t['process.generating'], { n: candidates }) }
728
762
  }
729
- const outcome = view?.settled?.outcome
730
- switch (outcome) {
763
+ const settled = view?.settled
764
+ if (settled?.stage === 'final') {
765
+ if (settled.outcome === 'accepted') return { tone: 'ok', text: t['activity.final.accepted'] }
766
+ if (settled.outcome === 'rejected') return { tone: 'error', text: t['activity.final.rejected'] }
767
+ return null
768
+ }
769
+ switch (settled?.outcome) {
731
770
  case 'replaced': return { tone: 'ok', text: t['process.replaced'] }
732
771
  case 'kept': return { tone: 'ok', text: t['process.kept'] }
733
772
  case 'same': return { tone: 'ok', text: t['process.same'] }