@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
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
} from './execute-build'
|
|
16
16
|
import { closeSessions, openSessionsIn } from './session-close'
|
|
17
17
|
|
|
18
|
+
import { roleEffort } from '../agents/role-effort'
|
|
18
19
|
import { hasSlowTests } from '../config/engine-config'
|
|
19
20
|
import type { EngineCommit } from '../git/git-adapter'
|
|
20
21
|
import { LOCKED_PICK } from '../jev/model-pick'
|
|
@@ -137,6 +138,7 @@ export const executeFinalReviewAction = async ({
|
|
|
137
138
|
case 'launch_lens': {
|
|
138
139
|
const { lens, role, prompt } = action
|
|
139
140
|
const round = state.final_review.round
|
|
141
|
+
const effort = roleEffort({ role, config })
|
|
140
142
|
journal.append({
|
|
141
143
|
kind: 'lens_started',
|
|
142
144
|
ticket: null,
|
|
@@ -152,6 +154,7 @@ export const executeFinalReviewAction = async ({
|
|
|
152
154
|
prompt,
|
|
153
155
|
follow_up_of: null,
|
|
154
156
|
...LOCKED_PICK,
|
|
157
|
+
effort,
|
|
155
158
|
},
|
|
156
159
|
})
|
|
157
160
|
const result = await runTurn({
|
|
@@ -171,6 +174,7 @@ export const executeFinalReviewAction = async ({
|
|
|
171
174
|
// Lenses are reviewers: no agent messages.
|
|
172
175
|
messaging: null,
|
|
173
176
|
model: LOCKED_PICK.model,
|
|
177
|
+
effort,
|
|
174
178
|
}),
|
|
175
179
|
})
|
|
176
180
|
if (result === null || !('verdict' in result.result)) return
|
|
@@ -206,7 +210,9 @@ export const executeFinalReviewAction = async ({
|
|
|
206
210
|
return
|
|
207
211
|
case 'launch_final_fixer': {
|
|
208
212
|
const { role, prompt, may_edit_tests } = action
|
|
209
|
-
// The last fixes before the PR are never routed: always Opus
|
|
213
|
+
// The last fixes before the PR are never routed: always Opus,
|
|
214
|
+
// and at the reviewers' effort, so the last code is the cleanest.
|
|
215
|
+
const effort = roleEffort({ role, config, final_fix: true })
|
|
210
216
|
journal.append({
|
|
211
217
|
kind: 'agent_started',
|
|
212
218
|
ticket: null,
|
|
@@ -216,6 +222,7 @@ export const executeFinalReviewAction = async ({
|
|
|
216
222
|
prompt,
|
|
217
223
|
follow_up_of: null,
|
|
218
224
|
...LOCKED_PICK,
|
|
225
|
+
effort,
|
|
219
226
|
},
|
|
220
227
|
})
|
|
221
228
|
await runTurn({
|
|
@@ -235,13 +242,14 @@ export const executeFinalReviewAction = async ({
|
|
|
235
242
|
// Every ticket is over, so nobody is left to talk to.
|
|
236
243
|
messaging: null,
|
|
237
244
|
model: LOCKED_PICK.model,
|
|
245
|
+
effort,
|
|
238
246
|
}),
|
|
239
247
|
})
|
|
240
248
|
return
|
|
241
249
|
}
|
|
242
250
|
case 'follow_up_final_fixer': {
|
|
243
251
|
const { role, session_id, message } = action
|
|
244
|
-
const { model, model_reason } = followUpModel({
|
|
252
|
+
const { model, model_reason, effort } = followUpModel({
|
|
245
253
|
records: journal.read(),
|
|
246
254
|
session_id,
|
|
247
255
|
})
|
|
@@ -255,6 +263,7 @@ export const executeFinalReviewAction = async ({
|
|
|
255
263
|
follow_up_of: session_id,
|
|
256
264
|
model,
|
|
257
265
|
model_reason,
|
|
266
|
+
effort,
|
|
258
267
|
},
|
|
259
268
|
})
|
|
260
269
|
await runTurn({
|
|
@@ -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
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The cap on a final review role's failed tries (`failureCap`): the stuck step
|
|
3
|
-
* once a failed turn used up the role's tries, or its engine
|
|
3
|
+
* once a failed turn used up the role's tries in a row, or its engine
|
|
4
|
+
* failures in a row. A finished turn of the role gives its tries back.
|
|
4
5
|
*/
|
|
5
6
|
import { stuck, type FinalReviewAction } from './final-review-action'
|
|
6
7
|
import { MAX_ENGINE_FAILURES, MAX_FIX_ROUNDS } from './loop-caps'
|
|
@@ -8,8 +9,11 @@ import { MAX_ENGINE_FAILURES, MAX_FIX_ROUNDS } from './loop-caps'
|
|
|
8
9
|
import type { FinalReviewState, ReplayedAgentFailure } from '../journal/replay'
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
|
-
* The stuck step when a failed turn used up its role's tries (or
|
|
12
|
-
* failures in a row), else `null`: the role gets another go.
|
|
12
|
+
* The stuck step when a failed turn used up its role's tries in a row (or
|
|
13
|
+
* engine failures in a row), else `null`: the role gets another go.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* failureCap({ review, failed }) // null while the role has tries left
|
|
13
17
|
*/
|
|
14
18
|
export const failureCap = ({
|
|
15
19
|
review,
|
|
@@ -32,7 +36,7 @@ export const failureCap = ({
|
|
|
32
36
|
return tries >= MAX_FIX_ROUNDS
|
|
33
37
|
? stuck({
|
|
34
38
|
reason: 'agent_failed',
|
|
35
|
-
detail: `The ${role} failed ${tries} tries in the final review; the last one: ${error}`,
|
|
39
|
+
detail: `The ${role} failed ${tries} tries in a row in the final review; the last one: ${error}`,
|
|
36
40
|
})
|
|
37
41
|
: null
|
|
38
42
|
}
|
|
@@ -190,13 +190,27 @@ const findingsSection = ({
|
|
|
190
190
|
FIX_RULES,
|
|
191
191
|
].join('\n\n')
|
|
192
192
|
|
|
193
|
+
/** The round's failed slow tests, beside the implementer's code findings (#563). */
|
|
194
|
+
const slowTestsSection = ({
|
|
195
|
+
slow_tests,
|
|
196
|
+
}: {
|
|
197
|
+
slow_tests: ReplayedGates
|
|
198
|
+
}): string =>
|
|
199
|
+
[
|
|
200
|
+
'## The slow tests failed too',
|
|
201
|
+
'Once every lens finished, the engine ran the slow tests on the run branch, and these failed. Fix the code so they pass too: the gates run them again after you answer.',
|
|
202
|
+
failedChecks({ gates: slow_tests }),
|
|
203
|
+
].join('\n\n')
|
|
204
|
+
|
|
193
205
|
/**
|
|
194
206
|
* The prompt a fresh final review fixer starts with: its task, the spec,
|
|
195
207
|
* every ticket, and its findings (the test findings for the test-writer,
|
|
196
208
|
* the code findings for the implementer, or the findings the other fixer
|
|
197
|
-
* answered `other_role`, #616).
|
|
198
|
-
*
|
|
199
|
-
*
|
|
209
|
+
* answered `other_role`, #616). The implementer that starts a round whose
|
|
210
|
+
* slow tests failed (#563) gets their output beside its code findings. An
|
|
211
|
+
* implementer launched only to fix failed gates (the round had only test
|
|
212
|
+
* findings) gets the gates' output instead. The engine journals it word
|
|
213
|
+
* for word.
|
|
200
214
|
*
|
|
201
215
|
* @example
|
|
202
216
|
* const prompt = finalFixerPrompt({ role: 'implementer', snapshot, fix, gates: null })
|
|
@@ -206,6 +220,7 @@ export const finalFixerPrompt = ({
|
|
|
206
220
|
snapshot,
|
|
207
221
|
fix,
|
|
208
222
|
gates,
|
|
223
|
+
slow_tests,
|
|
209
224
|
run_notes,
|
|
210
225
|
sections,
|
|
211
226
|
}: {
|
|
@@ -214,6 +229,8 @@ export const finalFixerPrompt = ({
|
|
|
214
229
|
fix: FinalReviewFix
|
|
215
230
|
/** Failed gates to fix, for an implementer with no findings left. */
|
|
216
231
|
gates: ReplayedGates | null
|
|
232
|
+
/** The round's failed slow tests, for the implementer that starts it. */
|
|
233
|
+
slow_tests?: ReplayedGates | null
|
|
217
234
|
/** Notes earlier agents in the run left, oldest first. */
|
|
218
235
|
run_notes?: PromptRunNote[]
|
|
219
236
|
/** More sections after the task, such as a follow-up's message. */
|
|
@@ -239,6 +256,7 @@ export const finalFixerPrompt = ({
|
|
|
239
256
|
FIXER_TASKS[role],
|
|
240
257
|
...workSections({ snapshot }),
|
|
241
258
|
task,
|
|
259
|
+
...(slow_tests?.ok === false ? [slowTestsSection({ slow_tests })] : []),
|
|
242
260
|
...(sections ?? []),
|
|
243
261
|
...runNotesSection({ run_notes: run_notes ?? [] }),
|
|
244
262
|
].join('\n\n')
|
|
@@ -15,6 +15,7 @@ import { reasonLine } from './stuck-text'
|
|
|
15
15
|
import type { AgentRole, MergeDanger } from '../agents/role-results'
|
|
16
16
|
import {
|
|
17
17
|
EMPTY_FINAL_REVIEW,
|
|
18
|
+
finalPullRequestFindings,
|
|
18
19
|
type FinalReviewState,
|
|
19
20
|
type ReplayedSnapshot,
|
|
20
21
|
type TicketProgress,
|
|
@@ -114,7 +115,9 @@ const outOfTurnsSection = ({
|
|
|
114
115
|
* agents ran out of turns (#543), the reviews' nits,
|
|
115
116
|
* the findings declined through "won't fix", and the memories the learner
|
|
116
117
|
* added or updated (#370). A final review shipped
|
|
117
|
-
* while stuck puts its open findings at the very top
|
|
118
|
+
* while stuck puts its open findings at the very top; a round it never
|
|
119
|
+
* settled counts the findings, nits, and rulings of the lenses that
|
|
120
|
+
* finished it (see `finalPullRequestFindings`). Right after the
|
|
118
121
|
* "Built by" line come the Summary picture and the Merge Danger (from the
|
|
119
122
|
* final review's integration lens, or "Not available" without them) and
|
|
120
123
|
* the Evidence that each ticket's new tests failed first and pass now
|
|
@@ -137,6 +140,8 @@ export const pullRequestText = ({
|
|
|
137
140
|
memory_saves?: MemorySave[]
|
|
138
141
|
}): { title: string; body: string } => {
|
|
139
142
|
const review = final_review ?? EMPTY_FINAL_REVIEW
|
|
143
|
+
// A round a stuck lens never settled keeps its finished lenses' findings.
|
|
144
|
+
const findings = finalPullRequestFindings({ review })
|
|
140
145
|
const { spec } = snapshot
|
|
141
146
|
const title = (number: number) => snapshot.tickets[number]?.title ?? ''
|
|
142
147
|
const isSkipped = (number: number) =>
|
|
@@ -175,7 +180,7 @@ export const pullRequestText = ({
|
|
|
175
180
|
`- #${number} ${id}${fileText(file)}: ${title}`
|
|
176
181
|
)
|
|
177
182
|
),
|
|
178
|
-
...
|
|
183
|
+
...findings.nits.map(
|
|
179
184
|
({ lens, id, title, file }) =>
|
|
180
185
|
`- final review, ${lens} lens: ${id}${fileText(file)}: ${title}`
|
|
181
186
|
),
|
|
@@ -206,23 +211,22 @@ export const pullRequestText = ({
|
|
|
206
211
|
declinedLine({ where: `#${number} `, ...entry })
|
|
207
212
|
)
|
|
208
213
|
),
|
|
209
|
-
...
|
|
214
|
+
...findings.declined.map((entry) =>
|
|
210
215
|
declinedLine({
|
|
211
216
|
where: `final review, ${entry.lens} lens: `,
|
|
212
217
|
...entry,
|
|
213
218
|
})
|
|
214
219
|
),
|
|
215
220
|
]
|
|
216
|
-
const stillOpen = review.fix?.findings ?? []
|
|
217
221
|
const shipped =
|
|
218
222
|
review.shipped && review.stuck !== null
|
|
219
223
|
? shippedFindingsSection({
|
|
220
|
-
findings:
|
|
224
|
+
findings: findings.open,
|
|
221
225
|
stuck: review.stuck,
|
|
222
226
|
edits: review.shipped_edits,
|
|
223
227
|
})
|
|
224
|
-
: review.fresh_started &&
|
|
225
|
-
? freshFindingsSection({ findings:
|
|
228
|
+
: review.fresh_started && findings.open.length > 0
|
|
229
|
+
? freshFindingsSection({ findings: findings.open })
|
|
226
230
|
: ''
|
|
227
231
|
const body = [
|
|
228
232
|
shipped,
|
|
@@ -18,5 +18,17 @@ export const fenced = (text: string): string => {
|
|
|
18
18
|
return `${fence}\n${clipped}\n${fence}`
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
/**
|
|
22
|
+
* `count` and `word`, with `s` past one; a word ending in a consonant and
|
|
23
|
+
* `y` ends in `ies` instead.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* plural(2, 'fix round') // '2 fix rounds'
|
|
27
|
+
* plural(3, 'failed try') // '3 failed tries'
|
|
28
|
+
*/
|
|
29
|
+
export const plural = (count: number, word: string): string => {
|
|
30
|
+
if (count === 1) return `${count} ${word}`
|
|
31
|
+
return /[^aeiou]y$/.test(word)
|
|
32
|
+
? `${count} ${word.slice(0, -1)}ies`
|
|
33
|
+
: `${count} ${word}s`
|
|
34
|
+
}
|
|
@@ -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
|
|
|
@@ -143,7 +143,10 @@ export const finalReviewAfter = ({
|
|
|
143
143
|
return finalResultChange({
|
|
144
144
|
review: {
|
|
145
145
|
...review,
|
|
146
|
+
// A finished turn gives the role its tries back: the
|
|
147
|
+
// cap is on failed tries in a row.
|
|
146
148
|
agent_failures: omit(review.agent_failures, role),
|
|
149
|
+
failed_tries: omit(review.failed_tries, role),
|
|
147
150
|
engine_failures: omit(review.engine_failures, role),
|
|
148
151
|
sessions: sessionsAfter({
|
|
149
152
|
sessions: review.sessions,
|
|
@@ -13,7 +13,11 @@ import {
|
|
|
13
13
|
handoffRole,
|
|
14
14
|
type FinishedContent,
|
|
15
15
|
} from './agent-turn-replay'
|
|
16
|
-
import type {
|
|
16
|
+
import type {
|
|
17
|
+
FinalDeclined,
|
|
18
|
+
FinalFinding,
|
|
19
|
+
FinalReviewState,
|
|
20
|
+
} from './final-review-state'
|
|
17
21
|
import { setupChangeOf } from './ticket-result-replay'
|
|
18
22
|
|
|
19
23
|
import {
|
|
@@ -112,6 +116,21 @@ const lensResultOf = ({
|
|
|
112
116
|
}
|
|
113
117
|
}
|
|
114
118
|
|
|
119
|
+
/**
|
|
120
|
+
* The blocking findings of each due lens that finished this round, with
|
|
121
|
+
* its lens.
|
|
122
|
+
*/
|
|
123
|
+
const roundBlocking = ({
|
|
124
|
+
review,
|
|
125
|
+
}: {
|
|
126
|
+
review: FinalReviewState
|
|
127
|
+
}): FinalFinding[] =>
|
|
128
|
+
review.lenses_due.flatMap((lens) =>
|
|
129
|
+
(review.results[lens]?.findings ?? [])
|
|
130
|
+
.filter(isBlocking)
|
|
131
|
+
.map((entry) => ({ ...entry, lens }))
|
|
132
|
+
)
|
|
133
|
+
|
|
115
134
|
/**
|
|
116
135
|
* Every due lens finished this round, and its slow tests ran if they were
|
|
117
136
|
* due (#563): the round can settle.
|
|
@@ -161,9 +180,7 @@ export const settleRound = ({
|
|
|
161
180
|
result.findings.filter((entry) => !isBlocking(entry))
|
|
162
181
|
)
|
|
163
182
|
)
|
|
164
|
-
const blocking =
|
|
165
|
-
tagged(lens, result.findings.filter(isBlocking))
|
|
166
|
-
)
|
|
183
|
+
const blocking = roundBlocking({ review })
|
|
167
184
|
const settled: FinalReviewState = {
|
|
168
185
|
...review,
|
|
169
186
|
declined: [...review.declined, ...declined],
|
|
@@ -199,6 +216,51 @@ export const settleRound = ({
|
|
|
199
216
|
}
|
|
200
217
|
}
|
|
201
218
|
|
|
219
|
+
/** The final review's findings as its PR lists them. */
|
|
220
|
+
export type PullRequestFindings = {
|
|
221
|
+
/** The blocking findings still open. */
|
|
222
|
+
open: FinalFinding[]
|
|
223
|
+
nits: FinalFinding[]
|
|
224
|
+
declined: FinalDeclined[]
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* The final review's findings as its PR lists them: the blocking ones still
|
|
229
|
+
* open, the nits, and the "won't fix" answers a lens let go. A round that
|
|
230
|
+
* never settled (a lens failed on every try, or a step crashed, and the
|
|
231
|
+
* owner replied `ship`) is settled over the lenses that finished it, as
|
|
232
|
+
* `settleRound` would: their blocking findings, nits, and rulings count,
|
|
233
|
+
* and a finding one of them cleared is no longer open. The open fix round's
|
|
234
|
+
* findings stay open only for the lenses that never finished. A settled
|
|
235
|
+
* round's findings are as they stand.
|
|
236
|
+
*
|
|
237
|
+
* @example
|
|
238
|
+
* finalPullRequestFindings({ review }).open
|
|
239
|
+
* // [{ id: 'simplification-F1', lens: 'simplification', severity: 'should_fix', ... }]
|
|
240
|
+
*/
|
|
241
|
+
export const finalPullRequestFindings = ({
|
|
242
|
+
review,
|
|
243
|
+
}: {
|
|
244
|
+
review: FinalReviewState
|
|
245
|
+
}): PullRequestFindings => {
|
|
246
|
+
const earlier = review.fix?.findings ?? []
|
|
247
|
+
const finished = review.lenses_due.filter(
|
|
248
|
+
(lens) => review.results[lens] !== undefined
|
|
249
|
+
)
|
|
250
|
+
if (roundDone({ review }) || finished.length === 0) {
|
|
251
|
+
return { open: earlier, nits: review.nits, declined: review.declined }
|
|
252
|
+
}
|
|
253
|
+
const settled = settleRound({ review: { ...review, lenses_due: finished } })
|
|
254
|
+
return {
|
|
255
|
+
open: [
|
|
256
|
+
...earlier.filter(({ lens }) => !finished.includes(lens)),
|
|
257
|
+
...(settled.fix?.findings ?? []),
|
|
258
|
+
],
|
|
259
|
+
nits: settled.nits,
|
|
260
|
+
declined: settled.declined,
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
202
264
|
/**
|
|
203
265
|
* Where a finished agent's result goes in the final review: a lens's
|
|
204
266
|
* result this round (settling the round once every due lens finished), or
|
|
@@ -72,7 +72,10 @@ export type FinalReviewState = {
|
|
|
72
72
|
sessions: Partial<Record<AgentRole, string>>
|
|
73
73
|
/** Each role's failed turn, if no agent of that role finished after it. */
|
|
74
74
|
agent_failures: Partial<Record<AgentRole, ReplayedAgentFailure>>
|
|
75
|
-
/**
|
|
75
|
+
/**
|
|
76
|
+
* Failed tries (agent, result, guard) per role, in a row: a finished
|
|
77
|
+
* turn of the role starts its count again.
|
|
78
|
+
*/
|
|
76
79
|
failed_tries: Partial<Record<AgentRole, number>>
|
|
77
80
|
/** Engine failures in a row, per role. */
|
|
78
81
|
engine_failures: Partial<Record<AgentRole, number>>
|
package/engine/journal/replay.ts
CHANGED
|
@@ -71,7 +71,7 @@ export { EMPTY_FINAL_REVIEW } from './final-review-state'
|
|
|
71
71
|
export type { PlanState } from './plan-replay'
|
|
72
72
|
export type { LearnerState, MemoryState } from './memory-replay'
|
|
73
73
|
export { EMPTY_MEMORY } from './memory-replay'
|
|
74
|
-
export { namespacedId } from './final-review-round'
|
|
74
|
+
export { finalPullRequestFindings, namespacedId } from './final-review-round'
|
|
75
75
|
export { isFinalKey } from './final-review-replay'
|
|
76
76
|
export { handoffRole } from './agent-turn-replay'
|
|
77
77
|
|