@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,6 +1,7 @@
|
|
|
1
1
|
import type { MemoryAction, MemorySaveStep } from './decide-memory'
|
|
2
2
|
import { buildContext, need, runTurn, type BuildDeps } from './execute-build'
|
|
3
3
|
|
|
4
|
+
import { roleEffort } from '../agents/role-effort'
|
|
4
5
|
import { launchPick } from '../jev/model-pick'
|
|
5
6
|
import type { Journal } from '../journal/journal'
|
|
6
7
|
import type { JournalRecord } from '../journal/journal-record'
|
|
@@ -416,6 +417,10 @@ export const executeMemoryAction = async ({
|
|
|
416
417
|
role: 'learner',
|
|
417
418
|
step,
|
|
418
419
|
})
|
|
420
|
+
const effort = roleEffort({
|
|
421
|
+
role: 'learner',
|
|
422
|
+
config: built.config,
|
|
423
|
+
})
|
|
419
424
|
journal.append({
|
|
420
425
|
kind: 'agent_started',
|
|
421
426
|
ticket: null,
|
|
@@ -426,6 +431,7 @@ export const executeMemoryAction = async ({
|
|
|
426
431
|
follow_up_of: null,
|
|
427
432
|
model,
|
|
428
433
|
model_reason,
|
|
434
|
+
effort,
|
|
429
435
|
},
|
|
430
436
|
})
|
|
431
437
|
await runTurn({
|
|
@@ -445,6 +451,7 @@ export const executeMemoryAction = async ({
|
|
|
445
451
|
// The learner writes nothing and talks to nobody.
|
|
446
452
|
messaging: null,
|
|
447
453
|
model,
|
|
454
|
+
effort,
|
|
448
455
|
}),
|
|
449
456
|
})
|
|
450
457
|
return
|
|
@@ -6,6 +6,8 @@ import { z } from 'zod'
|
|
|
6
6
|
|
|
7
7
|
import { ALLOW, deny, READ_TOOLS, type ToolDecision } from './guard-roles'
|
|
8
8
|
|
|
9
|
+
import type { EffortLevel } from '../config/engine-config'
|
|
10
|
+
|
|
9
11
|
/** The SDK tool that starts a helper. */
|
|
10
12
|
export const HELPER_TOOL = 'Agent'
|
|
11
13
|
|
|
@@ -15,6 +17,9 @@ export const HELPER_TYPE = 'searcher'
|
|
|
15
17
|
/** Helpers always run on the newest Haiku, by its short name. */
|
|
16
18
|
export const HELPER_MODEL = 'haiku'
|
|
17
19
|
|
|
20
|
+
/** Helpers always run at low effort (#659): a search needs little thought. */
|
|
21
|
+
export const HELPER_EFFORT: EffortLevel = 'low'
|
|
22
|
+
|
|
18
23
|
/** The only tools a helper has: it reads and searches, nothing else. */
|
|
19
24
|
export const HELPER_TOOLS = READ_TOOLS
|
|
20
25
|
|
|
@@ -26,6 +31,7 @@ export const SEARCHER = {
|
|
|
26
31
|
description:
|
|
27
32
|
'A read-only helper that searches the repo: finds files, symbols and callers, and reports what it found. It cannot edit files or run commands.',
|
|
28
33
|
model: HELPER_MODEL,
|
|
34
|
+
effort: HELPER_EFFORT,
|
|
29
35
|
tools: [...HELPER_TOOLS],
|
|
30
36
|
prompt: 'You are a read-only searcher. You may only read and search files in the current worktree with Read, Grep and Glob. Never try to change a file or run a command. Answer with what you found: file paths, line numbers and short excerpts, as briefly as the question allows.',
|
|
31
37
|
}
|
package/engine/index.ts
CHANGED
|
@@ -15,7 +15,6 @@ export {
|
|
|
15
15
|
type AgentQuerySession,
|
|
16
16
|
} from './agents/claude-launcher'
|
|
17
17
|
export {
|
|
18
|
-
AGENT_EFFORT,
|
|
19
18
|
agentEnv,
|
|
20
19
|
agentOptions,
|
|
21
20
|
BANNED_ENV,
|
|
@@ -32,6 +31,7 @@ export {
|
|
|
32
31
|
LUCA_SERVER,
|
|
33
32
|
sendMessageDescription,
|
|
34
33
|
} from './agents/message-tool'
|
|
34
|
+
export { REVIEW_EFFORT, ROLE_EFFORT, roleEffort } from './agents/role-effort'
|
|
35
35
|
export { roleInstructions } from './agents/role-instructions'
|
|
36
36
|
export {
|
|
37
37
|
rolePrompt,
|
package/engine/jev/jev-jobs.ts
CHANGED
|
@@ -10,8 +10,13 @@ import {
|
|
|
10
10
|
} from './jev-schemas'
|
|
11
11
|
import { offeredModels, type RoutedRole } from './model-pick'
|
|
12
12
|
|
|
13
|
+
import { ROLE_EFFORT, roleEffort } from '../agents/role-effort'
|
|
13
14
|
import type { Finding } from '../agents/role-results'
|
|
14
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
DEFAULT_MODEL,
|
|
17
|
+
modelsOf,
|
|
18
|
+
type EffortLevel,
|
|
19
|
+
} from '../config/engine-config'
|
|
15
20
|
import type { EngineAction } from '../core/decide'
|
|
16
21
|
import type { TicketSnapshot } from '../intake/intake-schemas'
|
|
17
22
|
import type { JournalRecord } from '../journal/journal-record'
|
|
@@ -210,6 +215,51 @@ const agentModelAsk = ({
|
|
|
210
215
|
}
|
|
211
216
|
}
|
|
212
217
|
|
|
218
|
+
/** The efforts an `agent_effort` ask offers, lowest first. */
|
|
219
|
+
const OFFERED_EFFORTS: readonly EffortLevel[] = ['medium', 'high', 'xhigh']
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Which effort a fresh test-writer or implementer on a ticket should use
|
|
223
|
+
* (#661), with the same facts as its `agent_model` ask. Shadow mode only:
|
|
224
|
+
* the agent always runs at its role's effort.
|
|
225
|
+
*/
|
|
226
|
+
const agentEffortAsk = ({
|
|
227
|
+
state,
|
|
228
|
+
ticket,
|
|
229
|
+
role,
|
|
230
|
+
attempt,
|
|
231
|
+
}: {
|
|
232
|
+
state: RunState
|
|
233
|
+
ticket: number
|
|
234
|
+
role: RoutedRole
|
|
235
|
+
attempt: number
|
|
236
|
+
}): JevAsk => ({
|
|
237
|
+
job: 'agent_effort',
|
|
238
|
+
ticket,
|
|
239
|
+
role,
|
|
240
|
+
request: {
|
|
241
|
+
state: {
|
|
242
|
+
...ticketState({ state, ticket }),
|
|
243
|
+
role,
|
|
244
|
+
try: String(attempt),
|
|
245
|
+
},
|
|
246
|
+
questions: {
|
|
247
|
+
effort: choice({
|
|
248
|
+
instructions:
|
|
249
|
+
'Which effort should this agent use? Pick the lowest that will get it right.',
|
|
250
|
+
options: OFFERED_EFFORTS,
|
|
251
|
+
}),
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
fixed: {
|
|
255
|
+
effort:
|
|
256
|
+
state.config === null
|
|
257
|
+
? ROLE_EFFORT[role]
|
|
258
|
+
: roleEffort({ role, config: state.config }),
|
|
259
|
+
},
|
|
260
|
+
acted: false,
|
|
261
|
+
})
|
|
262
|
+
|
|
213
263
|
const agentSkillsAsk = ({
|
|
214
264
|
state,
|
|
215
265
|
ticket,
|
|
@@ -250,8 +300,9 @@ const isRoutedTicketRole = (role: string): role is RoutedRole =>
|
|
|
250
300
|
|
|
251
301
|
/**
|
|
252
302
|
* The asks to make before the engine carries out `action`: ticket order
|
|
253
|
-
* before a ticket starts, skills before each agent,
|
|
254
|
-
*
|
|
303
|
+
* before a ticket starts, skills before each agent, the model before each
|
|
304
|
+
* fresh test-writer, implementer, and learner, and the effort before each
|
|
305
|
+
* fresh test-writer and implementer.
|
|
255
306
|
*/
|
|
256
307
|
export const jevAsksBefore = ({
|
|
257
308
|
action,
|
|
@@ -265,16 +316,12 @@ export const jevAsksBefore = ({
|
|
|
265
316
|
return createTicketWorktreeAsks({ state, ticket: action.ticket })
|
|
266
317
|
case 'launch_agent': {
|
|
267
318
|
const { ticket, role } = action
|
|
268
|
-
const
|
|
319
|
+
const attempt = (state.tickets[ticket]?.failed_tries[role] ?? 0) + 1
|
|
269
320
|
return [
|
|
270
321
|
...(isRoutedTicketRole(role)
|
|
271
322
|
? [
|
|
272
|
-
agentModelAsk({
|
|
273
|
-
|
|
274
|
-
ticket,
|
|
275
|
-
role,
|
|
276
|
-
attempt: failed + 1,
|
|
277
|
-
}),
|
|
323
|
+
agentModelAsk({ state, ticket, role, attempt }),
|
|
324
|
+
agentEffortAsk({ state, ticket, role, attempt }),
|
|
278
325
|
]
|
|
279
326
|
: []),
|
|
280
327
|
agentSkillsAsk({ state, ticket, role }),
|
|
@@ -100,11 +100,13 @@ export type JevAnswer = z.infer<typeof JevAnswerSchema>
|
|
|
100
100
|
/**
|
|
101
101
|
* The jobs the engine asks Jev about. `ticket_model` (one model per ticket)
|
|
102
102
|
* is no longer asked; `agent_model` (one per agent) replaced it in #602. It
|
|
103
|
-
* stays so old journals read.
|
|
103
|
+
* stays so old journals read. `agent_effort` is only ever asked in shadow
|
|
104
|
+
* mode (#661).
|
|
104
105
|
*/
|
|
105
106
|
export const JevJobSchema = z.enum([
|
|
106
107
|
'ticket_model',
|
|
107
108
|
'agent_model',
|
|
109
|
+
'agent_effort',
|
|
108
110
|
'agent_skills',
|
|
109
111
|
'ticket_order',
|
|
110
112
|
'failure_kind',
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
FileModeSchema,
|
|
9
9
|
RoleResultSchema,
|
|
10
10
|
} from '../agents/role-results'
|
|
11
|
+
import { EffortLevelSchema } from '../config/engine-config'
|
|
11
12
|
import { GateCheckSchema } from '../gates/gate-schemas'
|
|
12
13
|
import { ModelReasonSchema } from '../jev/model-pick'
|
|
13
14
|
|
|
@@ -23,7 +24,9 @@ import { ModelReasonSchema } from '../jev/model-pick'
|
|
|
23
24
|
* follow-up's is its session's); `null` in journals from before #600. The
|
|
24
25
|
* exact version is in the turn's `agent_session`. `model_reason` is why it
|
|
25
26
|
* launched on that model (#603; a follow-up's is its session's); `null` in
|
|
26
|
-
* journals from before it.
|
|
27
|
+
* journals from before it. `effort` is the effort it ran at (#659; a
|
|
28
|
+
* follow-up's is its session's); `null` in journals from before it, when
|
|
29
|
+
* every agent ran at `high`.
|
|
27
30
|
*/
|
|
28
31
|
export const AgentStartedEntrySchema = z.object({
|
|
29
32
|
...ENTRY_FIELDS,
|
|
@@ -34,6 +37,7 @@ export const AgentStartedEntrySchema = z.object({
|
|
|
34
37
|
follow_up_of: z.string().nullable().default(null),
|
|
35
38
|
model: z.string().nullable().default(null),
|
|
36
39
|
model_reason: ModelReasonSchema.nullable().default(null),
|
|
40
|
+
effort: EffortLevelSchema.nullable().default(null),
|
|
37
41
|
}),
|
|
38
42
|
})
|
|
39
43
|
|
|
@@ -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'
|