@alecsibilia/luca 14.0.0-alpha.27 → 14.0.0-alpha.28
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/client/model-line.tsx +14 -3
- package/board/package.json +1 -1
- package/board/server/luca-version.ts +1 -1
- package/board/shared/board-state-run.ts +1 -1
- package/engine/config/engine-config-schema.ts +5 -2
- package/engine/core/agent-turn.ts +19 -7
- package/engine/core/execute-final-review.ts +1 -0
- package/engine/core/execute-memory.ts +5 -0
- package/engine/jev/model-pick.ts +115 -17
- package/engine/journal/agent-entries.ts +5 -2
- package/engine/models/model-score-facts.ts +37 -16
- package/engine/models/model-score-sides.ts +80 -0
- package/engine/models/model-score.ts +146 -88
- package/engine/testing/effort-pick-fixtures.ts +7 -8
- package/engine/testing/model-score-fixtures.ts +30 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -36,8 +36,8 @@ const capital = ({ text }: { text: string }): string =>
|
|
|
36
36
|
* doesn't know.
|
|
37
37
|
*
|
|
38
38
|
* @example
|
|
39
|
-
* clefLine({ model: { role: 'implementer', name: 'opus', reason: 'unsure', answer: { role: 'implementer', pick: 'sonnet', confidence: 0.
|
|
40
|
-
* // { short: '0.
|
|
39
|
+
* clefLine({ model: { role: 'implementer', name: 'opus', reason: 'unsure', answer: { role: 'implementer', pick: 'sonnet', confidence: 0.36 } }, bar: 0.5 })
|
|
40
|
+
* // { short: '0.36 split → stayed on opus', full: 'Clef picked sonnet but was only 0.36 sure (split: under 0.5), so Luca stayed on opus.', confidence: 0.36, sharp: false }
|
|
41
41
|
*/
|
|
42
42
|
export const clefLine = ({
|
|
43
43
|
model,
|
|
@@ -54,10 +54,11 @@ export const clefLine = ({
|
|
|
54
54
|
const sure = confidence?.toFixed(2) ?? ''
|
|
55
55
|
const judged = `${word}: ${sharp ? 'at or over' : 'under'} ${bar}`
|
|
56
56
|
const plain = { confidence: null, sharp: false }
|
|
57
|
+
const picked = `${confidence === null ? 'clef' : `${sure} ${word}`} → ${name}`
|
|
57
58
|
switch (reason) {
|
|
58
59
|
case 'decision_model':
|
|
59
60
|
return {
|
|
60
|
-
short:
|
|
61
|
+
short: picked,
|
|
61
62
|
full:
|
|
62
63
|
confidence === null
|
|
63
64
|
? `Clef picked ${name}, so Luca ran the ${role} on it.`
|
|
@@ -65,6 +66,16 @@ export const clefLine = ({
|
|
|
65
66
|
confidence,
|
|
66
67
|
sharp,
|
|
67
68
|
}
|
|
69
|
+
case 'trial':
|
|
70
|
+
return {
|
|
71
|
+
short: `${picked} (trial)`,
|
|
72
|
+
full:
|
|
73
|
+
confidence === null
|
|
74
|
+
? `Clef picked ${name}, and Luca ran the ${role} on it in a model trial.`
|
|
75
|
+
: `Clef picked ${name} and was ${sure} sure (${judged}), and Luca ran the ${role} on it in a model trial.`,
|
|
76
|
+
confidence,
|
|
77
|
+
sharp,
|
|
78
|
+
}
|
|
68
79
|
case 'unsure':
|
|
69
80
|
return {
|
|
70
81
|
short: `${confidence === null ? 'unsure' : `${sure} ${word}`} → stayed on ${name}`,
|
package/board/package.json
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** The Luca version of this board, stamped when the package was packed. */
|
|
2
|
-
export const LUCA_VERSION: string = "14.0.0-alpha.
|
|
2
|
+
export const LUCA_VERSION: string = "14.0.0-alpha.28"
|
|
@@ -94,7 +94,7 @@ export const DEFAULT_RUN_BUDGET_TOKENS = 9_000_000
|
|
|
94
94
|
* `engine/src/config/engine-config-schema.ts`): the decision model is sharp at or
|
|
95
95
|
* over it, split under it. Kept here for the same reason.
|
|
96
96
|
*/
|
|
97
|
-
export const DEFAULT_CONFIDENCE_BAR = 0.
|
|
97
|
+
export const DEFAULT_CONFIDENCE_BAR = 0.5
|
|
98
98
|
|
|
99
99
|
export const EngineEndedSchema = z.object({
|
|
100
100
|
ok: z.boolean(),
|
|
@@ -216,12 +216,15 @@ const RoleEffortsSchema = z.strictObject(
|
|
|
216
216
|
/**
|
|
217
217
|
* The config's `models` block (#603): each routed role's model floor, each
|
|
218
218
|
* writer's effort (#660), and how sure the decision model must be
|
|
219
|
-
* (`confidence_bar`) before its model pick is used
|
|
219
|
+
* (`confidence_bar`) before its model pick is used, and the share of its
|
|
220
|
+
* unsure but cheaper picks that run anyway in a model trial (`trial_share`,
|
|
221
|
+
* #698).
|
|
220
222
|
*/
|
|
221
223
|
export const ModelsConfigSchema = z.object({
|
|
222
224
|
floors: ModelFloorsSchema.prefault({}),
|
|
223
225
|
effort: RoleEffortsSchema.prefault({}),
|
|
224
|
-
confidence_bar: z.number().min(0).max(1).default(0.
|
|
226
|
+
confidence_bar: z.number().min(0).max(1).default(0.5),
|
|
227
|
+
trial_share: z.number().min(0).max(1).default(0),
|
|
225
228
|
})
|
|
226
229
|
|
|
227
230
|
export type ModelsConfig = z.infer<typeof ModelsConfigSchema>
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* ended, journaled. Also the `launch_agent` and `follow_up_agent` steps,
|
|
4
4
|
* which start a turn.
|
|
5
5
|
*/
|
|
6
|
+
import { basename } from 'node:path'
|
|
7
|
+
|
|
6
8
|
import { need, ticketWorktree, type BuildContext } from './build-context'
|
|
7
9
|
import { mayEditTests, type BuildAction } from './decide-build'
|
|
8
10
|
import { doChores } from './engine-chores'
|
|
@@ -19,7 +21,11 @@ import { DEFAULT_MODEL, type EffortLevel } from '../config/engine-config'
|
|
|
19
21
|
import { describeViolations } from '../guards/after-turn-check'
|
|
20
22
|
import { guardRoleOf } from '../guards/role-rules'
|
|
21
23
|
import { enforceAfterTurn, snapshotWorktree } from '../guards/worktree-state'
|
|
22
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
launchPick,
|
|
26
|
+
type ModelReason,
|
|
27
|
+
type TrialGroup,
|
|
28
|
+
} from '../jev/model-pick'
|
|
23
29
|
import type { AgentFailure, JournalRecord } from '../journal/journal-record'
|
|
24
30
|
import type { ReplayedWorktree } from '../journal/replay'
|
|
25
31
|
import { sessionSignal } from '../limits/plan-signals'
|
|
@@ -253,13 +259,15 @@ const sessionOf = (record: JournalRecord): string | null => {
|
|
|
253
259
|
}
|
|
254
260
|
|
|
255
261
|
/**
|
|
256
|
-
* The model a session launched on, and why (`null` before #603),
|
|
257
|
-
* effort it runs at (`null` before #659)
|
|
262
|
+
* The model a session launched on, and why (`null` before #603), the
|
|
263
|
+
* effort it runs at (`null` before #659), and its model trial group
|
|
264
|
+
* (`null` when not eligible, or before #698).
|
|
258
265
|
*/
|
|
259
266
|
export type SessionModel = {
|
|
260
267
|
model: string
|
|
261
268
|
model_reason: ModelReason | null
|
|
262
269
|
effort: EffortLevel | null
|
|
270
|
+
trial_group: TrialGroup | null
|
|
263
271
|
}
|
|
264
272
|
|
|
265
273
|
/**
|
|
@@ -302,8 +310,8 @@ export const sessionModel = ({
|
|
|
302
310
|
record.content.follow_up_of === null
|
|
303
311
|
))
|
|
304
312
|
if (started?.kind !== 'agent_started') return null
|
|
305
|
-
const { model, model_reason, effort } = started.content
|
|
306
|
-
return model === null ? null : { model, model_reason, effort }
|
|
313
|
+
const { model, model_reason, effort, trial_group } = started.content
|
|
314
|
+
return model === null ? null : { model, model_reason, effort, trial_group }
|
|
307
315
|
}
|
|
308
316
|
|
|
309
317
|
/**
|
|
@@ -321,6 +329,7 @@ export const followUpModel = ({
|
|
|
321
329
|
model: DEFAULT_MODEL,
|
|
322
330
|
model_reason: null,
|
|
323
331
|
effort: null,
|
|
332
|
+
trial_group: null,
|
|
324
333
|
}
|
|
325
334
|
|
|
326
335
|
export const launchAgent = async ({
|
|
@@ -335,10 +344,11 @@ export const launchAgent = async ({
|
|
|
335
344
|
// a routed role's model; a reviewer is locked on Opus. A redo keeps
|
|
336
345
|
// the model its first try journaled. The role's effort applies on any
|
|
337
346
|
// model (#659).
|
|
338
|
-
const { model, model_reason } = launchPick({
|
|
347
|
+
const { model, model_reason, trial_group } = launchPick({
|
|
339
348
|
records: context.journal.read(),
|
|
340
349
|
state: context.state,
|
|
341
350
|
config: context.config,
|
|
351
|
+
run_id: basename(context.run_dir),
|
|
342
352
|
ticket,
|
|
343
353
|
role,
|
|
344
354
|
step: context.step,
|
|
@@ -355,6 +365,7 @@ export const launchAgent = async ({
|
|
|
355
365
|
model,
|
|
356
366
|
model_reason,
|
|
357
367
|
effort,
|
|
368
|
+
trial_group,
|
|
358
369
|
},
|
|
359
370
|
})
|
|
360
371
|
await runTurn({
|
|
@@ -394,7 +405,7 @@ export const followUpAgent = async ({
|
|
|
394
405
|
value: context.state.snapshot?.tickets[ticket],
|
|
395
406
|
what: `snapshot of #${ticket}`,
|
|
396
407
|
})
|
|
397
|
-
const { model, model_reason, effort } = followUpModel({
|
|
408
|
+
const { model, model_reason, effort, trial_group } = followUpModel({
|
|
398
409
|
records: context.journal.read(),
|
|
399
410
|
session_id,
|
|
400
411
|
})
|
|
@@ -409,6 +420,7 @@ export const followUpAgent = async ({
|
|
|
409
420
|
model,
|
|
410
421
|
model_reason,
|
|
411
422
|
effort,
|
|
423
|
+
trial_group,
|
|
412
424
|
},
|
|
413
425
|
})
|
|
414
426
|
await runTurn({
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { basename } from 'node:path'
|
|
2
|
+
|
|
1
3
|
import type { MemoryAction, MemorySaveStep } from './decide-memory'
|
|
2
4
|
import { buildContext, need, runTurn, type BuildDeps } from './execute-build'
|
|
3
5
|
|
|
@@ -413,6 +415,7 @@ export const executeMemoryAction = async ({
|
|
|
413
415
|
records: journal.read(),
|
|
414
416
|
state: built.state,
|
|
415
417
|
config: built.config,
|
|
418
|
+
run_id: basename(built.run_dir),
|
|
416
419
|
ticket: null,
|
|
417
420
|
role: 'learner',
|
|
418
421
|
step,
|
|
@@ -432,6 +435,8 @@ export const executeMemoryAction = async ({
|
|
|
432
435
|
model,
|
|
433
436
|
model_reason,
|
|
434
437
|
effort,
|
|
438
|
+
// The learner is never in a model trial.
|
|
439
|
+
trial_group: null,
|
|
435
440
|
},
|
|
436
441
|
})
|
|
437
442
|
await runTurn({
|
package/engine/jev/model-pick.ts
CHANGED
|
@@ -29,6 +29,8 @@ import type { RunState } from '../journal/replay'
|
|
|
29
29
|
* - `unsure`: its confidence was below the bar or missing, or it picked a
|
|
30
30
|
* model the ask didn't offer.
|
|
31
31
|
* - `decision_model`: the decision model's pick.
|
|
32
|
+
* - `trial`: an unsure but cheaper pick that runs anyway, in a model
|
|
33
|
+
* trial's trial group (#698).
|
|
32
34
|
*/
|
|
33
35
|
export const ModelReasonSchema = z.enum([
|
|
34
36
|
'locked',
|
|
@@ -37,19 +39,79 @@ export const ModelReasonSchema = z.enum([
|
|
|
37
39
|
'no_answer',
|
|
38
40
|
'unsure',
|
|
39
41
|
'decision_model',
|
|
42
|
+
'trial',
|
|
40
43
|
])
|
|
41
44
|
|
|
42
45
|
export type ModelReason = z.infer<typeof ModelReasonSchema>
|
|
43
46
|
|
|
44
|
-
/**
|
|
45
|
-
|
|
47
|
+
/**
|
|
48
|
+
* A model trial's group (#698): `trial` runs Clef's unsure but cheaper
|
|
49
|
+
* pick, `control` stays on Opus.
|
|
50
|
+
*/
|
|
51
|
+
export const TrialGroupSchema = z.enum(['trial', 'control'])
|
|
52
|
+
|
|
53
|
+
export type TrialGroup = z.infer<typeof TrialGroupSchema>
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The model an agent launches on, why, and its model trial group (`null`
|
|
57
|
+
* when the launch isn't eligible for a trial).
|
|
58
|
+
*/
|
|
59
|
+
export type ModelPick = {
|
|
60
|
+
model: ModelName
|
|
61
|
+
model_reason: ModelReason
|
|
62
|
+
trial_group: TrialGroup | null
|
|
63
|
+
}
|
|
46
64
|
|
|
47
65
|
/** Reviewers, lenses, and the final review's fixers: always Opus. */
|
|
48
66
|
export const LOCKED_PICK: ModelPick = {
|
|
49
67
|
model: DEFAULT_MODEL,
|
|
50
68
|
model_reason: 'locked',
|
|
69
|
+
trial_group: null,
|
|
51
70
|
}
|
|
52
71
|
|
|
72
|
+
/**
|
|
73
|
+
* A launch's model trial group (#698), the same for the same key every
|
|
74
|
+
* time: the first 8 hex digits of the key's SHA-256, over `0x100000000`,
|
|
75
|
+
* below `share` is `trial`, else `control`.
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* trialGroupOf({ key: 'luca-20261028-202119-k6q1:697:test-writer', share: 0.5 }) // 'trial'
|
|
79
|
+
*/
|
|
80
|
+
export const trialGroupOf = ({
|
|
81
|
+
key,
|
|
82
|
+
share,
|
|
83
|
+
}: {
|
|
84
|
+
key: string
|
|
85
|
+
share: number
|
|
86
|
+
}): TrialGroup => {
|
|
87
|
+
const hex = new Bun.CryptoHasher('sha256').update(key).digest('hex')
|
|
88
|
+
return parseInt(hex.slice(0, 8), 16) / 0x100000000 < share
|
|
89
|
+
? 'trial'
|
|
90
|
+
: 'control'
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The text a launch's model trial group is hashed from: the run folder's
|
|
95
|
+
* name, the ticket's bare number and the role; `null` for a launch that
|
|
96
|
+
* can't be in a trial (the learner, or one with no ticket).
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* trialKeyOf({ run_id: 'luca-20261028-202119-k6q1', ticket: 697, role: 'test-writer' })
|
|
100
|
+
* // 'luca-20261028-202119-k6q1:697:test-writer'
|
|
101
|
+
*/
|
|
102
|
+
export const trialKeyOf = ({
|
|
103
|
+
run_id,
|
|
104
|
+
ticket,
|
|
105
|
+
role,
|
|
106
|
+
}: {
|
|
107
|
+
run_id: string
|
|
108
|
+
ticket: number | null
|
|
109
|
+
role: AgentRole
|
|
110
|
+
}): string | null =>
|
|
111
|
+
ticket === null || (role !== 'test-writer' && role !== 'implementer')
|
|
112
|
+
? null
|
|
113
|
+
: `${run_id}:${ticket}:${role}`
|
|
114
|
+
|
|
53
115
|
export type RoutedRole = (typeof ROUTED_ROLES)[number]
|
|
54
116
|
|
|
55
117
|
/** Whether a role has a model floor, so the decision model may pick its model. */
|
|
@@ -84,38 +146,69 @@ export type ModelAnswer = {
|
|
|
84
146
|
* offered is `unsure`; else the decision model's pick. Every fallback is
|
|
85
147
|
* Opus.
|
|
86
148
|
*
|
|
149
|
+
* An `unsure` pick is in a model trial (#698) when `trial_share` is above
|
|
150
|
+
* 0, the launch has a `trial_key`, its only fault is a confidence below
|
|
151
|
+
* the bar, and it picked an offered model at or above the floor and
|
|
152
|
+
* cheaper than Opus: its trial group runs the pick as `trial`, its control
|
|
153
|
+
* group stays on Opus as `unsure`.
|
|
154
|
+
*
|
|
87
155
|
* @example
|
|
88
|
-
* pickModel({ role: 'test-writer', models, failed_cheaper: false, answer: { offered: ['sonnet', 'opus'], pick: 'sonnet', confidence: 0.9 } })
|
|
89
|
-
* // { model: 'sonnet', model_reason: 'decision_model' } with a sonnet floor and the 0.
|
|
156
|
+
* pickModel({ role: 'test-writer', models, failed_cheaper: false, answer: { offered: ['sonnet', 'opus'], pick: 'sonnet', confidence: 0.9 }, trial_key: null })
|
|
157
|
+
* // { model: 'sonnet', model_reason: 'decision_model', trial_group: null } with a sonnet floor and the 0.5 bar
|
|
90
158
|
*/
|
|
91
159
|
export const pickModel = ({
|
|
92
160
|
role,
|
|
93
161
|
models,
|
|
94
162
|
failed_cheaper,
|
|
95
163
|
answer,
|
|
164
|
+
trial_key,
|
|
96
165
|
}: {
|
|
97
166
|
role: AgentRole
|
|
98
167
|
models: ModelsConfig
|
|
99
168
|
/** Whether the role already failed a try on its ticket on a cheaper model. */
|
|
100
169
|
failed_cheaper: boolean
|
|
101
170
|
answer: ModelAnswer | null
|
|
171
|
+
/** The text hashed for the trial group; `null` if it can't be in one. */
|
|
172
|
+
trial_key: string | null
|
|
102
173
|
}): ModelPick => {
|
|
174
|
+
const opus = (model_reason: ModelReason): ModelPick => ({
|
|
175
|
+
model: 'opus',
|
|
176
|
+
model_reason,
|
|
177
|
+
trial_group: null,
|
|
178
|
+
})
|
|
103
179
|
if (!isRoutedRole(role)) return LOCKED_PICK
|
|
104
|
-
if (failed_cheaper) return
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if (answer === null) return { model: 'opus', model_reason: 'no_answer' }
|
|
180
|
+
if (failed_cheaper) return opus('after_failure')
|
|
181
|
+
const floor = models.floors[role]
|
|
182
|
+
if (floor === 'opus') return opus('floor')
|
|
183
|
+
if (answer === null) return opus('no_answer')
|
|
109
184
|
const pick = ModelNameSchema.safeParse(answer.pick)
|
|
110
185
|
if (
|
|
111
186
|
!pick.success ||
|
|
112
187
|
!answer.offered.includes(pick.data) ||
|
|
113
|
-
answer.confidence === null
|
|
114
|
-
|
|
188
|
+
answer.confidence === null
|
|
189
|
+
) {
|
|
190
|
+
return opus('unsure')
|
|
191
|
+
}
|
|
192
|
+
if (answer.confidence >= models.confidence_bar) {
|
|
193
|
+
return {
|
|
194
|
+
model: pick.data,
|
|
195
|
+
model_reason: 'decision_model',
|
|
196
|
+
trial_group: null,
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const all = ModelNameSchema.options
|
|
200
|
+
if (
|
|
201
|
+
trial_key === null ||
|
|
202
|
+
models.trial_share <= 0 ||
|
|
203
|
+
pick.data === 'opus' ||
|
|
204
|
+
all.indexOf(pick.data) > all.indexOf(floor)
|
|
115
205
|
) {
|
|
116
|
-
return
|
|
206
|
+
return opus('unsure')
|
|
117
207
|
}
|
|
118
|
-
return {
|
|
208
|
+
return trialGroupOf({ key: trial_key, share: models.trial_share }) ===
|
|
209
|
+
'trial'
|
|
210
|
+
? { model: pick.data, model_reason: 'trial', trial_group: 'trial' }
|
|
211
|
+
: { ...opus('unsure'), trial_group: 'control' }
|
|
119
212
|
}
|
|
120
213
|
|
|
121
214
|
/**
|
|
@@ -211,9 +304,9 @@ export const redonePick = ({
|
|
|
211
304
|
)
|
|
212
305
|
if (started?.kind !== 'agent_started') return null
|
|
213
306
|
const model = ModelNameSchema.safeParse(started.content.model)
|
|
214
|
-
const { model_reason } = started.content
|
|
307
|
+
const { model_reason, trial_group } = started.content
|
|
215
308
|
if (!model.success || model_reason === null) return null
|
|
216
|
-
return { model: model.data, model_reason }
|
|
309
|
+
return { model: model.data, model_reason, trial_group }
|
|
217
310
|
}
|
|
218
311
|
|
|
219
312
|
/** Which try of its step a launch is: a redo names its first try's seq. */
|
|
@@ -246,13 +339,16 @@ export const asksModel = ({
|
|
|
246
339
|
|
|
247
340
|
/**
|
|
248
341
|
* The model a fresh agent launches on, from the run's config and the
|
|
249
|
-
* answer the launch step just journaled. A redo launches on the model
|
|
250
|
-
* first try journaled.
|
|
342
|
+
* answer the launch step just journaled. A redo launches on the model, and
|
|
343
|
+
* in the trial group, its first try journaled.
|
|
344
|
+
*
|
|
345
|
+
* @param run_id - The run folder's name, for the model trial's key.
|
|
251
346
|
*/
|
|
252
347
|
export const launchPick = ({
|
|
253
348
|
records,
|
|
254
349
|
state,
|
|
255
350
|
config,
|
|
351
|
+
run_id,
|
|
256
352
|
ticket,
|
|
257
353
|
role,
|
|
258
354
|
step,
|
|
@@ -260,6 +356,7 @@ export const launchPick = ({
|
|
|
260
356
|
records: JournalRecord[]
|
|
261
357
|
state: RunState
|
|
262
358
|
config: EngineConfig
|
|
359
|
+
run_id: string
|
|
263
360
|
ticket: number | null
|
|
264
361
|
role: AgentRole
|
|
265
362
|
step: LaunchTry
|
|
@@ -272,4 +369,5 @@ export const launchPick = ({
|
|
|
272
369
|
models: modelsOf({ config }),
|
|
273
370
|
failed_cheaper: failedCheaper({ state, ticket, role }),
|
|
274
371
|
answer: modelAnswerOf({ records, ticket, role }),
|
|
372
|
+
trial_key: trialKeyOf({ run_id, ticket, role }),
|
|
275
373
|
})
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
} from '../agents/role-results'
|
|
11
11
|
import { EffortLevelSchema } from '../config/engine-config'
|
|
12
12
|
import { GateCheckSchema } from '../gates/gate-schemas'
|
|
13
|
-
import { ModelReasonSchema } from '../jev/model-pick'
|
|
13
|
+
import { ModelReasonSchema, TrialGroupSchema } from '../jev/model-pick'
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* The journal entries of agent turns: started, finished or failed, their
|
|
@@ -26,7 +26,9 @@ import { ModelReasonSchema } from '../jev/model-pick'
|
|
|
26
26
|
* launched on that model (#603; a follow-up's is its session's); `null` in
|
|
27
27
|
* journals from before it. `effort` is the effort it ran at (#659; a
|
|
28
28
|
* follow-up's is its session's); `null` in journals from before it, when
|
|
29
|
-
* every agent ran at `high`.
|
|
29
|
+
* every agent ran at `high`. `trial_group` is its model trial group (#698;
|
|
30
|
+
* a redo's is its first try's, a follow-up's is its session's); `null` when
|
|
31
|
+
* it wasn't eligible, and in journals from before it.
|
|
30
32
|
*/
|
|
31
33
|
export const AgentStartedEntrySchema = z.object({
|
|
32
34
|
...ENTRY_FIELDS,
|
|
@@ -38,6 +40,7 @@ export const AgentStartedEntrySchema = z.object({
|
|
|
38
40
|
model: z.string().nullable().default(null),
|
|
39
41
|
model_reason: ModelReasonSchema.nullable().default(null),
|
|
40
42
|
effort: EffortLevelSchema.nullable().default(null),
|
|
43
|
+
trial_group: TrialGroupSchema.nullable().default(null),
|
|
41
44
|
}),
|
|
42
45
|
})
|
|
43
46
|
|
|
@@ -6,14 +6,13 @@ import {
|
|
|
6
6
|
} from './model-score-rules'
|
|
7
7
|
|
|
8
8
|
import type { FindingSeverity } from '../agents/role-results'
|
|
9
|
-
import { modelsOf } from '../config/config-fields'
|
|
10
9
|
import {
|
|
11
10
|
EffortLevelSchema,
|
|
12
11
|
ModelNameSchema,
|
|
13
|
-
ModelsConfigSchema,
|
|
14
12
|
type EffortLevel,
|
|
15
13
|
type ModelName,
|
|
16
14
|
} from '../config/engine-config'
|
|
15
|
+
import type { ModelReason, TrialGroup } from '../jev/model-pick'
|
|
17
16
|
import type { JournalRecord } from '../journal/journal-record'
|
|
18
17
|
import { countedTokens } from '../limits/plan-usage'
|
|
19
18
|
|
|
@@ -31,6 +30,9 @@ export type Pick = {
|
|
|
31
30
|
shadow: boolean
|
|
32
31
|
}
|
|
33
32
|
|
|
33
|
+
/** Clef's effort pick for a role on a ticket, with its confidence. */
|
|
34
|
+
export type EffortPick = { effort: EffortLevel; confidence: number }
|
|
35
|
+
|
|
34
36
|
/** What one run's journal says about one ticket. */
|
|
35
37
|
export type TicketFacts = {
|
|
36
38
|
/** The model of each role's first agent. */
|
|
@@ -40,11 +42,18 @@ export type TicketFacts = {
|
|
|
40
42
|
/** The first ticket review's effort, `high` when none is journaled. */
|
|
41
43
|
review_effort: EffortLevel
|
|
42
44
|
picks: Partial<Record<ScoredRole, Pick>>
|
|
45
|
+
/** Each role's first agent's model trial group, when it had one (#698). */
|
|
46
|
+
trial_groups: Partial<Record<ScoredRole, TrialGroup>>
|
|
47
|
+
/**
|
|
48
|
+
* Clef's first `agent_effort` answer for each role that has a
|
|
49
|
+
* confidence, at any confidence (#697).
|
|
50
|
+
*/
|
|
51
|
+
effort_picks: Partial<Record<ScoredRole, EffortPick>>
|
|
43
52
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
53
|
+
* Why each role's first agent got its model; `null` for one from before
|
|
54
|
+
* #603.
|
|
46
55
|
*/
|
|
47
|
-
|
|
56
|
+
model_reasons: Partial<Record<ScoredRole, ModelReason | null>>
|
|
48
57
|
reviews: number
|
|
49
58
|
/** The first ticket review's weighted findings. */
|
|
50
59
|
findings: number
|
|
@@ -100,7 +109,9 @@ const newTicket = (): TicketFacts => ({
|
|
|
100
109
|
efforts: {},
|
|
101
110
|
review_effort: EFFORT_BEFORE_659,
|
|
102
111
|
picks: {},
|
|
112
|
+
trial_groups: {},
|
|
103
113
|
effort_picks: {},
|
|
114
|
+
model_reasons: {},
|
|
104
115
|
reviews: 0,
|
|
105
116
|
findings: 0,
|
|
106
117
|
fix_rounds: 0,
|
|
@@ -125,15 +136,11 @@ export const factsOf = (records: JournalRecord[]): RunFacts => {
|
|
|
125
136
|
const effort_asks = new Map<number, string | null>()
|
|
126
137
|
const reviewed = new Set<number>()
|
|
127
138
|
const learner: LearnerRun[] = []
|
|
128
|
-
let bar = ModelsConfigSchema.parse({}).confidence_bar
|
|
129
139
|
let routed = false
|
|
130
140
|
let final_rounds = 0
|
|
131
141
|
let final_findings = 0
|
|
132
142
|
for (const record of records) {
|
|
133
143
|
switch (record.kind) {
|
|
134
|
-
case 'run_started':
|
|
135
|
-
bar = modelsOf({ config: record.content.config }).confidence_bar
|
|
136
|
-
break
|
|
137
144
|
case 'jev_asked':
|
|
138
145
|
if (record.content.job === 'agent_effort') {
|
|
139
146
|
effort_asks.set(record.seq, record.role)
|
|
@@ -159,11 +166,12 @@ export const factsOf = (records: JournalRecord[]): RunFacts => {
|
|
|
159
166
|
if (
|
|
160
167
|
isScored(effort_role) &&
|
|
161
168
|
level.success &&
|
|
162
|
-
confidence !== null
|
|
163
|
-
confidence >= bar
|
|
169
|
+
confidence !== null
|
|
164
170
|
) {
|
|
165
|
-
ticketOf(record.ticket).effort_picks[effort_role] ??=
|
|
166
|
-
level.data
|
|
171
|
+
ticketOf(record.ticket).effort_picks[effort_role] ??= {
|
|
172
|
+
effort: level.data,
|
|
173
|
+
confidence,
|
|
174
|
+
}
|
|
167
175
|
}
|
|
168
176
|
break
|
|
169
177
|
}
|
|
@@ -184,7 +192,14 @@ export const factsOf = (records: JournalRecord[]): RunFacts => {
|
|
|
184
192
|
break
|
|
185
193
|
}
|
|
186
194
|
case 'agent_started': {
|
|
187
|
-
const {
|
|
195
|
+
const {
|
|
196
|
+
role,
|
|
197
|
+
model,
|
|
198
|
+
effort,
|
|
199
|
+
follow_up_of,
|
|
200
|
+
model_reason,
|
|
201
|
+
trial_group,
|
|
202
|
+
} = record.content
|
|
188
203
|
const family = familyOf(model) ?? 'opus'
|
|
189
204
|
const level = effort ?? EFFORT_BEFORE_659
|
|
190
205
|
if (follow_up_of !== null) break
|
|
@@ -197,8 +212,14 @@ export const factsOf = (records: JournalRecord[]): RunFacts => {
|
|
|
197
212
|
if (record.ticket === null) break
|
|
198
213
|
if (isScored(role)) {
|
|
199
214
|
const facts = ticketOf(record.ticket)
|
|
200
|
-
facts.models[role]
|
|
201
|
-
|
|
215
|
+
if (facts.models[role] === undefined) {
|
|
216
|
+
facts.models[role] = family
|
|
217
|
+
facts.efforts[role] = level
|
|
218
|
+
facts.model_reasons[role] = model_reason
|
|
219
|
+
if (trial_group !== null) {
|
|
220
|
+
facts.trial_groups[role] = trial_group
|
|
221
|
+
}
|
|
222
|
+
}
|
|
202
223
|
}
|
|
203
224
|
if (
|
|
204
225
|
role === 'ticket-reviewer' &&
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { sum, type TicketFacts } from './model-score-facts'
|
|
2
|
+
import { EPSILON, MIN_TICKETS, WORSE_BY } from './model-score-rules'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The model score report's sides (#605): the rates a set of tickets is
|
|
6
|
+
* judged by, the verdict between two sides, and a side as `luca models`
|
|
7
|
+
* prints it. Pure.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type Verdict = 'not enough data' | 'worse' | 'about the same'
|
|
11
|
+
|
|
12
|
+
/** The rates a model is judged by, for its tickets or its baseline's. */
|
|
13
|
+
export type ScoreSide = {
|
|
14
|
+
tickets: number
|
|
15
|
+
/** Weighted first-review findings per 100 changed lines; `null` with no lines. */
|
|
16
|
+
findings_per_100_lines: number | null
|
|
17
|
+
/** `null` with no tickets. */
|
|
18
|
+
fix_rounds_per_ticket: number | null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Findings per 100 lines over `tickets`, those with no lines left out. */
|
|
22
|
+
const findingsRate = (tickets: TicketFacts[]): number | null => {
|
|
23
|
+
const rated = tickets.filter(({ lines }) => lines !== null)
|
|
24
|
+
const lines = sum(rated.map(({ lines }) => lines ?? 0))
|
|
25
|
+
return lines === 0
|
|
26
|
+
? null
|
|
27
|
+
: (sum(rated.map(({ findings }) => findings)) * 100) / lines
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const sideOf = (tickets: TicketFacts[]): ScoreSide => ({
|
|
31
|
+
tickets: tickets.length,
|
|
32
|
+
findings_per_100_lines: findingsRate(tickets),
|
|
33
|
+
fix_rounds_per_ticket:
|
|
34
|
+
tickets.length === 0
|
|
35
|
+
? null
|
|
36
|
+
: sum(tickets.map(({ fix_rounds }) => fix_rounds)) / tickets.length,
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
/** Whether `value` is more than `WORSE_BY` above `base`. */
|
|
40
|
+
const above = (value: number | null, base: number | null): boolean =>
|
|
41
|
+
value !== null && base !== null && value > base * (1 + WORSE_BY) + EPSILON
|
|
42
|
+
|
|
43
|
+
/** Whether either side has fewer than `MIN_TICKETS` tickets. */
|
|
44
|
+
export const tooFew = (a: ScoreSide, b: ScoreSide): boolean =>
|
|
45
|
+
a.tickets < MIN_TICKETS || b.tickets < MIN_TICKETS
|
|
46
|
+
|
|
47
|
+
/** Whether `side`'s findings rate or fix rounds per ticket is above `base`'s. */
|
|
48
|
+
export const sideAbove = (side: ScoreSide, base: ScoreSide): boolean =>
|
|
49
|
+
above(side.findings_per_100_lines, base.findings_per_100_lines) ||
|
|
50
|
+
above(side.fix_rounds_per_ticket, base.fix_rounds_per_ticket)
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The verdict on a cheaper model against its baseline: "not enough data"
|
|
54
|
+
* while either side has fewer than `MIN_TICKETS` tickets, "worse" when its
|
|
55
|
+
* findings rate or fix rounds per ticket is more than `WORSE_BY` above the
|
|
56
|
+
* baseline's, else "about the same". Pure.
|
|
57
|
+
*/
|
|
58
|
+
export const verdictOf = ({
|
|
59
|
+
side,
|
|
60
|
+
baseline,
|
|
61
|
+
}: {
|
|
62
|
+
side: ScoreSide
|
|
63
|
+
baseline: ScoreSide
|
|
64
|
+
}): Verdict => {
|
|
65
|
+
if (tooFew(side, baseline)) return 'not enough data'
|
|
66
|
+
return sideAbove(side, baseline) ? 'worse' : 'about the same'
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const plural = (count: number, word: string): string =>
|
|
70
|
+
`${count} ${word}${count === 1 ? '' : 's'}`
|
|
71
|
+
|
|
72
|
+
export const decimal = (value: number | null): string =>
|
|
73
|
+
value === null ? '-' : value.toFixed(2)
|
|
74
|
+
|
|
75
|
+
export const sideText = (side: ScoreSide): string =>
|
|
76
|
+
[
|
|
77
|
+
plural(side.tickets, 'ticket'),
|
|
78
|
+
`${decimal(side.findings_per_100_lines)} findings per 100 lines`,
|
|
79
|
+
`${decimal(side.fix_rounds_per_ticket)} fix rounds per ticket`,
|
|
80
|
+
].join(', ')
|
|
@@ -5,13 +5,21 @@ import {
|
|
|
5
5
|
type TicketFacts,
|
|
6
6
|
} from './model-score-facts'
|
|
7
7
|
import {
|
|
8
|
-
EPSILON,
|
|
9
8
|
FINDING_WEIGHTS,
|
|
10
|
-
MIN_TICKETS,
|
|
11
9
|
SCORED_ROLES,
|
|
12
|
-
WORSE_BY,
|
|
13
10
|
type ScoredRole,
|
|
14
11
|
} from './model-score-rules'
|
|
12
|
+
import {
|
|
13
|
+
decimal,
|
|
14
|
+
plural,
|
|
15
|
+
sideAbove,
|
|
16
|
+
sideOf,
|
|
17
|
+
sideText,
|
|
18
|
+
tooFew,
|
|
19
|
+
verdictOf,
|
|
20
|
+
type ScoreSide,
|
|
21
|
+
type Verdict,
|
|
22
|
+
} from './model-score-sides'
|
|
15
23
|
|
|
16
24
|
import {
|
|
17
25
|
EffortLevelSchema,
|
|
@@ -28,6 +36,7 @@ export {
|
|
|
28
36
|
WORSE_BY,
|
|
29
37
|
type ScoredRole,
|
|
30
38
|
} from './model-score-rules'
|
|
39
|
+
export { verdictOf, type ScoreSide, type Verdict } from './model-score-sides'
|
|
31
40
|
|
|
32
41
|
/**
|
|
33
42
|
* The model score report (#605): how good each routed role's work was on
|
|
@@ -37,17 +46,6 @@ export {
|
|
|
37
46
|
* reads records and never writes.
|
|
38
47
|
*/
|
|
39
48
|
|
|
40
|
-
export type Verdict = 'not enough data' | 'worse' | 'about the same'
|
|
41
|
-
|
|
42
|
-
/** The rates a model is judged by, for its tickets or its baseline's. */
|
|
43
|
-
export type ScoreSide = {
|
|
44
|
-
tickets: number
|
|
45
|
-
/** Weighted first-review findings per 100 changed lines; `null` with no lines. */
|
|
46
|
-
findings_per_100_lines: number | null
|
|
47
|
-
/** `null` with no tickets. */
|
|
48
|
-
fix_rounds_per_ticket: number | null
|
|
49
|
-
}
|
|
50
|
-
|
|
51
49
|
/** One routed role on one model at one effort, its tickets reviewed at one effort. */
|
|
52
50
|
export type ModelScore = ScoreSide & {
|
|
53
51
|
role: ScoredRole
|
|
@@ -71,6 +69,17 @@ export type ModelScore = ScoreSide & {
|
|
|
71
69
|
baseline: ScoreSide
|
|
72
70
|
/** `null` for Opus, which is the baseline the others are judged by. */
|
|
73
71
|
verdict: Verdict | null
|
|
72
|
+
/** Its tickets in a model trial's trial group (#698). */
|
|
73
|
+
trial_tickets: number
|
|
74
|
+
/**
|
|
75
|
+
* The role's control group tickets (#698) where Clef picked this model,
|
|
76
|
+
* at the same effort and reviewed at the same effort.
|
|
77
|
+
*/
|
|
78
|
+
control: ScoreSide
|
|
79
|
+
/**
|
|
80
|
+
* Its trial tickets against its control tickets; `null` with neither.
|
|
81
|
+
*/
|
|
82
|
+
trial_verdict: Verdict | null
|
|
74
83
|
}
|
|
75
84
|
|
|
76
85
|
export type EffortPicksVerdict =
|
|
@@ -78,17 +87,33 @@ export type EffortPicksVerdict =
|
|
|
78
87
|
| 'Clef spots hard tickets'
|
|
79
88
|
| 'no sign yet'
|
|
80
89
|
|
|
90
|
+
/** Clef's confidence bands its effort picks are broken down by (#697). */
|
|
91
|
+
const CONFIDENCE_BANDS = [
|
|
92
|
+
{ band: 'below 0.3', from: -Infinity, below: 0.3 },
|
|
93
|
+
{ band: '0.3 to 0.5', from: 0.3, below: 0.5 },
|
|
94
|
+
{ band: '0.5 and up', from: 0.5, below: Infinity },
|
|
95
|
+
] as const
|
|
96
|
+
|
|
97
|
+
/** The two sides of Clef's effort picks within one confidence band. */
|
|
98
|
+
export type EffortPicksBand = {
|
|
99
|
+
band: (typeof CONFIDENCE_BANDS)[number]['band']
|
|
100
|
+
wanted_more: ScoreSide
|
|
101
|
+
rest: ScoreSide
|
|
102
|
+
}
|
|
103
|
+
|
|
81
104
|
/**
|
|
82
105
|
* Clef's shadow-mode effort picks for one writer role (#663): the tickets
|
|
83
|
-
* where its `agent_effort` answer
|
|
84
|
-
* was higher than the effort the role's first agent ran
|
|
85
|
-
* tickets where it was the same or lower
|
|
106
|
+
* where its first `agent_effort` answer with a confidence, at any
|
|
107
|
+
* confidence (#697), was higher than the effort the role's first agent ran
|
|
108
|
+
* at, against the tickets where it was the same or lower, then the same by
|
|
109
|
+
* Clef's confidence.
|
|
86
110
|
*/
|
|
87
111
|
export type EffortPicks = {
|
|
88
112
|
role: ScoredRole
|
|
89
113
|
wanted_more: ScoreSide
|
|
90
114
|
rest: ScoreSide
|
|
91
115
|
verdict: EffortPicksVerdict
|
|
116
|
+
by_confidence: EffortPicksBand[]
|
|
92
117
|
}
|
|
93
118
|
|
|
94
119
|
/** The final review's findings over a set of runs. */
|
|
@@ -102,6 +127,11 @@ export type ModelReport = {
|
|
|
102
127
|
scores: ModelScore[]
|
|
103
128
|
/** One per scored role. */
|
|
104
129
|
effort_picks: EffortPicks[]
|
|
130
|
+
/**
|
|
131
|
+
* The roles whose every counted ticket with a model reason ran Opus for
|
|
132
|
+
* its floor (#697): no cheaper model was ever tried.
|
|
133
|
+
*/
|
|
134
|
+
floor_only: ScoredRole[]
|
|
105
135
|
/** Fresh learner agents per model and effort; the learner isn't scored. */
|
|
106
136
|
learner: { model: ModelName; effort: EffortLevel; agents: number }[]
|
|
107
137
|
/** Runs with any agent on a cheaper model, and runs without. */
|
|
@@ -119,60 +149,13 @@ const median = (values: number[]): number | null => {
|
|
|
119
149
|
: ((sorted[middle - 1] ?? 0) + (sorted[middle] ?? 0)) / 2
|
|
120
150
|
}
|
|
121
151
|
|
|
122
|
-
/** Findings per 100 lines over `tickets`, those with no lines left out. */
|
|
123
|
-
const findingsRate = (tickets: TicketFacts[]): number | null => {
|
|
124
|
-
const rated = tickets.filter(({ lines }) => lines !== null)
|
|
125
|
-
const lines = sum(rated.map(({ lines }) => lines ?? 0))
|
|
126
|
-
return lines === 0
|
|
127
|
-
? null
|
|
128
|
-
: (sum(rated.map(({ findings }) => findings)) * 100) / lines
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const sideOf = (tickets: TicketFacts[]): ScoreSide => ({
|
|
132
|
-
tickets: tickets.length,
|
|
133
|
-
findings_per_100_lines: findingsRate(tickets),
|
|
134
|
-
fix_rounds_per_ticket:
|
|
135
|
-
tickets.length === 0
|
|
136
|
-
? null
|
|
137
|
-
: sum(tickets.map(({ fix_rounds }) => fix_rounds)) / tickets.length,
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
/** Whether `value` is more than `WORSE_BY` above `base`. */
|
|
141
|
-
const above = (value: number | null, base: number | null): boolean =>
|
|
142
|
-
value !== null && base !== null && value > base * (1 + WORSE_BY) + EPSILON
|
|
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
|
-
|
|
153
|
-
/**
|
|
154
|
-
* The verdict on a cheaper model against its baseline: "not enough data"
|
|
155
|
-
* while either side has fewer than `MIN_TICKETS` tickets, "worse" when its
|
|
156
|
-
* findings rate or fix rounds per ticket is more than `WORSE_BY` above the
|
|
157
|
-
* baseline's, else "about the same". Pure.
|
|
158
|
-
*/
|
|
159
|
-
export const verdictOf = ({
|
|
160
|
-
side,
|
|
161
|
-
baseline,
|
|
162
|
-
}: {
|
|
163
|
-
side: ScoreSide
|
|
164
|
-
baseline: ScoreSide
|
|
165
|
-
}): Verdict => {
|
|
166
|
-
if (tooFew(side, baseline)) return 'not enough data'
|
|
167
|
-
return sideAbove(side, baseline) ? 'worse' : 'about the same'
|
|
168
|
-
}
|
|
169
|
-
|
|
170
152
|
/**
|
|
171
153
|
* Clef's effort picks for `role` over every run's tickets: a ticket wants
|
|
172
154
|
* more when its pick is above the effort the role's first agent ran at, in
|
|
173
155
|
* `EffortLevelSchema`'s order. "Clef spots hard tickets" when the "wanted
|
|
174
156
|
* 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.
|
|
157
|
+
* `WORSE_BY` above the rest's, with `MIN_TICKETS` on each side. Each band
|
|
158
|
+
* holds the tickets whose pick's confidence is at or above its lower edge.
|
|
176
159
|
*/
|
|
177
160
|
const effortPicksOf = ({
|
|
178
161
|
tickets,
|
|
@@ -188,12 +171,23 @@ const effortPicksOf = ({
|
|
|
188
171
|
const ran = ticket.efforts[role]
|
|
189
172
|
return pick === undefined || ran === undefined
|
|
190
173
|
? []
|
|
191
|
-
: [
|
|
174
|
+
: [
|
|
175
|
+
{
|
|
176
|
+
ticket,
|
|
177
|
+
confidence: pick.confidence,
|
|
178
|
+
wants_more: rank(pick.effort) > rank(ran),
|
|
179
|
+
},
|
|
180
|
+
]
|
|
192
181
|
})
|
|
193
|
-
const sideWhere = (
|
|
182
|
+
const sideWhere = (
|
|
183
|
+
wants_more: boolean,
|
|
184
|
+
within: (confidence: number) => boolean = () => true
|
|
185
|
+
): ScoreSide =>
|
|
194
186
|
sideOf(
|
|
195
187
|
picked.flatMap((entry) =>
|
|
196
|
-
entry.wants_more === wants_more
|
|
188
|
+
entry.wants_more === wants_more && within(entry.confidence)
|
|
189
|
+
? [entry.ticket]
|
|
190
|
+
: []
|
|
197
191
|
)
|
|
198
192
|
)
|
|
199
193
|
const wanted_more = sideWhere(true)
|
|
@@ -204,7 +198,35 @@ const effortPicksOf = ({
|
|
|
204
198
|
? 'Clef spots hard tickets'
|
|
205
199
|
: 'no sign yet'
|
|
206
200
|
}
|
|
207
|
-
|
|
201
|
+
const by_confidence = CONFIDENCE_BANDS.map(({ band, from, below }) => {
|
|
202
|
+
const within = (confidence: number) =>
|
|
203
|
+
confidence >= from && confidence < below
|
|
204
|
+
return {
|
|
205
|
+
band,
|
|
206
|
+
wanted_more: sideWhere(true, within),
|
|
207
|
+
rest: sideWhere(false, within),
|
|
208
|
+
}
|
|
209
|
+
})
|
|
210
|
+
return { role, wanted_more, rest, verdict: verdictOn(), by_confidence }
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Whether every one of `role`'s tickets that has a model reason ran Opus
|
|
215
|
+
* for its floor, with at least one such ticket. A `null` reason, from
|
|
216
|
+
* before #603, counts neither way.
|
|
217
|
+
*/
|
|
218
|
+
const isFloorOnly = ({
|
|
219
|
+
tickets,
|
|
220
|
+
role,
|
|
221
|
+
}: {
|
|
222
|
+
tickets: TicketFacts[]
|
|
223
|
+
role: ScoredRole
|
|
224
|
+
}): boolean => {
|
|
225
|
+
const reasons = tickets.flatMap(({ models, model_reasons }) => {
|
|
226
|
+
const reason = model_reasons[role] ?? null
|
|
227
|
+
return models[role] === undefined || reason === null ? [] : [reason]
|
|
228
|
+
})
|
|
229
|
+
return reasons.length > 0 && reasons.every((reason) => reason === 'floor')
|
|
208
230
|
}
|
|
209
231
|
|
|
210
232
|
/**
|
|
@@ -245,6 +267,15 @@ const scoreOf = ({
|
|
|
245
267
|
)
|
|
246
268
|
const side = sideOf(group)
|
|
247
269
|
const baseline_side = sideOf(baseline)
|
|
270
|
+
const trial = group.filter(
|
|
271
|
+
({ trial_groups }) => trial_groups[role] === 'trial'
|
|
272
|
+
)
|
|
273
|
+
const control = sideOf(
|
|
274
|
+
alike.filter(
|
|
275
|
+
({ trial_groups, picks }) =>
|
|
276
|
+
trial_groups[role] === 'control' && picks[role]?.model === model
|
|
277
|
+
)
|
|
278
|
+
)
|
|
248
279
|
return {
|
|
249
280
|
role,
|
|
250
281
|
model,
|
|
@@ -269,6 +300,12 @@ const scoreOf = ({
|
|
|
269
300
|
model === 'opus'
|
|
270
301
|
? null
|
|
271
302
|
: verdictOf({ side, baseline: baseline_side }),
|
|
303
|
+
trial_tickets: trial.length,
|
|
304
|
+
control,
|
|
305
|
+
trial_verdict:
|
|
306
|
+
trial.length === 0 && control.tickets === 0
|
|
307
|
+
? null
|
|
308
|
+
: verdictOf({ side: sideOf(trial), baseline: control }),
|
|
272
309
|
}
|
|
273
310
|
}
|
|
274
311
|
|
|
@@ -319,7 +356,10 @@ export const scoreModels = ({
|
|
|
319
356
|
)
|
|
320
357
|
)
|
|
321
358
|
)
|
|
322
|
-
).filter(
|
|
359
|
+
).filter(
|
|
360
|
+
({ tickets, baseline, control }) =>
|
|
361
|
+
tickets > 0 || baseline.tickets > 0 || control.tickets > 0
|
|
362
|
+
)
|
|
323
363
|
const learner = facts.flatMap((run) => run.learner)
|
|
324
364
|
return {
|
|
325
365
|
runs: runs.length,
|
|
@@ -327,6 +367,9 @@ export const scoreModels = ({
|
|
|
327
367
|
effort_picks: SCORED_ROLES.map((role) =>
|
|
328
368
|
effortPicksOf({ tickets, role })
|
|
329
369
|
),
|
|
370
|
+
floor_only: SCORED_ROLES.filter((role) =>
|
|
371
|
+
isFloorOnly({ tickets, role })
|
|
372
|
+
),
|
|
330
373
|
learner: ModelNameSchema.options
|
|
331
374
|
.flatMap((model) =>
|
|
332
375
|
levels.map((effort) => ({
|
|
@@ -348,19 +391,6 @@ export const scoreModels = ({
|
|
|
348
391
|
}
|
|
349
392
|
}
|
|
350
393
|
|
|
351
|
-
const plural = (count: number, word: string): string =>
|
|
352
|
-
`${count} ${word}${count === 1 ? '' : 's'}`
|
|
353
|
-
|
|
354
|
-
const decimal = (value: number | null): string =>
|
|
355
|
-
value === null ? '-' : value.toFixed(2)
|
|
356
|
-
|
|
357
|
-
const sideText = (side: ScoreSide): string =>
|
|
358
|
-
[
|
|
359
|
-
plural(side.tickets, 'ticket'),
|
|
360
|
-
`${decimal(side.findings_per_100_lines)} findings per 100 lines`,
|
|
361
|
-
`${decimal(side.fix_rounds_per_ticket)} fix rounds per ticket`,
|
|
362
|
-
].join(', ')
|
|
363
|
-
|
|
364
394
|
const scoreText = (score: ModelScore): string[] => {
|
|
365
395
|
const other = SCORED_ROLES.find((role) => role !== score.role)
|
|
366
396
|
const tokens = score.tokens_per_finished_ticket
|
|
@@ -368,6 +398,9 @@ const scoreText = (score: ModelScore): string[] => {
|
|
|
368
398
|
` ${score.model} at ${score.effort}: ${[
|
|
369
399
|
sideText(score),
|
|
370
400
|
`${score.stuck_tickets} stuck`,
|
|
401
|
+
...(score.trial_tickets === 0
|
|
402
|
+
? []
|
|
403
|
+
: [plural(score.trial_tickets, 'trial')]),
|
|
371
404
|
`${tokens === null ? '-' : Math.round(tokens)} tokens per finished ticket`,
|
|
372
405
|
`median confidence ${decimal(score.median_confidence)}`,
|
|
373
406
|
`${other} cheaper on ${score.other_role_cheaper}`,
|
|
@@ -379,16 +412,43 @@ const scoreText = (score: ModelScore): string[] => {
|
|
|
379
412
|
` baseline (Clef picked ${score.model} in shadow mode, ran opus at ${score.effort}): ${sideText(score.baseline)}`,
|
|
380
413
|
` verdict: ${score.verdict}`,
|
|
381
414
|
]),
|
|
415
|
+
...(score.trial_verdict === null
|
|
416
|
+
? []
|
|
417
|
+
: [
|
|
418
|
+
` control (Clef picked ${score.model}, left out of the trial, ran opus at ${score.effort}): ${sideText(score.control)}`,
|
|
419
|
+
` trial verdict: ${score.trial_verdict}`,
|
|
420
|
+
]),
|
|
382
421
|
]
|
|
383
422
|
}
|
|
384
423
|
|
|
385
424
|
const effortPicksText = (picks: EffortPicks): string[] => [
|
|
386
|
-
" Clef's effort picks (shadow mode)",
|
|
425
|
+
" Clef's effort picks (shadow mode), every answer",
|
|
387
426
|
` wanted more effort than it ran at: ${sideText(picks.wanted_more)}`,
|
|
388
427
|
` the rest: ${sideText(picks.rest)}`,
|
|
389
428
|
` verdict: ${picks.verdict}`,
|
|
429
|
+
" by Clef's confidence:",
|
|
430
|
+
...picks.by_confidence.map(
|
|
431
|
+
({ band, wanted_more, rest }) =>
|
|
432
|
+
` ${band}: wanted more ${sideText(wanted_more)}; the rest ${sideText(rest)}`
|
|
433
|
+
),
|
|
390
434
|
]
|
|
391
435
|
|
|
436
|
+
const FLOOR_LINE =
|
|
437
|
+
" no cheaper model was ever tried: this role's floor is opus in every run. Set models.floors in .luca/config.json to let it."
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* A role's model lines: for a floor-only role, its Opus lines and the floor
|
|
441
|
+
* line in place of the rest.
|
|
442
|
+
*/
|
|
443
|
+
const scoresText = (scores: ModelScore[], floor_only: boolean): string[] => {
|
|
444
|
+
if (scores.length === 0) return [' no tickets yet']
|
|
445
|
+
if (!floor_only) return scores.flatMap(scoreText)
|
|
446
|
+
return [
|
|
447
|
+
...scores.filter(({ model }) => model === 'opus').flatMap(scoreText),
|
|
448
|
+
FLOOR_LINE,
|
|
449
|
+
]
|
|
450
|
+
}
|
|
451
|
+
|
|
392
452
|
const finalText = (label: string, side: FinalReviewSide): string =>
|
|
393
453
|
`${decimal(side.findings_per_100_lines)} ${label} (${plural(side.runs, 'run')})`
|
|
394
454
|
|
|
@@ -411,9 +471,7 @@ export const formatModelReport = ({
|
|
|
411
471
|
const picks = report.effort_picks.find((found) => found.role === role)
|
|
412
472
|
return [
|
|
413
473
|
role,
|
|
414
|
-
...(scores.
|
|
415
|
-
? [' no tickets yet']
|
|
416
|
-
: scores.flatMap(scoreText)),
|
|
474
|
+
...scoresText(scores, report.floor_only.includes(role)),
|
|
417
475
|
...(picks === undefined ? [] : effortPicksText(picks)),
|
|
418
476
|
]
|
|
419
477
|
})
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
roleSection,
|
|
2
3
|
runOf,
|
|
3
4
|
ticketRun,
|
|
4
5
|
type Built,
|
|
@@ -121,19 +122,17 @@ export const runWithBar = (
|
|
|
121
122
|
export const effortPicksOf = (report: ModelReport, role: Built) =>
|
|
122
123
|
report.effort_picks.find((picks) => picks.role === role)
|
|
123
124
|
|
|
125
|
+
/** The heading of a role's "Clef's effort picks" section, as printed. */
|
|
126
|
+
export const PICKS_HEADING = " Clef's effort picks (shadow mode), every answer"
|
|
127
|
+
|
|
124
128
|
/**
|
|
125
129
|
* The lines of `role`'s "Clef's effort picks (shadow mode)" section in the
|
|
126
130
|
* printed report: the lines after its heading, within the role's part.
|
|
127
131
|
*/
|
|
128
132
|
export const pickSection = (text: string, role: Built): string[] => {
|
|
129
|
-
const
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
(line, at) => at > start && line !== '' && !line.startsWith(' ')
|
|
133
|
-
)
|
|
134
|
-
const part = start < 0 ? [] : lines.slice(start + 1, end)
|
|
135
|
-
const heading = part.findIndex(
|
|
136
|
-
(line) => line.trim() === "Clef's effort picks (shadow mode)"
|
|
133
|
+
const part = roleSection(text, role)
|
|
134
|
+
const heading = part.findIndex((line) =>
|
|
135
|
+
line.trim().startsWith("Clef's effort picks (shadow mode)")
|
|
137
136
|
)
|
|
138
137
|
return heading < 0 ? [] : part.slice(heading + 1)
|
|
139
138
|
}
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import { recordsFrom } from './intake-fixtures'
|
|
13
13
|
|
|
14
14
|
import type { AgentRole } from '../agents/role-results'
|
|
15
|
+
import type { ModelReason } from '../jev/model-pick'
|
|
15
16
|
import type { JournalEntry, JournalRecord } from '../journal/journal-record'
|
|
16
17
|
import type { ModelScore } from '../models/model-score'
|
|
17
18
|
|
|
@@ -29,6 +30,8 @@ export type Built = 'test-writer' | 'implementer'
|
|
|
29
30
|
/**
|
|
30
31
|
* An agent turn started on `model`; `null` writes one from before #600. It
|
|
31
32
|
* ran at `effort`; left out writes one from before #659, with no effort.
|
|
33
|
+
* `model_reason` is why it got its model; left out writes one from before
|
|
34
|
+
* #603, with no reason.
|
|
32
35
|
*/
|
|
33
36
|
export const started = ({
|
|
34
37
|
ticket,
|
|
@@ -36,12 +39,14 @@ export const started = ({
|
|
|
36
39
|
model,
|
|
37
40
|
follow_up_of,
|
|
38
41
|
effort,
|
|
42
|
+
model_reason,
|
|
39
43
|
}: {
|
|
40
44
|
ticket: number | null
|
|
41
45
|
role: AgentRole
|
|
42
46
|
model: Model | null
|
|
43
47
|
follow_up_of?: string
|
|
44
48
|
effort?: Effort
|
|
49
|
+
model_reason?: ModelReason | null
|
|
45
50
|
}): JournalEntry => ({
|
|
46
51
|
kind: 'agent_started',
|
|
47
52
|
ticket,
|
|
@@ -52,6 +57,7 @@ export const started = ({
|
|
|
52
57
|
follow_up_of: follow_up_of ?? null,
|
|
53
58
|
...(model === null ? {} : { model }),
|
|
54
59
|
...(effort === undefined ? {} : { effort }),
|
|
60
|
+
...(model_reason === undefined ? {} : { model_reason }),
|
|
55
61
|
},
|
|
56
62
|
})
|
|
57
63
|
|
|
@@ -122,7 +128,8 @@ export type Ask = Omit<Parameters<typeof modelAsk>[0], 'ticket'>
|
|
|
122
128
|
* (opus unless told), the first review's `findings`, then `rounds` fix
|
|
123
129
|
* rounds (one when the findings block), each fix's review finding a
|
|
124
130
|
* should-fix until the last approves, and a join of `lines` lines. Each
|
|
125
|
-
* role's agents run at its effort, left out as before #659
|
|
131
|
+
* role's agents run at its effort, left out as before #659, and with its
|
|
132
|
+
* model reason, left out as before #603.
|
|
126
133
|
*/
|
|
127
134
|
export const ticketRun = ({
|
|
128
135
|
ticket,
|
|
@@ -130,6 +137,8 @@ export const ticketRun = ({
|
|
|
130
137
|
impl = 'opus',
|
|
131
138
|
tw_effort,
|
|
132
139
|
impl_effort,
|
|
140
|
+
tw_reason,
|
|
141
|
+
impl_reason,
|
|
133
142
|
review_effort,
|
|
134
143
|
asks = [],
|
|
135
144
|
findings = [],
|
|
@@ -143,6 +152,8 @@ export const ticketRun = ({
|
|
|
143
152
|
impl?: Model | null
|
|
144
153
|
tw_effort?: Effort
|
|
145
154
|
impl_effort?: Effort
|
|
155
|
+
tw_reason?: ModelReason | null
|
|
156
|
+
impl_reason?: ModelReason | null
|
|
146
157
|
review_effort?: Effort
|
|
147
158
|
asks?: Ask[]
|
|
148
159
|
findings?: Severity[]
|
|
@@ -184,6 +195,7 @@ export const ticketRun = ({
|
|
|
184
195
|
role: 'test-writer',
|
|
185
196
|
model: tw,
|
|
186
197
|
effort: tw_effort,
|
|
198
|
+
model_reason: tw_reason,
|
|
187
199
|
}),
|
|
188
200
|
agentFailed({
|
|
189
201
|
ticket,
|
|
@@ -197,6 +209,7 @@ export const ticketRun = ({
|
|
|
197
209
|
role: 'test-writer',
|
|
198
210
|
model: tw_failed_first ? 'opus' : tw,
|
|
199
211
|
effort: tw_effort,
|
|
212
|
+
model_reason: tw_failed_first ? 'after_failure' : tw_reason,
|
|
200
213
|
}),
|
|
201
214
|
testsWritten({ ticket }),
|
|
202
215
|
agentSession({
|
|
@@ -210,6 +223,7 @@ export const ticketRun = ({
|
|
|
210
223
|
role: 'implementer',
|
|
211
224
|
model: impl,
|
|
212
225
|
effort: impl_effort,
|
|
226
|
+
model_reason: impl_reason,
|
|
213
227
|
}),
|
|
214
228
|
implemented({ ticket }),
|
|
215
229
|
...review(findings, 0),
|
|
@@ -220,6 +234,7 @@ export const ticketRun = ({
|
|
|
220
234
|
model: impl,
|
|
221
235
|
follow_up_of: 'impl-1',
|
|
222
236
|
effort: impl_effort,
|
|
237
|
+
model_reason: impl_reason,
|
|
223
238
|
}),
|
|
224
239
|
implemented({ ticket }),
|
|
225
240
|
...review(index + 1 < fixes ? ['should_fix'] : [], index + 1),
|
|
@@ -291,6 +306,20 @@ export const groups = (scores: ModelScore[]): string[] =>
|
|
|
291
306
|
.map(({ role, model }) => `${role}/${model}`)
|
|
292
307
|
.sort()
|
|
293
308
|
|
|
309
|
+
/**
|
|
310
|
+
* The lines of `role`'s part of the printed report: the lines after its
|
|
311
|
+
* heading, up to the next line that isn't indented.
|
|
312
|
+
*/
|
|
313
|
+
export const roleSection = (text: string, role: Built): string[] => {
|
|
314
|
+
const lines = text.split('\n')
|
|
315
|
+
const start = lines.indexOf(role)
|
|
316
|
+
if (start < 0) return []
|
|
317
|
+
const end = lines.findIndex(
|
|
318
|
+
(line, at) => at > start && line !== '' && !line.startsWith(' ')
|
|
319
|
+
)
|
|
320
|
+
return lines.slice(start + 1, end < 0 ? undefined : end)
|
|
321
|
+
}
|
|
322
|
+
|
|
294
323
|
/** Clef picked `pick` for the test-writer, sure enough to act on it. */
|
|
295
324
|
export const sure = (pick: Model, confidence = 0.9): Ask => ({
|
|
296
325
|
role: 'test-writer',
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alecsibilia/luca",
|
|
3
3
|
"description": "Luca: turns a spec on GitHub Issues into a reviewed pull request, run from Paseo with Claude Code",
|
|
4
|
-
"version": "14.0.0-alpha.
|
|
4
|
+
"version": "14.0.0-alpha.28",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Alec Sibilia <sibilia.alec@gmail.com>",
|
|
7
7
|
"homepage": "https://github.com/asibilia/luca-framework#readme",
|