@alecsibilia/luca 14.0.0-alpha.21 → 14.0.0-alpha.22
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 +4 -0
- package/board/package.json +1 -1
- package/board/server/luca-version.ts +1 -1
- package/engine/agents/agent-launcher.ts +7 -2
- package/engine/agents/claude-launcher-helpers.ts +6 -1
- package/engine/agents/claude-launcher.ts +3 -12
- package/engine/agents/claude-options.ts +3 -6
- package/engine/agents/launch-session.ts +5 -9
- package/engine/agents/role-effort.ts +55 -0
- package/engine/agents/scripted-launcher.ts +17 -3
- package/engine/config/config-fields.ts +3 -2
- package/engine/config/engine-config-schema.ts +42 -4
- package/engine/config/engine-config.ts +2 -0
- package/engine/core/agent-turn.ts +34 -11
- package/engine/core/execute-final-review.ts +11 -2
- package/engine/core/execute-memory.ts +7 -0
- package/engine/guards/helper-rules.ts +6 -0
- package/engine/index.ts +1 -1
- package/engine/jev/jev-jobs.ts +57 -10
- package/engine/jev/jev-schemas.ts +3 -1
- package/engine/journal/agent-entries.ts +5 -1
- package/engine/models/model-score-facts.ts +273 -0
- package/engine/models/model-score-rules.ts +35 -0
- package/engine/models/model-score.ts +169 -246
- package/engine/testing/effort-pick-fixtures.ts +139 -0
- package/engine/testing/model-score-fixtures.ts +300 -0
- package/package.json +1 -1
|
@@ -1,35 +1,42 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
factsOf,
|
|
3
|
+
sum,
|
|
4
|
+
type RunFacts,
|
|
5
|
+
type TicketFacts,
|
|
6
|
+
} from './model-score-facts'
|
|
7
|
+
import {
|
|
8
|
+
EPSILON,
|
|
9
|
+
FINDING_WEIGHTS,
|
|
10
|
+
MIN_TICKETS,
|
|
11
|
+
SCORED_ROLES,
|
|
12
|
+
WORSE_BY,
|
|
13
|
+
type ScoredRole,
|
|
14
|
+
} from './model-score-rules'
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
EffortLevelSchema,
|
|
18
|
+
ModelNameSchema,
|
|
19
|
+
type EffortLevel,
|
|
20
|
+
type ModelName,
|
|
21
|
+
} from '../config/engine-config'
|
|
3
22
|
import type { JournalRecord } from '../journal/journal-record'
|
|
4
|
-
|
|
23
|
+
|
|
24
|
+
export {
|
|
25
|
+
FINDING_WEIGHTS,
|
|
26
|
+
MIN_TICKETS,
|
|
27
|
+
SCORED_ROLES,
|
|
28
|
+
WORSE_BY,
|
|
29
|
+
type ScoredRole,
|
|
30
|
+
} from './model-score-rules'
|
|
5
31
|
|
|
6
32
|
/**
|
|
7
33
|
* The model score report (#605): how good each routed role's work was on
|
|
8
|
-
* each model it ran
|
|
9
|
-
* a model floor or raise one back.
|
|
34
|
+
* each model and effort it ran at (#662), from run journals, so the owner
|
|
35
|
+
* knows when to lower a model floor or raise one back. A ticket is compared
|
|
36
|
+
* only with tickets whose ticket review ran at the same effort. Pure: it
|
|
37
|
+
* reads records and never writes.
|
|
10
38
|
*/
|
|
11
39
|
|
|
12
|
-
/** How much each ticket review finding weighs. */
|
|
13
|
-
export const FINDING_WEIGHTS: Record<FindingSeverity, number> = {
|
|
14
|
-
blocker: 3,
|
|
15
|
-
should_fix: 1,
|
|
16
|
-
nit: 0,
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** The fewest tickets on each side before a verdict is given. */
|
|
20
|
-
export const MIN_TICKETS = 10
|
|
21
|
-
|
|
22
|
-
/** How far above the baseline a rate may be before it is "worse". */
|
|
23
|
-
export const WORSE_BY = 0.2
|
|
24
|
-
|
|
25
|
-
/** Rounding slack when a rate is compared with the baseline's. */
|
|
26
|
-
const EPSILON = 1e-9
|
|
27
|
-
|
|
28
|
-
/** The roles scored by the ticket review's findings. */
|
|
29
|
-
export const SCORED_ROLES = ['test-writer', 'implementer'] as const
|
|
30
|
-
|
|
31
|
-
export type ScoredRole = (typeof SCORED_ROLES)[number]
|
|
32
|
-
|
|
33
40
|
export type Verdict = 'not enough data' | 'worse' | 'about the same'
|
|
34
41
|
|
|
35
42
|
/** The rates a model is judged by, for its tickets or its baseline's. */
|
|
@@ -41,10 +48,14 @@ export type ScoreSide = {
|
|
|
41
48
|
fix_rounds_per_ticket: number | null
|
|
42
49
|
}
|
|
43
50
|
|
|
44
|
-
/** One routed role on one model. */
|
|
51
|
+
/** One routed role on one model at one effort, its tickets reviewed at one effort. */
|
|
45
52
|
export type ModelScore = ScoreSide & {
|
|
46
53
|
role: ScoredRole
|
|
47
54
|
model: ModelName
|
|
55
|
+
/** The effort the role's first agent on each ticket ran at. */
|
|
56
|
+
effort: EffortLevel
|
|
57
|
+
/** The effort each ticket's first ticket review ran at. */
|
|
58
|
+
review_effort: EffortLevel
|
|
48
59
|
stuck_tickets: number
|
|
49
60
|
/** `null` when no finished ticket has its usage journaled. */
|
|
50
61
|
tokens_per_finished_ticket: number | null
|
|
@@ -54,13 +65,32 @@ export type ModelScore = ScoreSide & {
|
|
|
54
65
|
other_role_cheaper: number
|
|
55
66
|
/**
|
|
56
67
|
* The tickets where Clef picked this model in shadow mode but the role
|
|
57
|
-
* ran Opus
|
|
68
|
+
* ran Opus, at the same effort and reviewed at the same effort. Empty
|
|
69
|
+
* for Opus.
|
|
58
70
|
*/
|
|
59
71
|
baseline: ScoreSide
|
|
60
72
|
/** `null` for Opus, which is the baseline the others are judged by. */
|
|
61
73
|
verdict: Verdict | null
|
|
62
74
|
}
|
|
63
75
|
|
|
76
|
+
export type EffortPicksVerdict =
|
|
77
|
+
| 'not enough data'
|
|
78
|
+
| 'Clef spots hard tickets'
|
|
79
|
+
| 'no sign yet'
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Clef's shadow-mode effort picks for one writer role (#663): the tickets
|
|
83
|
+
* where its `agent_effort` answer, at or above the run's confidence bar,
|
|
84
|
+
* was higher than the effort the role's first agent ran at, against the
|
|
85
|
+
* tickets where it was the same or lower.
|
|
86
|
+
*/
|
|
87
|
+
export type EffortPicks = {
|
|
88
|
+
role: ScoredRole
|
|
89
|
+
wanted_more: ScoreSide
|
|
90
|
+
rest: ScoreSide
|
|
91
|
+
verdict: EffortPicksVerdict
|
|
92
|
+
}
|
|
93
|
+
|
|
64
94
|
/** The final review's findings over a set of runs. */
|
|
65
95
|
export type FinalReviewSide = {
|
|
66
96
|
runs: number
|
|
@@ -70,69 +100,16 @@ export type FinalReviewSide = {
|
|
|
70
100
|
export type ModelReport = {
|
|
71
101
|
runs: number
|
|
72
102
|
scores: ModelScore[]
|
|
73
|
-
/**
|
|
74
|
-
|
|
103
|
+
/** One per scored role. */
|
|
104
|
+
effort_picks: EffortPicks[]
|
|
105
|
+
/** Fresh learner agents per model and effort; the learner isn't scored. */
|
|
106
|
+
learner: { model: ModelName; effort: EffortLevel; agents: number }[]
|
|
75
107
|
/** Runs with any agent on a cheaper model, and runs without. */
|
|
76
108
|
final_review: { routed: FinalReviewSide; not_routed: FinalReviewSide }
|
|
77
109
|
/** Joined tickets with no `lines_changed`, left out of the rates. */
|
|
78
110
|
without_lines_changed: number
|
|
79
111
|
}
|
|
80
112
|
|
|
81
|
-
/** Clef's first model pick for a role on a ticket. */
|
|
82
|
-
type Pick = {
|
|
83
|
-
model: ModelName | null
|
|
84
|
-
confidence: number | null
|
|
85
|
-
/** Asked in shadow mode: `acted: false`, or an older `ticket_model` ask. */
|
|
86
|
-
shadow: boolean
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** What one run's journal says about one ticket. */
|
|
90
|
-
type TicketFacts = {
|
|
91
|
-
/** The model of each role's first agent. */
|
|
92
|
-
models: Partial<Record<ScoredRole, ModelName>>
|
|
93
|
-
picks: Partial<Record<ScoredRole, Pick>>
|
|
94
|
-
reviews: number
|
|
95
|
-
/** The first ticket review's weighted findings. */
|
|
96
|
-
findings: number
|
|
97
|
-
fix_rounds: number
|
|
98
|
-
joined: boolean
|
|
99
|
-
lines: number | null
|
|
100
|
-
stuck: boolean
|
|
101
|
-
tokens: number | null
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/** What one run's journal says. */
|
|
105
|
-
type RunFacts = {
|
|
106
|
-
tickets: TicketFacts[]
|
|
107
|
-
routed: boolean
|
|
108
|
-
learner: ModelName[]
|
|
109
|
-
/** The first final review round's weighted findings, or `null` with none. */
|
|
110
|
-
final_findings: number | null
|
|
111
|
-
/** The joined tickets' lines, or `null` when one has none. */
|
|
112
|
-
lines: number | null
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* The model family a short name or full model id (as older `ticket_model`
|
|
117
|
-
* asks named them) belongs to, or `null` for none.
|
|
118
|
-
*
|
|
119
|
-
* @example
|
|
120
|
-
* familyOf('sonnet') // 'sonnet'
|
|
121
|
-
*/
|
|
122
|
-
const familyOf = (name: unknown): ModelName | null => {
|
|
123
|
-
if (typeof name !== 'string') return null
|
|
124
|
-
const lower = name.toLowerCase()
|
|
125
|
-
return (
|
|
126
|
-
ModelNameSchema.options.find((model) => lower.includes(model)) ?? null
|
|
127
|
-
)
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const isScored = (role: string | null): role is ScoredRole =>
|
|
131
|
-
SCORED_ROLES.some((scored) => scored === role)
|
|
132
|
-
|
|
133
|
-
const sum = (values: number[]): number =>
|
|
134
|
-
values.reduce((total, value) => total + value, 0)
|
|
135
|
-
|
|
136
113
|
const median = (values: number[]): number | null => {
|
|
137
114
|
if (values.length === 0) return null
|
|
138
115
|
const sorted = values.toSorted((a, b) => a - b)
|
|
@@ -142,144 +119,6 @@ const median = (values: number[]): number | null => {
|
|
|
142
119
|
: ((sorted[middle - 1] ?? 0) + (sorted[middle] ?? 0)) / 2
|
|
143
120
|
}
|
|
144
121
|
|
|
145
|
-
const weigh = (counts: Record<FindingSeverity, number>): number =>
|
|
146
|
-
counts.blocker * FINDING_WEIGHTS.blocker +
|
|
147
|
-
counts.should_fix * FINDING_WEIGHTS.should_fix +
|
|
148
|
-
counts.nit * FINDING_WEIGHTS.nit
|
|
149
|
-
|
|
150
|
-
const newTicket = (): TicketFacts => ({
|
|
151
|
-
models: {},
|
|
152
|
-
picks: {},
|
|
153
|
-
reviews: 0,
|
|
154
|
-
findings: 0,
|
|
155
|
-
fix_rounds: 0,
|
|
156
|
-
joined: false,
|
|
157
|
-
lines: null,
|
|
158
|
-
stuck: false,
|
|
159
|
-
tokens: null,
|
|
160
|
-
})
|
|
161
|
-
|
|
162
|
-
/** What one run's records say, ticket by ticket. */
|
|
163
|
-
const factsOf = (records: JournalRecord[]): RunFacts => {
|
|
164
|
-
const tickets = new Map<number, TicketFacts>()
|
|
165
|
-
const ticketOf = (number: number): TicketFacts => {
|
|
166
|
-
const found = tickets.get(number) ?? newTicket()
|
|
167
|
-
tickets.set(number, found)
|
|
168
|
-
return found
|
|
169
|
-
}
|
|
170
|
-
const asks = new Map<number, { role: string | null; shadow: boolean }>()
|
|
171
|
-
const learner: ModelName[] = []
|
|
172
|
-
let routed = false
|
|
173
|
-
let final_rounds = 0
|
|
174
|
-
let final_findings = 0
|
|
175
|
-
for (const record of records) {
|
|
176
|
-
switch (record.kind) {
|
|
177
|
-
case 'jev_asked':
|
|
178
|
-
if (
|
|
179
|
-
record.content.job === 'agent_model' ||
|
|
180
|
-
record.content.job === 'ticket_model'
|
|
181
|
-
) {
|
|
182
|
-
asks.set(record.seq, {
|
|
183
|
-
role: record.role,
|
|
184
|
-
shadow:
|
|
185
|
-
record.content.job === 'ticket_model' ||
|
|
186
|
-
!record.content.acted,
|
|
187
|
-
})
|
|
188
|
-
}
|
|
189
|
-
break
|
|
190
|
-
case 'jev_answered': {
|
|
191
|
-
const ask = asks.get(record.content.asked_seq)
|
|
192
|
-
if (ask === undefined || record.ticket === null) break
|
|
193
|
-
const answer = record.content.answers.model
|
|
194
|
-
const pick: Pick = {
|
|
195
|
-
model: familyOf(answer?.value),
|
|
196
|
-
confidence: answer?.confidence ?? null,
|
|
197
|
-
shadow: ask.shadow,
|
|
198
|
-
}
|
|
199
|
-
const roles =
|
|
200
|
-
record.content.job === 'ticket_model'
|
|
201
|
-
? SCORED_ROLES
|
|
202
|
-
: SCORED_ROLES.filter((role) => role === ask.role)
|
|
203
|
-
const facts = ticketOf(record.ticket)
|
|
204
|
-
for (const role of roles) facts.picks[role] ??= pick
|
|
205
|
-
break
|
|
206
|
-
}
|
|
207
|
-
case 'agent_started': {
|
|
208
|
-
const { role, model, follow_up_of } = record.content
|
|
209
|
-
const family = familyOf(model) ?? 'opus'
|
|
210
|
-
if (follow_up_of !== null) break
|
|
211
|
-
if (role === 'learner') learner.push(family)
|
|
212
|
-
if (isScored(role) || role === 'learner') {
|
|
213
|
-
routed ||= family !== 'opus'
|
|
214
|
-
}
|
|
215
|
-
if (isScored(role) && record.ticket !== null) {
|
|
216
|
-
ticketOf(record.ticket).models[role] ??= family
|
|
217
|
-
}
|
|
218
|
-
break
|
|
219
|
-
}
|
|
220
|
-
case 'agent_finished': {
|
|
221
|
-
const { content } = record
|
|
222
|
-
if (
|
|
223
|
-
content.role !== 'ticket-reviewer' ||
|
|
224
|
-
record.ticket === null
|
|
225
|
-
) {
|
|
226
|
-
break
|
|
227
|
-
}
|
|
228
|
-
const facts = ticketOf(record.ticket)
|
|
229
|
-
if (facts.reviews === 0) {
|
|
230
|
-
facts.findings = sum(
|
|
231
|
-
content.result.findings.map(
|
|
232
|
-
({ severity }) => FINDING_WEIGHTS[severity]
|
|
233
|
-
)
|
|
234
|
-
)
|
|
235
|
-
}
|
|
236
|
-
facts.reviews += 1
|
|
237
|
-
if (content.result.verdict === 'changes_requested') {
|
|
238
|
-
facts.fix_rounds += 1
|
|
239
|
-
}
|
|
240
|
-
break
|
|
241
|
-
}
|
|
242
|
-
case 'ticket_joined':
|
|
243
|
-
if (record.ticket !== null && record.content.ok) {
|
|
244
|
-
const facts = ticketOf(record.ticket)
|
|
245
|
-
facts.joined = true
|
|
246
|
-
facts.lines = record.content.lines_changed
|
|
247
|
-
}
|
|
248
|
-
break
|
|
249
|
-
case 'ticket_stuck':
|
|
250
|
-
if (record.ticket !== null) ticketOf(record.ticket).stuck = true
|
|
251
|
-
break
|
|
252
|
-
case 'usage_recorded': {
|
|
253
|
-
const { scope, ticket, tokens } = record.content
|
|
254
|
-
if (scope === 'ticket' && ticket !== null) {
|
|
255
|
-
ticketOf(ticket).tokens = countedTokens({ tokens })
|
|
256
|
-
}
|
|
257
|
-
break
|
|
258
|
-
}
|
|
259
|
-
case 'final_review_started':
|
|
260
|
-
final_rounds += 1
|
|
261
|
-
break
|
|
262
|
-
case 'lens_finished':
|
|
263
|
-
if (final_rounds === 1) {
|
|
264
|
-
final_findings += weigh(record.content.findings)
|
|
265
|
-
}
|
|
266
|
-
break
|
|
267
|
-
default:
|
|
268
|
-
break
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
const joined = [...tickets.values()].filter(({ joined }) => joined)
|
|
272
|
-
return {
|
|
273
|
-
tickets: [...tickets.values()],
|
|
274
|
-
routed,
|
|
275
|
-
learner,
|
|
276
|
-
final_findings: final_rounds > 0 ? final_findings : null,
|
|
277
|
-
lines: joined.some(({ lines }) => lines === null)
|
|
278
|
-
? null
|
|
279
|
-
: sum(joined.map(({ lines }) => lines ?? 0)),
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
122
|
/** Findings per 100 lines over `tickets`, those with no lines left out. */
|
|
284
123
|
const findingsRate = (tickets: TicketFacts[]): number | null => {
|
|
285
124
|
const rated = tickets.filter(({ lines }) => lines !== null)
|
|
@@ -302,6 +141,15 @@ const sideOf = (tickets: TicketFacts[]): ScoreSide => ({
|
|
|
302
141
|
const above = (value: number | null, base: number | null): boolean =>
|
|
303
142
|
value !== null && base !== null && value > base * (1 + WORSE_BY) + EPSILON
|
|
304
143
|
|
|
144
|
+
/** Whether either side has fewer than `MIN_TICKETS` tickets. */
|
|
145
|
+
const tooFew = (a: ScoreSide, b: ScoreSide): boolean =>
|
|
146
|
+
a.tickets < MIN_TICKETS || b.tickets < MIN_TICKETS
|
|
147
|
+
|
|
148
|
+
/** Whether `side`'s findings rate or fix rounds per ticket is above `base`'s. */
|
|
149
|
+
const sideAbove = (side: ScoreSide, base: ScoreSide): boolean =>
|
|
150
|
+
above(side.findings_per_100_lines, base.findings_per_100_lines) ||
|
|
151
|
+
above(side.fix_rounds_per_ticket, base.fix_rounds_per_ticket)
|
|
152
|
+
|
|
305
153
|
/**
|
|
306
154
|
* The verdict on a cheaper model against its baseline: "not enough data"
|
|
307
155
|
* while either side has fewer than `MIN_TICKETS` tickets, "worse" when its
|
|
@@ -315,34 +163,81 @@ export const verdictOf = ({
|
|
|
315
163
|
side: ScoreSide
|
|
316
164
|
baseline: ScoreSide
|
|
317
165
|
}): Verdict => {
|
|
318
|
-
if (side
|
|
319
|
-
|
|
166
|
+
if (tooFew(side, baseline)) return 'not enough data'
|
|
167
|
+
return sideAbove(side, baseline) ? 'worse' : 'about the same'
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Clef's effort picks for `role` over every run's tickets: a ticket wants
|
|
172
|
+
* more when its pick is above the effort the role's first agent ran at, in
|
|
173
|
+
* `EffortLevelSchema`'s order. "Clef spots hard tickets" when the "wanted
|
|
174
|
+
* more" side's findings rate or fix rounds per ticket is more than
|
|
175
|
+
* `WORSE_BY` above the rest's, with `MIN_TICKETS` on each side.
|
|
176
|
+
*/
|
|
177
|
+
const effortPicksOf = ({
|
|
178
|
+
tickets,
|
|
179
|
+
role,
|
|
180
|
+
}: {
|
|
181
|
+
tickets: TicketFacts[]
|
|
182
|
+
role: ScoredRole
|
|
183
|
+
}): EffortPicks => {
|
|
184
|
+
const rank = (level: EffortLevel): number =>
|
|
185
|
+
EffortLevelSchema.options.indexOf(level)
|
|
186
|
+
const picked = tickets.flatMap((ticket) => {
|
|
187
|
+
const pick = ticket.effort_picks[role]
|
|
188
|
+
const ran = ticket.efforts[role]
|
|
189
|
+
return pick === undefined || ran === undefined
|
|
190
|
+
? []
|
|
191
|
+
: [{ ticket, wants_more: rank(pick) > rank(ran) }]
|
|
192
|
+
})
|
|
193
|
+
const sideWhere = (wants_more: boolean): ScoreSide =>
|
|
194
|
+
sideOf(
|
|
195
|
+
picked.flatMap((entry) =>
|
|
196
|
+
entry.wants_more === wants_more ? [entry.ticket] : []
|
|
197
|
+
)
|
|
198
|
+
)
|
|
199
|
+
const wanted_more = sideWhere(true)
|
|
200
|
+
const rest = sideWhere(false)
|
|
201
|
+
const verdictOn = (): EffortPicksVerdict => {
|
|
202
|
+
if (tooFew(wanted_more, rest)) return 'not enough data'
|
|
203
|
+
return sideAbove(wanted_more, rest)
|
|
204
|
+
? 'Clef spots hard tickets'
|
|
205
|
+
: 'no sign yet'
|
|
320
206
|
}
|
|
321
|
-
|
|
322
|
-
above(side.findings_per_100_lines, baseline.findings_per_100_lines) ||
|
|
323
|
-
above(side.fix_rounds_per_ticket, baseline.fix_rounds_per_ticket)
|
|
324
|
-
return worse ? 'worse' : 'about the same'
|
|
207
|
+
return { role, wanted_more, rest, verdict: verdictOn() }
|
|
325
208
|
}
|
|
326
209
|
|
|
327
|
-
/**
|
|
210
|
+
/**
|
|
211
|
+
* One routed role on one model at one effort, over every run's tickets
|
|
212
|
+
* whose first ticket review ran at `review_effort`.
|
|
213
|
+
*/
|
|
328
214
|
const scoreOf = ({
|
|
329
215
|
tickets,
|
|
330
216
|
role,
|
|
331
217
|
model,
|
|
218
|
+
effort,
|
|
219
|
+
review_effort,
|
|
332
220
|
}: {
|
|
333
221
|
tickets: TicketFacts[]
|
|
334
222
|
role: ScoredRole
|
|
335
223
|
model: ModelName
|
|
224
|
+
effort: EffortLevel
|
|
225
|
+
review_effort: EffortLevel
|
|
336
226
|
}): ModelScore => {
|
|
337
227
|
const other = SCORED_ROLES.find((scored) => scored !== role) ?? role
|
|
338
|
-
const
|
|
228
|
+
const alike = tickets.filter(
|
|
229
|
+
(ticket) =>
|
|
230
|
+
ticket.efforts[role] === effort &&
|
|
231
|
+
ticket.review_effort === review_effort
|
|
232
|
+
)
|
|
233
|
+
const group = alike.filter((ticket) => ticket.models[role] === model)
|
|
339
234
|
const finished = group.flatMap(({ joined, tokens }) =>
|
|
340
235
|
joined && tokens !== null ? [tokens] : []
|
|
341
236
|
)
|
|
342
237
|
const baseline =
|
|
343
238
|
model === 'opus'
|
|
344
239
|
? []
|
|
345
|
-
:
|
|
240
|
+
: alike.filter(
|
|
346
241
|
({ models, picks }) =>
|
|
347
242
|
models[role] === 'opus' &&
|
|
348
243
|
picks[role]?.shadow === true &&
|
|
@@ -353,6 +248,8 @@ const scoreOf = ({
|
|
|
353
248
|
return {
|
|
354
249
|
role,
|
|
355
250
|
model,
|
|
251
|
+
effort,
|
|
252
|
+
review_effort,
|
|
356
253
|
...side,
|
|
357
254
|
stuck_tickets: group.filter(({ stuck }) => stuck).length,
|
|
358
255
|
tokens_per_finished_ticket:
|
|
@@ -395,8 +292,10 @@ const finalReviewOf = (runs: RunFacts[]): FinalReviewSide => {
|
|
|
395
292
|
|
|
396
293
|
/**
|
|
397
294
|
* The model score report from run journals, each run's records oldest
|
|
398
|
-
* first. A ticket counts under the model of each role's first
|
|
399
|
-
* (an agent from before #600, with no model, ran Opus
|
|
295
|
+
* first. A ticket counts under the model and effort of each role's first
|
|
296
|
+
* agent on it (an agent from before #600, with no model, ran Opus; one from
|
|
297
|
+
* before #659, with no effort, ran at high), and under its first ticket
|
|
298
|
+
* review's effort. Pure.
|
|
400
299
|
*
|
|
401
300
|
* @example
|
|
402
301
|
* const report = scoreModels({ runs: [createJournal({ file }).read()] })
|
|
@@ -411,20 +310,33 @@ export const scoreModels = ({
|
|
|
411
310
|
const tickets = facts
|
|
412
311
|
.flatMap((run) => run.tickets)
|
|
413
312
|
.filter(({ models }) => Object.keys(models).length > 0)
|
|
313
|
+
const levels = EffortLevelSchema.options
|
|
414
314
|
const scores = SCORED_ROLES.flatMap((role) =>
|
|
415
|
-
ModelNameSchema.options.
|
|
416
|
-
|
|
315
|
+
ModelNameSchema.options.flatMap((model) =>
|
|
316
|
+
levels.flatMap((effort) =>
|
|
317
|
+
levels.map((review_effort) =>
|
|
318
|
+
scoreOf({ tickets, role, model, effort, review_effort })
|
|
319
|
+
)
|
|
320
|
+
)
|
|
417
321
|
)
|
|
418
322
|
).filter(({ tickets, baseline }) => tickets > 0 || baseline.tickets > 0)
|
|
419
323
|
const learner = facts.flatMap((run) => run.learner)
|
|
420
324
|
return {
|
|
421
325
|
runs: runs.length,
|
|
422
326
|
scores,
|
|
327
|
+
effort_picks: SCORED_ROLES.map((role) =>
|
|
328
|
+
effortPicksOf({ tickets, role })
|
|
329
|
+
),
|
|
423
330
|
learner: ModelNameSchema.options
|
|
424
|
-
.
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
331
|
+
.flatMap((model) =>
|
|
332
|
+
levels.map((effort) => ({
|
|
333
|
+
model,
|
|
334
|
+
effort,
|
|
335
|
+
agents: learner.filter(
|
|
336
|
+
(ran) => ran.model === model && ran.effort === effort
|
|
337
|
+
).length,
|
|
338
|
+
}))
|
|
339
|
+
)
|
|
428
340
|
.filter(({ agents }) => agents > 0),
|
|
429
341
|
final_review: {
|
|
430
342
|
routed: finalReviewOf(facts.filter(({ routed }) => routed)),
|
|
@@ -453,29 +365,37 @@ const scoreText = (score: ModelScore): string[] => {
|
|
|
453
365
|
const other = SCORED_ROLES.find((role) => role !== score.role)
|
|
454
366
|
const tokens = score.tokens_per_finished_ticket
|
|
455
367
|
return [
|
|
456
|
-
` ${score.model}: ${[
|
|
368
|
+
` ${score.model} at ${score.effort}: ${[
|
|
457
369
|
sideText(score),
|
|
458
370
|
`${score.stuck_tickets} stuck`,
|
|
459
371
|
`${tokens === null ? '-' : Math.round(tokens)} tokens per finished ticket`,
|
|
460
372
|
`median confidence ${decimal(score.median_confidence)}`,
|
|
461
373
|
`${other} cheaper on ${score.other_role_cheaper}`,
|
|
374
|
+
`reviewed at ${score.review_effort}`,
|
|
462
375
|
].join(', ')}`,
|
|
463
376
|
...(score.verdict === null
|
|
464
377
|
? []
|
|
465
378
|
: [
|
|
466
|
-
` baseline (Clef picked ${score.model} in shadow mode, ran opus): ${sideText(score.baseline)}`,
|
|
379
|
+
` baseline (Clef picked ${score.model} in shadow mode, ran opus at ${score.effort}): ${sideText(score.baseline)}`,
|
|
467
380
|
` verdict: ${score.verdict}`,
|
|
468
381
|
]),
|
|
469
382
|
]
|
|
470
383
|
}
|
|
471
384
|
|
|
385
|
+
const effortPicksText = (picks: EffortPicks): string[] => [
|
|
386
|
+
" Clef's effort picks (shadow mode)",
|
|
387
|
+
` wanted more effort than it ran at: ${sideText(picks.wanted_more)}`,
|
|
388
|
+
` the rest: ${sideText(picks.rest)}`,
|
|
389
|
+
` verdict: ${picks.verdict}`,
|
|
390
|
+
]
|
|
391
|
+
|
|
472
392
|
const finalText = (label: string, side: FinalReviewSide): string =>
|
|
473
393
|
`${decimal(side.findings_per_100_lines)} ${label} (${plural(side.runs, 'run')})`
|
|
474
394
|
|
|
475
395
|
/**
|
|
476
|
-
* The report as `luca models` prints it: each routed role's models
|
|
477
|
-
* their verdicts
|
|
478
|
-
* left out of the rates. Pure.
|
|
396
|
+
* The report as `luca models` prints it: each routed role's models and
|
|
397
|
+
* efforts with their verdicts and Clef's effort picks, the learner, the
|
|
398
|
+
* final review's line, and the tickets left out of the rates. Pure.
|
|
479
399
|
*
|
|
480
400
|
* @example
|
|
481
401
|
* console.log(formatModelReport({ report: scoreModels({ runs }) }))
|
|
@@ -488,20 +408,23 @@ export const formatModelReport = ({
|
|
|
488
408
|
const weights = `blocker ${FINDING_WEIGHTS.blocker}, should-fix ${FINDING_WEIGHTS.should_fix}, nit ${FINDING_WEIGHTS.nit}`
|
|
489
409
|
const roles = SCORED_ROLES.flatMap((role) => {
|
|
490
410
|
const scores = report.scores.filter((score) => score.role === role)
|
|
411
|
+
const picks = report.effort_picks.find((found) => found.role === role)
|
|
491
412
|
return [
|
|
492
413
|
role,
|
|
493
414
|
...(scores.length === 0
|
|
494
415
|
? [' no tickets yet']
|
|
495
416
|
: scores.flatMap(scoreText)),
|
|
417
|
+
...(picks === undefined ? [] : effortPicksText(picks)),
|
|
496
418
|
]
|
|
497
419
|
})
|
|
498
420
|
const learner =
|
|
499
421
|
report.learner.length === 0
|
|
500
422
|
? ''
|
|
501
|
-
: ` (${report.learner.map(({ model, agents }) => `${plural(agents, 'agent')} on ${model}`).join(', ')})`
|
|
423
|
+
: ` (${report.learner.map(({ model, effort, agents }) => `${plural(agents, 'agent')} on ${model} at ${effort}`).join(', ')})`
|
|
502
424
|
const { routed, not_routed } = report.final_review
|
|
503
425
|
return [
|
|
504
426
|
`Model scores from ${plural(report.runs, 'run')}: the first ticket review's findings weighted ${weights}, per 100 changed lines.`,
|
|
427
|
+
'Each line is a model at an effort, compared only with tickets reviewed at the same effort.',
|
|
505
428
|
'',
|
|
506
429
|
...roles,
|
|
507
430
|
`learner: not measured by findings${learner}`,
|