@alecsibilia/luca 14.0.0-alpha.2 → 14.0.0-alpha.21

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.
Files changed (321) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/README.md +7 -2
  3. package/board/client/board-look.ts +31 -1
  4. package/board/client/board-panel-parts.tsx +150 -0
  5. package/board/client/board-panel-stages.ts +99 -0
  6. package/board/client/board-panel-styles.ts +223 -0
  7. package/board/client/board-panel.tsx +77 -1275
  8. package/board/client/engine-life.ts +48 -0
  9. package/board/client/final-review-stack.tsx +144 -0
  10. package/board/client/heartbeat.tsx +61 -0
  11. package/board/client/helper-lines.tsx +69 -0
  12. package/board/client/live-feed.tsx +86 -0
  13. package/board/client/live-line.ts +73 -0
  14. package/board/client/model-line.tsx +178 -0
  15. package/board/client/needs-you-section.tsx +133 -0
  16. package/board/client/one-line.tsx +193 -0
  17. package/board/client/repo-wait-banner.tsx +69 -0
  18. package/board/client/run-footer.tsx +68 -0
  19. package/board/client/run-header.tsx +189 -0
  20. package/board/client/run-picker.tsx +98 -0
  21. package/board/client/status-bar.tsx +131 -0
  22. package/board/client/step-dots.tsx +127 -0
  23. package/board/client/stuck-actions.tsx +269 -0
  24. package/board/client/ticket-card-view.tsx +238 -0
  25. package/board/client/ticket-detail.tsx +87 -0
  26. package/board/client/ticket-stack.tsx +119 -0
  27. package/board/client/ticket-wait.ts +188 -0
  28. package/board/client/timeline-rows.tsx +41 -14
  29. package/board/client/usage-line.tsx +51 -0
  30. package/board/client/wait-line.tsx +39 -0
  31. package/board/index.client.tsx +1 -1
  32. package/board/index.server.ts +7 -1
  33. package/board/package.json +1 -1
  34. package/board/server/board-panel-state-helpers.ts +67 -0
  35. package/board/server/board-server-check.ts +294 -0
  36. package/board/server/board-server-engine.ts +129 -0
  37. package/board/server/board-server-events.ts +134 -0
  38. package/board/server/board-server-read.ts +190 -0
  39. package/board/server/board-server-resume.ts +164 -0
  40. package/board/server/board-server-start.ts +174 -0
  41. package/board/server/board-server-store.ts +281 -0
  42. package/board/server/board-server-types.ts +114 -0
  43. package/board/server/board-server-util.ts +4 -0
  44. package/board/server/board-server.ts +52 -943
  45. package/board/server/board-vocabulary-engine.ts +144 -0
  46. package/board/server/board-vocabulary-review.ts +115 -0
  47. package/board/server/board-vocabulary-run.ts +145 -0
  48. package/board/server/board-vocabulary-shapes.ts +153 -0
  49. package/board/server/board-vocabulary.ts +20 -375
  50. package/board/server/current-step-panel-helpers.ts +108 -0
  51. package/board/server/describe-record.ts +403 -0
  52. package/board/server/engine-launch.ts +20 -3
  53. package/board/server/engine-restart-helpers.ts +105 -0
  54. package/board/server/engine-watch.ts +45 -5
  55. package/board/server/journal-rebuild-helpers.ts +169 -0
  56. package/board/server/live-file.ts +130 -0
  57. package/board/server/luca-version.ts +1 -1
  58. package/board/server/make-rows.ts +14 -629
  59. package/board/server/record-texts.ts +350 -0
  60. package/board/server/reduce-board-agent-start.ts +224 -0
  61. package/board/server/reduce-board-agents.ts +160 -0
  62. package/board/server/reduce-board-counters.ts +138 -0
  63. package/board/server/reduce-board-create.ts +148 -0
  64. package/board/server/reduce-board-final-review.ts +174 -0
  65. package/board/server/reduce-board-needs-you.ts +283 -0
  66. package/board/server/reduce-board-reasons.ts +59 -0
  67. package/board/server/reduce-board-run.ts +255 -0
  68. package/board/server/reduce-board-settle.ts +88 -0
  69. package/board/server/reduce-board-steps.ts +226 -0
  70. package/board/server/reduce-board-text.ts +139 -0
  71. package/board/server/reduce-board-ticket-checks.ts +168 -0
  72. package/board/server/reduce-board-ticket-join.ts +144 -0
  73. package/board/server/reduce-board-ticket-start.ts +132 -0
  74. package/board/server/reduce-board-update.ts +117 -0
  75. package/board/server/reduce-board-usage.ts +247 -0
  76. package/board/server/reduce-board.ts +100 -1917
  77. package/board/server/reply-post.ts +407 -0
  78. package/board/server/run-registry.ts +8 -4
  79. package/board/server/ticket-stages-panel-helpers.ts +296 -0
  80. package/board/shared/board-rows.ts +9 -0
  81. package/board/shared/board-rpc.ts +42 -0
  82. package/board/shared/board-state-counts.ts +77 -0
  83. package/board/shared/board-state-live.ts +99 -0
  84. package/board/shared/board-state-review.ts +59 -0
  85. package/board/shared/board-state-run.ts +236 -0
  86. package/board/shared/board-state-ticket.ts +257 -0
  87. package/board/shared/board-state-usage.ts +107 -0
  88. package/board/shared/board-state.ts +97 -567
  89. package/board/shared/reply-actions.ts +207 -0
  90. package/engine/agents/agent-launcher.ts +27 -4
  91. package/engine/agents/claude-launcher-helpers.ts +233 -0
  92. package/engine/agents/claude-launcher.ts +28 -669
  93. package/engine/agents/claude-messages.ts +212 -0
  94. package/engine/agents/claude-options.ts +43 -8
  95. package/engine/agents/common-instructions.ts +138 -0
  96. package/engine/agents/input-channel.ts +36 -0
  97. package/engine/agents/launch-paths.ts +29 -0
  98. package/engine/agents/launch-session.ts +227 -0
  99. package/engine/agents/learner-instructions.ts +31 -0
  100. package/engine/agents/open-session.ts +210 -0
  101. package/engine/agents/prompt-sections.ts +95 -0
  102. package/engine/agents/rejoin-prompts.ts +135 -0
  103. package/engine/agents/reviewer-instructions.ts +99 -0
  104. package/engine/agents/role-instructions.ts +31 -244
  105. package/engine/agents/role-prompts.ts +39 -109
  106. package/engine/agents/role-results.ts +106 -7
  107. package/engine/agents/scripted-launcher.ts +21 -6
  108. package/engine/agents/session-pool.ts +124 -0
  109. package/engine/agents/turn-progress.ts +283 -0
  110. package/engine/agents/writer-instructions.ts +151 -0
  111. package/engine/cli/computer-adapters-real.ts +15 -3
  112. package/engine/cli/computer-adapters.ts +2 -0
  113. package/engine/cli/computer-checks.ts +87 -4
  114. package/engine/cli/config-reload-text.ts +39 -0
  115. package/engine/cli/decision-model-setup.ts +70 -0
  116. package/engine/cli/doctor-command.ts +1 -0
  117. package/engine/cli/doctor.ts +21 -3
  118. package/engine/cli/going-runs.ts +192 -36
  119. package/engine/cli/init-command.ts +1 -0
  120. package/engine/cli/init.ts +23 -1
  121. package/engine/cli/live-runs.ts +73 -0
  122. package/engine/cli/luca-run.ts +42 -7
  123. package/engine/cli/luca-skills.ts +183 -0
  124. package/engine/cli/luca.ts +8 -1
  125. package/engine/cli/models-command.ts +119 -0
  126. package/engine/cli/read-run.ts +81 -0
  127. package/engine/cli/run-args.ts +33 -6
  128. package/engine/cli/run-demo.ts +143 -0
  129. package/engine/cli/run-end.ts +184 -0
  130. package/engine/cli/run-modes-helpers.ts +81 -0
  131. package/engine/cli/run-modes.ts +62 -439
  132. package/engine/cli/setup.ts +5 -48
  133. package/engine/cli/unfinished-runs.ts +122 -0
  134. package/engine/cli/upgrade-command.ts +212 -30
  135. package/engine/cli/upgrade-finish.ts +149 -0
  136. package/engine/cli/upgrade.ts +112 -71
  137. package/engine/config/config-fields.ts +127 -0
  138. package/engine/config/config-reload.ts +250 -0
  139. package/engine/config/engine-config-schema.ts +235 -0
  140. package/engine/config/engine-config.ts +72 -142
  141. package/engine/config/test-commands.ts +102 -0
  142. package/engine/core/agent-guards-helpers.ts +84 -0
  143. package/engine/core/agent-turn.ts +395 -0
  144. package/engine/core/already-done.ts +44 -1
  145. package/engine/core/build-action.ts +214 -0
  146. package/engine/core/build-context.ts +135 -0
  147. package/engine/core/build-launch.ts +259 -0
  148. package/engine/core/commit-in.ts +282 -0
  149. package/engine/core/decide-build-early.ts +108 -0
  150. package/engine/core/decide-build-ending.ts +145 -0
  151. package/engine/core/decide-build-helpers.ts +87 -0
  152. package/engine/core/decide-build-tickets.ts +207 -0
  153. package/engine/core/decide-build-view.ts +103 -0
  154. package/engine/core/decide-build.ts +101 -1368
  155. package/engine/core/decide-final-fix.ts +203 -0
  156. package/engine/core/decide-final-review.ts +55 -310
  157. package/engine/core/decide-red-green.ts +398 -0
  158. package/engine/core/decide-rejoin.ts +259 -0
  159. package/engine/core/decide-repo-wait.ts +129 -0
  160. package/engine/core/decide-run-budget.ts +86 -12
  161. package/engine/core/decide-stuck-helpers.ts +127 -0
  162. package/engine/core/decide-stuck.ts +60 -8
  163. package/engine/core/decide-ticket-review.ts +95 -0
  164. package/engine/core/decide-ticket.ts +267 -0
  165. package/engine/core/decide-waits.ts +47 -0
  166. package/engine/core/decide.ts +74 -23
  167. package/engine/core/engine-chores.ts +163 -0
  168. package/engine/core/environment-rerun.ts +83 -0
  169. package/engine/core/evidence-text.ts +84 -0
  170. package/engine/core/execute-action.ts +303 -0
  171. package/engine/core/execute-already-done-check.ts +133 -0
  172. package/engine/core/execute-build.ts +68 -944
  173. package/engine/core/execute-changeset.ts +5 -7
  174. package/engine/core/execute-final-review.ts +125 -13
  175. package/engine/core/execute-gates.ts +182 -0
  176. package/engine/core/execute-intake.ts +130 -0
  177. package/engine/core/execute-join.ts +276 -0
  178. package/engine/core/execute-memory.ts +12 -0
  179. package/engine/core/execute-pull-request.ts +84 -0
  180. package/engine/core/execute-red-check.ts +219 -0
  181. package/engine/core/execute-repo-wait.ts +129 -0
  182. package/engine/core/execute-stuck.ts +25 -10
  183. package/engine/core/execute-waits.ts +158 -0
  184. package/engine/core/execute-with-jev.ts +118 -0
  185. package/engine/core/execute.ts +124 -831
  186. package/engine/core/final-failure-cap.ts +38 -0
  187. package/engine/core/final-review-action.ts +125 -0
  188. package/engine/core/final-review-text.ts +63 -17
  189. package/engine/core/final-stuck-text.ts +86 -0
  190. package/engine/core/fix-loop-text.ts +49 -2
  191. package/engine/core/left-out.ts +120 -0
  192. package/engine/core/live-file.ts +372 -0
  193. package/engine/core/loop-caps.ts +25 -2
  194. package/engine/core/not-in-run.ts +7 -3
  195. package/engine/core/pull-request-text.ts +255 -6
  196. package/engine/core/repo-wait-text.ts +81 -0
  197. package/engine/core/review-text.ts +55 -1
  198. package/engine/core/run-crash-recovery-helpers.ts +16 -0
  199. package/engine/core/run-session-close-helpers.ts +96 -0
  200. package/engine/core/run-start.ts +148 -0
  201. package/engine/core/run-stuck-text.ts +86 -0
  202. package/engine/core/schedule-rules.ts +193 -0
  203. package/engine/core/stuck-detail-text.ts +89 -0
  204. package/engine/core/stuck-format.ts +22 -0
  205. package/engine/core/stuck-text.ts +29 -332
  206. package/engine/core/ticket-stuck-text.ts +192 -0
  207. package/engine/core/worktree-changes.ts +106 -0
  208. package/engine/gates/already-done-check.ts +120 -0
  209. package/engine/gates/feature-map.ts +96 -0
  210. package/engine/gates/gate-runner.ts +108 -108
  211. package/engine/gates/gate-schemas.ts +61 -2
  212. package/engine/gates/leftover-scan.ts +154 -31
  213. package/engine/gates/new-tests.ts +45 -0
  214. package/engine/gates/own-files-check.ts +98 -0
  215. package/engine/gates/prepare-check.ts +60 -0
  216. package/engine/gates/prepare-slots.ts +99 -0
  217. package/engine/gates/red-check.ts +184 -16
  218. package/engine/gates/shell-check.ts +64 -0
  219. package/engine/gates/slot-waits.ts +55 -0
  220. package/engine/gates/slow-test-runner.ts +59 -0
  221. package/engine/gates/test-checks.ts +161 -0
  222. package/engine/gates/test-runner.ts +164 -5
  223. package/engine/gates/test-slots.ts +48 -0
  224. package/engine/git/git-adapter.ts +55 -396
  225. package/engine/git/git-commits.ts +132 -0
  226. package/engine/git/git-join.ts +139 -0
  227. package/engine/git/git-read.ts +123 -0
  228. package/engine/git/git-run.ts +32 -0
  229. package/engine/git/git-worktrees.ts +143 -0
  230. package/engine/git/outside-links.ts +55 -0
  231. package/engine/guards/after-turn-check.ts +31 -5
  232. package/engine/guards/guard-hook.ts +60 -12
  233. package/engine/guards/guard-roles.ts +68 -0
  234. package/engine/guards/helper-rules.ts +92 -0
  235. package/engine/guards/permission-rules.ts +161 -0
  236. package/engine/guards/role-rules-helpers.ts +48 -0
  237. package/engine/guards/role-rules.ts +34 -606
  238. package/engine/guards/shell-rules.ts +344 -0
  239. package/engine/guards/worktree-git.ts +208 -0
  240. package/engine/guards/worktree-snapshot.ts +412 -0
  241. package/engine/guards/worktree-state.ts +28 -825
  242. package/engine/guards/worktree-undo.ts +260 -0
  243. package/engine/guards/write-rules.ts +149 -0
  244. package/engine/index.ts +30 -9
  245. package/engine/intake/criteria-checks.ts +114 -0
  246. package/engine/intake/intake-checks.ts +164 -30
  247. package/engine/intake/intake-schemas.ts +37 -2
  248. package/engine/jev/clef-client.ts +259 -0
  249. package/engine/jev/decision-model-credentials.ts +212 -0
  250. package/engine/jev/jev-jobs.ts +144 -60
  251. package/engine/jev/jev-schemas.ts +86 -5
  252. package/engine/jev/jev-shadow-helpers.ts +218 -0
  253. package/engine/jev/jev-shadow.ts +109 -64
  254. package/engine/jev/model-pick.ts +275 -0
  255. package/engine/journal/agent-entries.ts +167 -0
  256. package/engine/journal/agent-message-entries.ts +72 -0
  257. package/engine/journal/agent-turn-replay.ts +203 -0
  258. package/engine/journal/already-done-entries.ts +55 -0
  259. package/engine/journal/commit-entries.ts +56 -0
  260. package/engine/journal/cross-ticket-replay.ts +222 -0
  261. package/engine/journal/decision-model-entries.ts +86 -0
  262. package/engine/journal/entry-fields.ts +10 -0
  263. package/engine/journal/final-review-entries.ts +128 -0
  264. package/engine/journal/final-review-replay.ts +324 -0
  265. package/engine/journal/final-review-round.ts +320 -0
  266. package/engine/journal/final-review-state.ts +180 -0
  267. package/engine/journal/gate-entries.ts +81 -0
  268. package/engine/journal/intake-entries.ts +87 -0
  269. package/engine/journal/join-entries.ts +126 -0
  270. package/engine/journal/journal-entry-schema.ts +218 -0
  271. package/engine/journal/journal-kinds.ts +97 -0
  272. package/engine/journal/journal-record-schema.ts +218 -0
  273. package/engine/journal/journal-record.ts +43 -1400
  274. package/engine/journal/journal.ts +6 -1
  275. package/engine/journal/limit-entries.ts +90 -0
  276. package/engine/journal/memory-entries.ts +124 -0
  277. package/engine/journal/memory-replay.ts +157 -0
  278. package/engine/journal/plan-replay.ts +139 -0
  279. package/engine/journal/pull-request-entries.ts +41 -0
  280. package/engine/journal/replay.ts +157 -2144
  281. package/engine/journal/reply-replay.ts +144 -0
  282. package/engine/journal/repo-wait-entries.ts +53 -0
  283. package/engine/journal/run-entries.ts +172 -0
  284. package/engine/journal/run-state.ts +264 -0
  285. package/engine/journal/shared-failure-replay.ts +133 -0
  286. package/engine/journal/stuck-entries.ts +193 -0
  287. package/engine/journal/ticket-progress.ts +392 -0
  288. package/engine/journal/ticket-replay.ts +411 -0
  289. package/engine/journal/ticket-result-replay.ts +364 -0
  290. package/engine/journal/ticket-retry-replay.ts +142 -0
  291. package/engine/journal/worktree-entries.ts +122 -0
  292. package/engine/models/model-score.ts +512 -0
  293. package/engine/shell/run-command.ts +187 -18
  294. package/engine/testing/agent-turn-fixtures.ts +287 -0
  295. package/engine/testing/already-done-fixtures.ts +97 -0
  296. package/engine/testing/build-fixtures.ts +68 -973
  297. package/engine/testing/checkouts-fixtures.ts +48 -0
  298. package/engine/testing/final-review-fixtures.ts +62 -1
  299. package/engine/testing/final-review-steps.ts +143 -0
  300. package/engine/testing/limit-fixtures.ts +85 -0
  301. package/engine/testing/live-file-fixtures.ts +57 -0
  302. package/engine/testing/practice-repo.ts +10 -4
  303. package/engine/testing/practice-run.ts +1 -1
  304. package/engine/testing/reply-fixtures.ts +88 -0
  305. package/engine/testing/run-budget-fixtures.ts +165 -0
  306. package/engine/testing/run-fixtures.ts +219 -0
  307. package/engine/testing/sdk-play.ts +177 -0
  308. package/engine/testing/setup-repo.ts +194 -0
  309. package/engine/testing/step-record-fixtures.ts +55 -0
  310. package/engine/testing/ticket-step-fixtures.ts +272 -0
  311. package/engine/testing/wait-scenarios.ts +186 -0
  312. package/engine/tracker/gh-retry.ts +217 -0
  313. package/engine/tracker/github-tracker.ts +310 -90
  314. package/engine/tracker/in-memory-tracker.ts +8 -4
  315. package/engine/tracker/tracker.ts +131 -6
  316. package/package.json +2 -1
  317. package/skills/luca-retro/SKILL.md +74 -0
  318. package/skills/luca-retro/scripts/retro-summary.ts +1356 -0
  319. package/skills/luca-unstick/SKILL.md +153 -0
  320. package/skills/luca-unstick/scripts/stuck-summary.ts +1063 -0
  321. package/engine/jev/jev-client.ts +0 -173
