@alecsibilia/luca 14.0.0-alpha.21 → 14.0.0-alpha.23
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 +8 -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 +10 -9
- 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/decide-final-fix.ts +11 -4
- package/engine/core/decide-final-review.ts +3 -1
- package/engine/core/execute-final-review.ts +11 -2
- package/engine/core/execute-memory.ts +7 -0
- package/engine/core/final-failure-cap.ts +8 -4
- package/engine/core/final-review-text.ts +21 -3
- package/engine/core/pull-request-text.ts +11 -7
- package/engine/core/stuck-format.ts +14 -2
- 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/journal/final-review-replay.ts +3 -0
- package/engine/journal/final-review-round.ts +66 -4
- package/engine/journal/final-review-state.ts +4 -1
- package/engine/journal/replay.ts +1 -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
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EFFORT_BEFORE_659,
|
|
3
|
+
FINDING_WEIGHTS,
|
|
4
|
+
SCORED_ROLES,
|
|
5
|
+
type ScoredRole,
|
|
6
|
+
} from './model-score-rules'
|
|
7
|
+
|
|
8
|
+
import type { FindingSeverity } from '../agents/role-results'
|
|
9
|
+
import { modelsOf } from '../config/config-fields'
|
|
10
|
+
import {
|
|
11
|
+
EffortLevelSchema,
|
|
12
|
+
ModelNameSchema,
|
|
13
|
+
ModelsConfigSchema,
|
|
14
|
+
type EffortLevel,
|
|
15
|
+
type ModelName,
|
|
16
|
+
} from '../config/engine-config'
|
|
17
|
+
import type { JournalRecord } from '../journal/journal-record'
|
|
18
|
+
import { countedTokens } from '../limits/plan-usage'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* What one run's journal says for the model score report (#605): each
|
|
22
|
+
* ticket's models, efforts, Clef's picks and review results, the learner's
|
|
23
|
+
* agents, and the final review's findings. Pure: it only reads records.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** Clef's first model pick for a role on a ticket. */
|
|
27
|
+
export type Pick = {
|
|
28
|
+
model: ModelName | null
|
|
29
|
+
confidence: number | null
|
|
30
|
+
/** Asked in shadow mode: `acted: false`, or an older `ticket_model` ask. */
|
|
31
|
+
shadow: boolean
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** What one run's journal says about one ticket. */
|
|
35
|
+
export type TicketFacts = {
|
|
36
|
+
/** The model of each role's first agent. */
|
|
37
|
+
models: Partial<Record<ScoredRole, ModelName>>
|
|
38
|
+
/** The effort of each role's first agent, an older one's `high`. */
|
|
39
|
+
efforts: Partial<Record<ScoredRole, EffortLevel>>
|
|
40
|
+
/** The first ticket review's effort, `high` when none is journaled. */
|
|
41
|
+
review_effort: EffortLevel
|
|
42
|
+
picks: Partial<Record<ScoredRole, Pick>>
|
|
43
|
+
/**
|
|
44
|
+
* Clef's first `agent_effort` answer for each role at or above the run's
|
|
45
|
+
* confidence bar.
|
|
46
|
+
*/
|
|
47
|
+
effort_picks: Partial<Record<ScoredRole, EffortLevel>>
|
|
48
|
+
reviews: number
|
|
49
|
+
/** The first ticket review's weighted findings. */
|
|
50
|
+
findings: number
|
|
51
|
+
fix_rounds: number
|
|
52
|
+
joined: boolean
|
|
53
|
+
lines: number | null
|
|
54
|
+
stuck: boolean
|
|
55
|
+
tokens: number | null
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** A fresh learner agent's model and effort. */
|
|
59
|
+
export type LearnerRun = { model: ModelName; effort: EffortLevel }
|
|
60
|
+
|
|
61
|
+
/** What one run's journal says. */
|
|
62
|
+
export type RunFacts = {
|
|
63
|
+
tickets: TicketFacts[]
|
|
64
|
+
routed: boolean
|
|
65
|
+
learner: LearnerRun[]
|
|
66
|
+
/** The first final review round's weighted findings, or `null` with none. */
|
|
67
|
+
final_findings: number | null
|
|
68
|
+
/** The joined tickets' lines, or `null` when one has none. */
|
|
69
|
+
lines: number | null
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The model family a short name or full model id (as older `ticket_model`
|
|
74
|
+
* asks named them) belongs to, or `null` for none.
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* familyOf('sonnet') // 'sonnet'
|
|
78
|
+
*/
|
|
79
|
+
const familyOf = (name: unknown): ModelName | null => {
|
|
80
|
+
if (typeof name !== 'string') return null
|
|
81
|
+
const lower = name.toLowerCase()
|
|
82
|
+
return (
|
|
83
|
+
ModelNameSchema.options.find((model) => lower.includes(model)) ?? null
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const isScored = (role: string | null): role is ScoredRole =>
|
|
88
|
+
SCORED_ROLES.some((scored) => scored === role)
|
|
89
|
+
|
|
90
|
+
export const sum = (values: number[]): number =>
|
|
91
|
+
values.reduce((total, value) => total + value, 0)
|
|
92
|
+
|
|
93
|
+
const weigh = (counts: Record<FindingSeverity, number>): number =>
|
|
94
|
+
counts.blocker * FINDING_WEIGHTS.blocker +
|
|
95
|
+
counts.should_fix * FINDING_WEIGHTS.should_fix +
|
|
96
|
+
counts.nit * FINDING_WEIGHTS.nit
|
|
97
|
+
|
|
98
|
+
const newTicket = (): TicketFacts => ({
|
|
99
|
+
models: {},
|
|
100
|
+
efforts: {},
|
|
101
|
+
review_effort: EFFORT_BEFORE_659,
|
|
102
|
+
picks: {},
|
|
103
|
+
effort_picks: {},
|
|
104
|
+
reviews: 0,
|
|
105
|
+
findings: 0,
|
|
106
|
+
fix_rounds: 0,
|
|
107
|
+
joined: false,
|
|
108
|
+
lines: null,
|
|
109
|
+
stuck: false,
|
|
110
|
+
tokens: null,
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* What one run's records say, ticket by ticket. An agent with no model ran
|
|
115
|
+
* Opus (before #600), and one with no effort ran at `high` (before #659).
|
|
116
|
+
*/
|
|
117
|
+
export const factsOf = (records: JournalRecord[]): RunFacts => {
|
|
118
|
+
const tickets = new Map<number, TicketFacts>()
|
|
119
|
+
const ticketOf = (number: number): TicketFacts => {
|
|
120
|
+
const found = tickets.get(number) ?? newTicket()
|
|
121
|
+
tickets.set(number, found)
|
|
122
|
+
return found
|
|
123
|
+
}
|
|
124
|
+
const asks = new Map<number, { role: string | null; shadow: boolean }>()
|
|
125
|
+
const effort_asks = new Map<number, string | null>()
|
|
126
|
+
const reviewed = new Set<number>()
|
|
127
|
+
const learner: LearnerRun[] = []
|
|
128
|
+
let bar = ModelsConfigSchema.parse({}).confidence_bar
|
|
129
|
+
let routed = false
|
|
130
|
+
let final_rounds = 0
|
|
131
|
+
let final_findings = 0
|
|
132
|
+
for (const record of records) {
|
|
133
|
+
switch (record.kind) {
|
|
134
|
+
case 'run_started':
|
|
135
|
+
bar = modelsOf({ config: record.content.config }).confidence_bar
|
|
136
|
+
break
|
|
137
|
+
case 'jev_asked':
|
|
138
|
+
if (record.content.job === 'agent_effort') {
|
|
139
|
+
effort_asks.set(record.seq, record.role)
|
|
140
|
+
}
|
|
141
|
+
if (
|
|
142
|
+
record.content.job === 'agent_model' ||
|
|
143
|
+
record.content.job === 'ticket_model'
|
|
144
|
+
) {
|
|
145
|
+
asks.set(record.seq, {
|
|
146
|
+
role: record.role,
|
|
147
|
+
shadow:
|
|
148
|
+
record.content.job === 'ticket_model' ||
|
|
149
|
+
!record.content.acted,
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
break
|
|
153
|
+
case 'jev_answered': {
|
|
154
|
+
const effort_role = effort_asks.get(record.content.asked_seq)
|
|
155
|
+
if (effort_role !== undefined && record.ticket !== null) {
|
|
156
|
+
const answer = record.content.answers.effort
|
|
157
|
+
const level = EffortLevelSchema.safeParse(answer?.value)
|
|
158
|
+
const confidence = answer?.confidence ?? null
|
|
159
|
+
if (
|
|
160
|
+
isScored(effort_role) &&
|
|
161
|
+
level.success &&
|
|
162
|
+
confidence !== null &&
|
|
163
|
+
confidence >= bar
|
|
164
|
+
) {
|
|
165
|
+
ticketOf(record.ticket).effort_picks[effort_role] ??=
|
|
166
|
+
level.data
|
|
167
|
+
}
|
|
168
|
+
break
|
|
169
|
+
}
|
|
170
|
+
const ask = asks.get(record.content.asked_seq)
|
|
171
|
+
if (ask === undefined || record.ticket === null) break
|
|
172
|
+
const answer = record.content.answers.model
|
|
173
|
+
const pick: Pick = {
|
|
174
|
+
model: familyOf(answer?.value),
|
|
175
|
+
confidence: answer?.confidence ?? null,
|
|
176
|
+
shadow: ask.shadow,
|
|
177
|
+
}
|
|
178
|
+
const roles =
|
|
179
|
+
record.content.job === 'ticket_model'
|
|
180
|
+
? SCORED_ROLES
|
|
181
|
+
: SCORED_ROLES.filter((role) => role === ask.role)
|
|
182
|
+
const facts = ticketOf(record.ticket)
|
|
183
|
+
for (const role of roles) facts.picks[role] ??= pick
|
|
184
|
+
break
|
|
185
|
+
}
|
|
186
|
+
case 'agent_started': {
|
|
187
|
+
const { role, model, effort, follow_up_of } = record.content
|
|
188
|
+
const family = familyOf(model) ?? 'opus'
|
|
189
|
+
const level = effort ?? EFFORT_BEFORE_659
|
|
190
|
+
if (follow_up_of !== null) break
|
|
191
|
+
if (role === 'learner') {
|
|
192
|
+
learner.push({ model: family, effort: level })
|
|
193
|
+
}
|
|
194
|
+
if (isScored(role) || role === 'learner') {
|
|
195
|
+
routed ||= family !== 'opus'
|
|
196
|
+
}
|
|
197
|
+
if (record.ticket === null) break
|
|
198
|
+
if (isScored(role)) {
|
|
199
|
+
const facts = ticketOf(record.ticket)
|
|
200
|
+
facts.models[role] ??= family
|
|
201
|
+
facts.efforts[role] ??= level
|
|
202
|
+
}
|
|
203
|
+
if (
|
|
204
|
+
role === 'ticket-reviewer' &&
|
|
205
|
+
!reviewed.has(record.ticket)
|
|
206
|
+
) {
|
|
207
|
+
reviewed.add(record.ticket)
|
|
208
|
+
ticketOf(record.ticket).review_effort = level
|
|
209
|
+
}
|
|
210
|
+
break
|
|
211
|
+
}
|
|
212
|
+
case 'agent_finished': {
|
|
213
|
+
const { content } = record
|
|
214
|
+
if (
|
|
215
|
+
content.role !== 'ticket-reviewer' ||
|
|
216
|
+
record.ticket === null
|
|
217
|
+
) {
|
|
218
|
+
break
|
|
219
|
+
}
|
|
220
|
+
const facts = ticketOf(record.ticket)
|
|
221
|
+
if (facts.reviews === 0) {
|
|
222
|
+
facts.findings = sum(
|
|
223
|
+
content.result.findings.map(
|
|
224
|
+
({ severity }) => FINDING_WEIGHTS[severity]
|
|
225
|
+
)
|
|
226
|
+
)
|
|
227
|
+
}
|
|
228
|
+
facts.reviews += 1
|
|
229
|
+
if (content.result.verdict === 'changes_requested') {
|
|
230
|
+
facts.fix_rounds += 1
|
|
231
|
+
}
|
|
232
|
+
break
|
|
233
|
+
}
|
|
234
|
+
case 'ticket_joined':
|
|
235
|
+
if (record.ticket !== null && record.content.ok) {
|
|
236
|
+
const facts = ticketOf(record.ticket)
|
|
237
|
+
facts.joined = true
|
|
238
|
+
facts.lines = record.content.lines_changed
|
|
239
|
+
}
|
|
240
|
+
break
|
|
241
|
+
case 'ticket_stuck':
|
|
242
|
+
if (record.ticket !== null) ticketOf(record.ticket).stuck = true
|
|
243
|
+
break
|
|
244
|
+
case 'usage_recorded': {
|
|
245
|
+
const { scope, ticket, tokens } = record.content
|
|
246
|
+
if (scope === 'ticket' && ticket !== null) {
|
|
247
|
+
ticketOf(ticket).tokens = countedTokens({ tokens })
|
|
248
|
+
}
|
|
249
|
+
break
|
|
250
|
+
}
|
|
251
|
+
case 'final_review_started':
|
|
252
|
+
final_rounds += 1
|
|
253
|
+
break
|
|
254
|
+
case 'lens_finished':
|
|
255
|
+
if (final_rounds === 1) {
|
|
256
|
+
final_findings += weigh(record.content.findings)
|
|
257
|
+
}
|
|
258
|
+
break
|
|
259
|
+
default:
|
|
260
|
+
break
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const joined = [...tickets.values()].filter(({ joined }) => joined)
|
|
264
|
+
return {
|
|
265
|
+
tickets: [...tickets.values()],
|
|
266
|
+
routed,
|
|
267
|
+
learner,
|
|
268
|
+
final_findings: final_rounds > 0 ? final_findings : null,
|
|
269
|
+
lines: joined.some(({ lines }) => lines === null)
|
|
270
|
+
? null
|
|
271
|
+
: sum(joined.map(({ lines }) => lines ?? 0)),
|
|
272
|
+
}
|
|
273
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { FindingSeverity } from '../agents/role-results'
|
|
2
|
+
import type { EffortLevel } from '../config/engine-config'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The model score report's constants (#605, #662): the finding weights, the
|
|
6
|
+
* verdict edges, the scored roles, and the effort an older journal's agent
|
|
7
|
+
* ran at. The one place they live.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** How much each ticket review finding weighs. */
|
|
11
|
+
export const FINDING_WEIGHTS: Record<FindingSeverity, number> = {
|
|
12
|
+
blocker: 3,
|
|
13
|
+
should_fix: 1,
|
|
14
|
+
nit: 0,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** The fewest tickets on each side before a verdict is given. */
|
|
18
|
+
export const MIN_TICKETS = 10
|
|
19
|
+
|
|
20
|
+
/** How far above the baseline a rate may be before it is "worse". */
|
|
21
|
+
export const WORSE_BY = 0.2
|
|
22
|
+
|
|
23
|
+
/** Rounding slack when a rate is compared with the baseline's. */
|
|
24
|
+
export const EPSILON = 1e-9
|
|
25
|
+
|
|
26
|
+
/** The roles scored by the ticket review's findings. */
|
|
27
|
+
export const SCORED_ROLES = ['test-writer', 'implementer'] as const
|
|
28
|
+
|
|
29
|
+
export type ScoredRole = (typeof SCORED_ROLES)[number]
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The effort an agent with no journaled effort ran at: every role ran at
|
|
33
|
+
* `high` before #659.
|
|
34
|
+
*/
|
|
35
|
+
export const EFFORT_BEFORE_659: EffortLevel = 'high'
|