@@ -0,0 +1,275 @@
1
+ import { z } from 'zod'
2
+
3
+ import type { AgentRole } from '../agents/role-results'
4
+ import {
5
+ DEFAULT_MODEL,
6
+ ModelNameSchema,
7
+ modelsOf,
8
+ ROUTED_ROLES,
9
+ type EngineConfig,
10
+ type ModelName,
11
+ type ModelsConfig,
12
+ } from '../config/engine-config'
13
+ import type { JournalRecord } from '../journal/journal-record'
14
+ import type { RunState } from '../journal/replay'
15
+
16
+ /**
17
+ * Which model each fresh agent launches on (#603): the pick rule, and the
18
+ * decision model's answer it reads from the journal.
19
+ */
20
+
21
+ /**
22
+ * Why an agent launched on its model:
23
+ * - `locked`: a reviewer, a lens, or a final-review fixer, always Opus.
24
+ * - `after_failure`: its role already failed a try on its ticket on a
25
+ * cheaper model (#604).
26
+ * - `floor`: its role's floor is Opus.
27
+ * - `no_answer`: the decision model gave no answer (off, timed out,
28
+ * errored, or the ask was skipped).
29
+ * - `unsure`: its confidence was below the bar or missing, or it picked a
30
+ * model the ask didn't offer.
31
+ * - `decision_model`: the decision model's pick.
32
+ */
33
+ export const ModelReasonSchema = z.enum([
34
+ 'locked',
35
+ 'after_failure',
36
+ 'floor',
37
+ 'no_answer',
38
+ 'unsure',
39
+ 'decision_model',
40
+ ])
41
+
42
+ export type ModelReason = z.infer<typeof ModelReasonSchema>
43
+
44
+ /** The model an agent launches on, and why. */
45
+ export type ModelPick = { model: ModelName; model_reason: ModelReason }
46
+
47
+ /** Reviewers, lenses, and the final review's fixers: always Opus. */
48
+ export const LOCKED_PICK: ModelPick = {
49
+ model: DEFAULT_MODEL,
50
+ model_reason: 'locked',
51
+ }
52
+
53
+ export type RoutedRole = (typeof ROUTED_ROLES)[number]
54
+
55
+ /** Whether a role has a model floor, so the decision model may pick its model. */
56
+ export const isRoutedRole = (role: string): role is RoutedRole =>
57
+ ROUTED_ROLES.some((routed) => routed === role)
58
+
59
+ /**
60
+ * The models an `agent_model` ask offers for a role with this floor: the
61
+ * floor and every stronger one. A floor of `opus` offers all three, since
62
+ * that ask is shadow only.
63
+ *
64
+ * @example
65
+ * offeredModels({ floor: 'sonnet' }) // ['opus', 'sonnet']
66
+ */
67
+ export const offeredModels = ({ floor }: { floor: ModelName }): ModelName[] => {
68
+ const all = ModelNameSchema.options
69
+ return floor === 'opus' ? [...all] : all.slice(0, all.indexOf(floor) + 1)
70
+ }
71
+
72
+ /** The decision model's answer to an `agent_model` ask, and what it offered. */
73
+ export type ModelAnswer = {
74
+ offered: string[]
75
+ pick: string | number | null
76
+ confidence: number | null
77
+ }
78
+
79
+ /**
80
+ * The model a fresh agent launches on. Pure; the first match wins: a
81
+ * reviewer or lens is `locked`; a role that failed a try on its ticket on a
82
+ * cheaper model is `after_failure`; a floor of `opus` is `floor`; no answer
83
+ * is `no_answer`; an answer below the bar, with no confidence, or not
84
+ * offered is `unsure`; else the decision model's pick. Every fallback is
85
+ * Opus.
86
+ *
87
+ * @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.8 bar
90
+ */
91
+ export const pickModel = ({
92
+ role,
93
+ models,
94
+ failed_cheaper,
95
+ answer,
96
+ }: {
97
+ role: AgentRole
98
+ models: ModelsConfig
99
+ /** Whether the role already failed a try on its ticket on a cheaper model. */
100
+ failed_cheaper: boolean
101
+ answer: ModelAnswer | null
102
+ }): ModelPick => {
103
+ if (!isRoutedRole(role)) return LOCKED_PICK
104
+ if (failed_cheaper) return { model: 'opus', model_reason: 'after_failure' }
105
+ if (models.floors[role] === 'opus') {
106
+ return { model: 'opus', model_reason: 'floor' }
107
+ }
108
+ if (answer === null) return { model: 'opus', model_reason: 'no_answer' }
109
+ const pick = ModelNameSchema.safeParse(answer.pick)
110
+ if (
111
+ !pick.success ||
112
+ !answer.offered.includes(pick.data) ||
113
+ answer.confidence === null ||
114
+ answer.confidence < models.confidence_bar
115
+ ) {
116
+ return { model: 'opus', model_reason: 'unsure' }
117
+ }
118
+ return { model: pick.data, model_reason: 'decision_model' }
119
+ }
120
+
121
+ /**
122
+ * The answer to the `agent_model` ask journaled for this agent since its
123
+ * role's last start on the ticket, or `null` with none: no ask (the decision
124
+ * model is off, or the ask was skipped), or an ask that failed.
125
+ */
126
+ export const modelAnswerOf = ({
127
+ records,
128
+ ticket,
129
+ role,
130
+ }: {
131
+ records: JournalRecord[]
132
+ ticket: number | null
133
+ role: AgentRole
134
+ }): ModelAnswer | null => {
135
+ const lastStart = records.findLastIndex(
136
+ (record) =>
137
+ record.kind === 'agent_started' &&
138
+ record.ticket === ticket &&
139
+ record.role === role
140
+ )
141
+ const asked = records
142
+ .slice(lastStart + 1)
143
+ .findLast(
144
+ (record) =>
145
+ record.kind === 'jev_asked' &&
146
+ record.content.job === 'agent_model' &&
147
+ record.ticket === ticket &&
148
+ record.role === role
149
+ )
150
+ if (asked?.kind !== 'jev_asked') return null
151
+ const answered = records.find(
152
+ (record) =>
153
+ record.kind === 'jev_answered' &&
154
+ record.content.asked_seq === asked.seq
155
+ )
156
+ if (answered?.kind !== 'jev_answered') return null
157
+ const answer = answered.content.answers.model
158
+ if (answer === undefined) return null
159
+ const question = asked.content.request.questions.model
160
+ return {
161
+ offered:
162
+ question?.type === 'choice' ? Object.keys(question.criteria) : [],
163
+ pick: answer.value,
164
+ confidence: answer.confidence,
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Whether this role failed a try on this ticket on a model cheaper than
170
+ * Opus (#604), as replay counts a try (`failed_cheaper`): the learner's at
171
+ * the end of a run, with no ticket.
172
+ */
173
+ export const failedCheaper = ({
174
+ state,
175
+ ticket,
176
+ role,
177
+ }: {
178
+ state: RunState
179
+ ticket: number | null
180
+ role: string
181
+ }): boolean =>
182
+ ticket === null
183
+ ? role === 'learner' && state.memory.learner.failed_cheaper
184
+ : (state.tickets[ticket]?.failed_cheaper ?? []).some(
185
+ (failed) => failed === role
186
+ )
187
+
188
+ /**
189
+ * The model a launch step's earlier try journaled before a crash cut it
190
+ * off, so its redo launches on it again (#604); `null` if none did.
191
+ *
192
+ * @param first_seq - The seq of the step's first try.
193
+ */
194
+ export const redonePick = ({
195
+ records,
196
+ first_seq,
197
+ ticket,
198
+ role,
199
+ }: {
200
+ records: JournalRecord[]
201
+ first_seq: number
202
+ ticket: number | null
203
+ role: string
204
+ }): ModelPick | null => {
205
+ const started = records.findLast(
206
+ (record) =>
207
+ record.seq >= first_seq &&
208
+ record.kind === 'agent_started' &&
209
+ record.ticket === ticket &&
210
+ record.content.role === role
211
+ )
212
+ if (started?.kind !== 'agent_started') return null
213
+ const model = ModelNameSchema.safeParse(started.content.model)
214
+ const { model_reason } = started.content
215
+ if (!model.success || model_reason === null) return null
216
+ return { model: model.data, model_reason }
217
+ }
218
+
219
+ /** Which try of its step a launch is: a redo names its first try's seq. */
220
+ export type LaunchTry = { first_seq: number; redo: boolean }
221
+
222
+ /**
223
+ * Whether a fresh agent's launch asks the decision model for its model: not
224
+ * after its role failed a try on a cheaper model, and not on a redo whose
225
+ * first try journaled its model.
226
+ */
227
+ export const asksModel = ({
228
+ records,
229
+ state,
230
+ ticket,
231
+ role,
232
+ step,
233
+ }: {
234
+ records: JournalRecord[]
235
+ state: RunState
236
+ ticket: number | null
237
+ role: string
238
+ step: LaunchTry | undefined
239
+ }): boolean =>
240
+ !(
241
+ (step?.redo === true &&
242
+ redonePick({ records, first_seq: step.first_seq, ticket, role }) !==
243
+ null) ||
244
+ failedCheaper({ state, ticket, role })
245
+ )
246
+
247
+ /**
248
+ * 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 its
250
+ * first try journaled.
251
+ */
252
+ export const launchPick = ({
253
+ records,
254
+ state,
255
+ config,
256
+ ticket,
257
+ role,
258
+ step,
259
+ }: {
260
+ records: JournalRecord[]
261
+ state: RunState
262
+ config: EngineConfig
263
+ ticket: number | null
264
+ role: AgentRole
265
+ step: LaunchTry
266
+ }): ModelPick =>
267
+ (step.redo
268
+ ? redonePick({ records, first_seq: step.first_seq, ticket, role })
269
+ : null) ??
270
+ pickModel({
271
+ role,
272
+ models: modelsOf({ config }),
273
+ failed_cheaper: failedCheaper({ state, ticket, role }),
274
+ answer: modelAnswerOf({ records, ticket, role }),
275
+ })
@@ -0,0 +1,167 @@
1
+ import { z } from 'zod'
2
+
3
+ import { ENTRY_FIELDS } from './entry-fields'
4
+
5
+ import { AgentSessionSchema } from '../agents/agent-launcher'
6
+ import {
7
+ AgentRoleSchema,
8
+ FileModeSchema,
9
+ RoleResultSchema,
10
+ } from '../agents/role-results'
11
+ import { GateCheckSchema } from '../gates/gate-schemas'
12
+ import { ModelReasonSchema } from '../jev/model-pick'
13
+
14
+ /**
15
+ * The journal entries of agent turns: started, finished or failed, their
16
+ * sessions, and shared `.git` changes during a turn.
17
+ */
18
+
19
+ /**
20
+ * An agent turn was started, with its prompt word for word. A follow-up in a
21
+ * fix loop names the session it went to in `follow_up_of`, and `prompt` holds
22
+ * the follow-up message. `model` is the short name it launched on (a
23
+ * follow-up's is its session's); `null` in journals from before #600. The
24
+ * exact version is in the turn's `agent_session`. `model_reason` is why it
25
+ * launched on that model (#603; a follow-up's is its session's); `null` in
26
+ * journals from before it.
27
+ */
28
+ export const AgentStartedEntrySchema = z.object({
29
+ ...ENTRY_FIELDS,
30
+ kind: z.literal('agent_started'),
31
+ content: z.object({
32
+ role: AgentRoleSchema,
33
+ prompt: z.string(),
34
+ follow_up_of: z.string().nullable().default(null),
35
+ model: z.string().nullable().default(null),
36
+ model_reason: ModelReasonSchema.nullable().default(null),
37
+ }),
38
+ })
39
+
40
+ /** The session an agent turn ran in; `null` in journals from before #363. */
41
+ const SESSION_FIELDS = { session_id: z.string().nullable().default(null) }
42
+
43
+ /**
44
+ * An agent finished with a result that fits its role's schema. One option
45
+ * per role: keep it in step with `RoleResultSchema` (a test checks every
46
+ * role parses).
47
+ */
48
+ export const AgentFinishedEntrySchema = z.object({
49
+ ...ENTRY_FIELDS,
50
+ kind: z.literal('agent_finished'),
51
+ content: z.discriminatedUnion('role', [
52
+ RoleResultSchema.options[0].extend(SESSION_FIELDS),
53
+ RoleResultSchema.options[1].extend(SESSION_FIELDS),
54
+ RoleResultSchema.options[2].extend(SESSION_FIELDS),
55
+ RoleResultSchema.options[3].extend(SESSION_FIELDS),
56
+ RoleResultSchema.options[4].extend(SESSION_FIELDS),
57
+ RoleResultSchema.options[5].extend(SESSION_FIELDS),
58
+ RoleResultSchema.options[6].extend(SESSION_FIELDS),
59
+ RoleResultSchema.options[7].extend(SESSION_FIELDS),
60
+ RoleResultSchema.options[8].extend(SESSION_FIELDS),
61
+ ]),
62
+ })
63
+
64
+ /**
65
+ * How an agent's turn failed:
66
+ * - `agent`: the turn itself failed (an error result, a timeout).
67
+ * - `result`: no structured output, or output that misfits the role's schema.
68
+ * - `guard`: the after-turn check found a change the role may not make.
69
+ * - `engine`: the engine's side failed (the SDK crashed, or a follow-up's
70
+ * session is gone).
71
+ * - `evidence`: a test-writer's `already_done` evidence did not check out
72
+ * (#495). Replay makes it from a failed `already_done_checked`, not from
73
+ * an `agent_failed` record.
74
+ * - `chore`: a chore the writer's result asked the engine for could not be
75
+ * done (#618): a file mode on a path the role may not write, an unknown
76
+ * generator, or a failed one. See `chores_done`.
77
+ *
78
+ * The decision step counts all but `engine` as failed tries; an engine
79
+ * failure starts a fresh agent without using one.
80
+ */
81
+ export const AgentFailureSchema = z.enum([
82
+ 'agent',
83
+ 'result',
84
+ 'guard',
85
+ 'engine',
86
+ 'evidence',
87
+ 'chore',
88
+ ])
89
+
90
+ export type AgentFailure = z.infer<typeof AgentFailureSchema>
91
+
92
+ /**
93
+ * An agent's turn failed, journaled once per turn with how it failed.
94
+ * `session_gone`: a follow-up found its session closed or gone (#521), so no
95
+ * agent ran. Replay forgets the session as a closed one, and counts no engine
96
+ * failure; the fix round goes to a fresh agent. `false` in older journals.
97
+ * `out_of_turns`: the turn reached its role's turn limit (#543), as the
98
+ * launcher said; the PR lists such tickets. `false` in older journals.
99
+ */
100
+ export const AgentFailedEntrySchema = z.object({
101
+ ...ENTRY_FIELDS,
102
+ kind: z.literal('agent_failed'),
103
+ content: z.object({
104
+ role: AgentRoleSchema,
105
+ error: z.string(),
106
+ failure: AgentFailureSchema.default('agent'),
107
+ ...SESSION_FIELDS,
108
+ session_gone: z.boolean().default(false),
109
+ out_of_turns: z.boolean().default(false),
110
+ }),
111
+ })
112
+
113
+ /**
114
+ * The engine did the chores a writer's result asked for (#618), after its
115
+ * turn and before the checks: `file_modes` it set, and `checks`, one
116
+ * `generate` check per named generator it ran. `problems` lists what it
117
+ * could not do (a path the role may not write, an unknown generator, a
118
+ * failed one); with any, an `agent_failed` (`chore`) follows, for the
119
+ * role's fix loop.
120
+ */
121
+ export const ChoresDoneEntrySchema = z.object({
122
+ ...ENTRY_FIELDS,
123
+ kind: z.literal('chores_done'),
124
+ content: z.object({
125
+ role: AgentRoleSchema,
126
+ file_modes: z.array(FileModeSchema),
127
+ checks: z.array(GateCheckSchema),
128
+ problems: z.array(z.string()),
129
+ }),
130
+ })
131
+
132
+ /** The launcher's summary of one agent turn's model session. */
133
+ export const AgentSessionEntrySchema = z.object({
134
+ ...ENTRY_FIELDS,
135
+ kind: z.literal('agent_session'),
136
+ content: z.object({ role: AgentRoleSchema, session: AgentSessionSchema }),
137
+ })
138
+
139
+ /**
140
+ * The engine closed an agent's session: nothing can send it a follow-up
141
+ * anymore (its step's result was accepted, or its step ended another way).
142
+ * The decision step never follows up a closed session, even one a later
143
+ * record names again; a fix round that would have gone to it goes to a
144
+ * fresh agent.
145
+ */
146
+ export const AgentSessionClosedEntrySchema = z.object({
147
+ ...ENTRY_FIELDS,
148
+ kind: z.literal('agent_session_closed'),
149
+ content: z.object({ role: AgentRoleSchema, session_id: z.string() }),
150
+ })
151
+
152
+ /**
153
+ * Shared `.git` files changed during an agent's turn by something the
154
+ * engine can't attribute to the agent: config outside the ticket branch's
155
+ * own section, `info/exclude`, or hooks, such as another agent's
156
+ * `git push -u` in the same repo. The change is neither blamed on the agent
157
+ * nor undone; this record only notes it.
158
+ */
159
+ export const SharedGitChangedEntrySchema = z.object({
160
+ ...ENTRY_FIELDS,
161
+ kind: z.literal('shared_git_changed'),
162
+ content: z.object({
163
+ role: AgentRoleSchema,
164
+ /** What changed, such as `the shared .git/config changed`. */
165
+ changes: z.array(z.string()),
166
+ }),
167
+ })
@@ -0,0 +1,72 @@
1
+ import { z } from 'zod'
2
+
3
+ import { ENTRY_FIELDS } from './entry-fields'
4
+
5
+ /** The journal entries of agent messages: each one sent, and each delivery. */
6
+
7
+ /**
8
+ * What an agent message did when it was sent:
9
+ * - `queued`: it waits for its recipients' next tool calls.
10
+ * - `not_delivered`: nobody can get it (the receiver's ticket is over, or
11
+ * `all` found no one); it stays in the journal.
12
+ * - `refused`: it broke a rule (a reviewer, a bad address, the cap).
13
+ */
14
+ export const AgentMessageStatusSchema = z.enum([
15
+ 'queued',
16
+ 'not_delivered',
17
+ 'refused',
18
+ ])
19
+
20
+ export type AgentMessageStatus = z.infer<typeof AgentMessageStatusSchema>
21
+
22
+ /**
23
+ * One agent message, word for word, as it was sent. Addresses look like
24
+ * `implementer#11`: a role and a ticket.
25
+ */
26
+ export const AgentMessageSchema = z.object({
27
+ /** `msg-<n>`: n is 1 + the earlier `agent_message` records in the journal. */
28
+ id: z.string().min(1),
29
+ /** The sender's address. */
30
+ from: z.string(),
31
+ /** What the sender asked for: an address, or `all`. */
32
+ to: z.string(),
33
+ text: z.string(),
34
+ status: AgentMessageStatusSchema,
35
+ /** The addresses it waits for; empty unless queued. */
36
+ recipients: z.array(z.string()),
37
+ /** Why it was refused or not delivered; `null` when queued. */
38
+ reason: z.string().nullable(),
39
+ })
40
+
41
+ export type AgentMessage = z.infer<typeof AgentMessageSchema>
42
+
43
+ /** An agent sent a message. `ticket` and `role` are the sender's. */
44
+ export const AgentMessageEntrySchema = z.object({
45
+ ...ENTRY_FIELDS,
46
+ kind: z.literal('agent_message'),
47
+ content: AgentMessageSchema,
48
+ })
49
+
50
+ /** Messages handed to their receiver at one tool call, word for word. */
51
+ export const AgentMessageDeliveredSchema = z.object({
52
+ /** The receiver's address. */
53
+ to: z.string(),
54
+ /** The ids of the messages handed over together. */
55
+ ids: z.array(z.string()),
56
+ /** The tool call they rode on. */
57
+ tool_name: z.string().nullable(),
58
+ /** The additional context the agent got, word for word. */
59
+ text: z.string(),
60
+ })
61
+
62
+ export type AgentMessageDelivered = z.infer<typeof AgentMessageDeliveredSchema>
63
+
64
+ /**
65
+ * Agent messages reached their receiver. `ticket` and `role` are the
66
+ * receiver's; the record's time is when it saw them.
67
+ */
68
+ export const AgentMessageDeliveredEntrySchema = z.object({
69
+ ...ENTRY_FIELDS,
70
+ kind: z.literal('agent_message_delivered'),
71
+ content: AgentMessageDeliveredSchema,
72
+ })