@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.
- package/CHANGELOG.md +76 -0
- package/README.md +7 -2
- package/board/client/board-look.ts +31 -1
- package/board/client/board-panel-parts.tsx +150 -0
- package/board/client/board-panel-stages.ts +99 -0
- package/board/client/board-panel-styles.ts +223 -0
- package/board/client/board-panel.tsx +77 -1275
- package/board/client/engine-life.ts +48 -0
- package/board/client/final-review-stack.tsx +144 -0
- package/board/client/heartbeat.tsx +61 -0
- package/board/client/helper-lines.tsx +69 -0
- package/board/client/live-feed.tsx +86 -0
- package/board/client/live-line.ts +73 -0
- package/board/client/model-line.tsx +178 -0
- package/board/client/needs-you-section.tsx +133 -0
- package/board/client/one-line.tsx +193 -0
- package/board/client/repo-wait-banner.tsx +69 -0
- package/board/client/run-footer.tsx +68 -0
- package/board/client/run-header.tsx +189 -0
- package/board/client/run-picker.tsx +98 -0
- package/board/client/status-bar.tsx +131 -0
- package/board/client/step-dots.tsx +127 -0
- package/board/client/stuck-actions.tsx +269 -0
- package/board/client/ticket-card-view.tsx +238 -0
- package/board/client/ticket-detail.tsx +87 -0
- package/board/client/ticket-stack.tsx +119 -0
- package/board/client/ticket-wait.ts +188 -0
- package/board/client/timeline-rows.tsx +41 -14
- package/board/client/usage-line.tsx +51 -0
- package/board/client/wait-line.tsx +39 -0
- package/board/index.client.tsx +1 -1
- package/board/index.server.ts +7 -1
- package/board/package.json +1 -1
- package/board/server/board-panel-state-helpers.ts +67 -0
- package/board/server/board-server-check.ts +294 -0
- package/board/server/board-server-engine.ts +129 -0
- package/board/server/board-server-events.ts +134 -0
- package/board/server/board-server-read.ts +190 -0
- package/board/server/board-server-resume.ts +164 -0
- package/board/server/board-server-start.ts +174 -0
- package/board/server/board-server-store.ts +281 -0
- package/board/server/board-server-types.ts +114 -0
- package/board/server/board-server-util.ts +4 -0
- package/board/server/board-server.ts +52 -943
- package/board/server/board-vocabulary-engine.ts +144 -0
- package/board/server/board-vocabulary-review.ts +115 -0
- package/board/server/board-vocabulary-run.ts +145 -0
- package/board/server/board-vocabulary-shapes.ts +153 -0
- package/board/server/board-vocabulary.ts +20 -375
- package/board/server/current-step-panel-helpers.ts +108 -0
- package/board/server/describe-record.ts +403 -0
- package/board/server/engine-launch.ts +20 -3
- package/board/server/engine-restart-helpers.ts +105 -0
- package/board/server/engine-watch.ts +45 -5
- package/board/server/journal-rebuild-helpers.ts +169 -0
- package/board/server/live-file.ts +130 -0
- package/board/server/luca-version.ts +1 -1
- package/board/server/make-rows.ts +14 -629
- package/board/server/record-texts.ts +350 -0
- package/board/server/reduce-board-agent-start.ts +224 -0
- package/board/server/reduce-board-agents.ts +160 -0
- package/board/server/reduce-board-counters.ts +138 -0
- package/board/server/reduce-board-create.ts +148 -0
- package/board/server/reduce-board-final-review.ts +174 -0
- package/board/server/reduce-board-needs-you.ts +283 -0
- package/board/server/reduce-board-reasons.ts +59 -0
- package/board/server/reduce-board-run.ts +255 -0
- package/board/server/reduce-board-settle.ts +88 -0
- package/board/server/reduce-board-steps.ts +226 -0
- package/board/server/reduce-board-text.ts +139 -0
- package/board/server/reduce-board-ticket-checks.ts +168 -0
- package/board/server/reduce-board-ticket-join.ts +144 -0
- package/board/server/reduce-board-ticket-start.ts +132 -0
- package/board/server/reduce-board-update.ts +117 -0
- package/board/server/reduce-board-usage.ts +247 -0
- package/board/server/reduce-board.ts +100 -1917
- package/board/server/reply-post.ts +407 -0
- package/board/server/run-registry.ts +8 -4
- package/board/server/ticket-stages-panel-helpers.ts +296 -0
- package/board/shared/board-rows.ts +9 -0
- package/board/shared/board-rpc.ts +42 -0
- package/board/shared/board-state-counts.ts +77 -0
- package/board/shared/board-state-live.ts +99 -0
- package/board/shared/board-state-review.ts +59 -0
- package/board/shared/board-state-run.ts +236 -0
- package/board/shared/board-state-ticket.ts +257 -0
- package/board/shared/board-state-usage.ts +107 -0
- package/board/shared/board-state.ts +97 -567
- package/board/shared/reply-actions.ts +207 -0
- package/engine/agents/agent-launcher.ts +27 -4
- package/engine/agents/claude-launcher-helpers.ts +233 -0
- package/engine/agents/claude-launcher.ts +28 -669
- package/engine/agents/claude-messages.ts +212 -0
- package/engine/agents/claude-options.ts +43 -8
- package/engine/agents/common-instructions.ts +138 -0
- package/engine/agents/input-channel.ts +36 -0
- package/engine/agents/launch-paths.ts +29 -0
- package/engine/agents/launch-session.ts +227 -0
- package/engine/agents/learner-instructions.ts +31 -0
- package/engine/agents/open-session.ts +210 -0
- package/engine/agents/prompt-sections.ts +95 -0
- package/engine/agents/rejoin-prompts.ts +135 -0
- package/engine/agents/reviewer-instructions.ts +99 -0
- package/engine/agents/role-instructions.ts +31 -244
- package/engine/agents/role-prompts.ts +39 -109
- package/engine/agents/role-results.ts +106 -7
- package/engine/agents/scripted-launcher.ts +21 -6
- package/engine/agents/session-pool.ts +124 -0
- package/engine/agents/turn-progress.ts +283 -0
- package/engine/agents/writer-instructions.ts +151 -0
- package/engine/cli/computer-adapters-real.ts +15 -3
- package/engine/cli/computer-adapters.ts +2 -0
- package/engine/cli/computer-checks.ts +87 -4
- package/engine/cli/config-reload-text.ts +39 -0
- package/engine/cli/decision-model-setup.ts +70 -0
- package/engine/cli/doctor-command.ts +1 -0
- package/engine/cli/doctor.ts +21 -3
- package/engine/cli/going-runs.ts +192 -36
- package/engine/cli/init-command.ts +1 -0
- package/engine/cli/init.ts +23 -1
- package/engine/cli/live-runs.ts +73 -0
- package/engine/cli/luca-run.ts +42 -7
- package/engine/cli/luca-skills.ts +183 -0
- package/engine/cli/luca.ts +8 -1
- package/engine/cli/models-command.ts +119 -0
- package/engine/cli/read-run.ts +81 -0
- package/engine/cli/run-args.ts +33 -6
- package/engine/cli/run-demo.ts +143 -0
- package/engine/cli/run-end.ts +184 -0
- package/engine/cli/run-modes-helpers.ts +81 -0
- package/engine/cli/run-modes.ts +62 -439
- package/engine/cli/setup.ts +5 -48
- package/engine/cli/unfinished-runs.ts +122 -0
- package/engine/cli/upgrade-command.ts +212 -30
- package/engine/cli/upgrade-finish.ts +149 -0
- package/engine/cli/upgrade.ts +112 -71
- package/engine/config/config-fields.ts +127 -0
- package/engine/config/config-reload.ts +250 -0
- package/engine/config/engine-config-schema.ts +235 -0
- package/engine/config/engine-config.ts +72 -142
- package/engine/config/test-commands.ts +102 -0
- package/engine/core/agent-guards-helpers.ts +84 -0
- package/engine/core/agent-turn.ts +395 -0
- package/engine/core/already-done.ts +44 -1
- package/engine/core/build-action.ts +214 -0
- package/engine/core/build-context.ts +135 -0
- package/engine/core/build-launch.ts +259 -0
- package/engine/core/commit-in.ts +282 -0
- package/engine/core/decide-build-early.ts +108 -0
- package/engine/core/decide-build-ending.ts +145 -0
- package/engine/core/decide-build-helpers.ts +87 -0
- package/engine/core/decide-build-tickets.ts +207 -0
- package/engine/core/decide-build-view.ts +103 -0
- package/engine/core/decide-build.ts +101 -1368
- package/engine/core/decide-final-fix.ts +203 -0
- package/engine/core/decide-final-review.ts +55 -310
- package/engine/core/decide-red-green.ts +398 -0
- package/engine/core/decide-rejoin.ts +259 -0
- package/engine/core/decide-repo-wait.ts +129 -0
- package/engine/core/decide-run-budget.ts +86 -12
- package/engine/core/decide-stuck-helpers.ts +127 -0
- package/engine/core/decide-stuck.ts +60 -8
- package/engine/core/decide-ticket-review.ts +95 -0
- package/engine/core/decide-ticket.ts +267 -0
- package/engine/core/decide-waits.ts +47 -0
- package/engine/core/decide.ts +74 -23
- package/engine/core/engine-chores.ts +163 -0
- package/engine/core/environment-rerun.ts +83 -0
- package/engine/core/evidence-text.ts +84 -0
- package/engine/core/execute-action.ts +303 -0
- package/engine/core/execute-already-done-check.ts +133 -0
- package/engine/core/execute-build.ts +68 -944
- package/engine/core/execute-changeset.ts +5 -7
- package/engine/core/execute-final-review.ts +125 -13
- package/engine/core/execute-gates.ts +182 -0
- package/engine/core/execute-intake.ts +130 -0
- package/engine/core/execute-join.ts +276 -0
- package/engine/core/execute-memory.ts +12 -0
- package/engine/core/execute-pull-request.ts +84 -0
- package/engine/core/execute-red-check.ts +219 -0
- package/engine/core/execute-repo-wait.ts +129 -0
- package/engine/core/execute-stuck.ts +25 -10
- package/engine/core/execute-waits.ts +158 -0
- package/engine/core/execute-with-jev.ts +118 -0
- package/engine/core/execute.ts +124 -831
- package/engine/core/final-failure-cap.ts +38 -0
- package/engine/core/final-review-action.ts +125 -0
- package/engine/core/final-review-text.ts +63 -17
- package/engine/core/final-stuck-text.ts +86 -0
- package/engine/core/fix-loop-text.ts +49 -2
- package/engine/core/left-out.ts +120 -0
- package/engine/core/live-file.ts +372 -0
- package/engine/core/loop-caps.ts +25 -2
- package/engine/core/not-in-run.ts +7 -3
- package/engine/core/pull-request-text.ts +255 -6
- package/engine/core/repo-wait-text.ts +81 -0
- package/engine/core/review-text.ts +55 -1
- package/engine/core/run-crash-recovery-helpers.ts +16 -0
- package/engine/core/run-session-close-helpers.ts +96 -0
- package/engine/core/run-start.ts +148 -0
- package/engine/core/run-stuck-text.ts +86 -0
- package/engine/core/schedule-rules.ts +193 -0
- package/engine/core/stuck-detail-text.ts +89 -0
- package/engine/core/stuck-format.ts +22 -0
- package/engine/core/stuck-text.ts +29 -332
- package/engine/core/ticket-stuck-text.ts +192 -0
- package/engine/core/worktree-changes.ts +106 -0
- package/engine/gates/already-done-check.ts +120 -0
- package/engine/gates/feature-map.ts +96 -0
- package/engine/gates/gate-runner.ts +108 -108
- package/engine/gates/gate-schemas.ts +61 -2
- package/engine/gates/leftover-scan.ts +154 -31
- package/engine/gates/new-tests.ts +45 -0
- package/engine/gates/own-files-check.ts +98 -0
- package/engine/gates/prepare-check.ts +60 -0
- package/engine/gates/prepare-slots.ts +99 -0
- package/engine/gates/red-check.ts +184 -16
- package/engine/gates/shell-check.ts +64 -0
- package/engine/gates/slot-waits.ts +55 -0
- package/engine/gates/slow-test-runner.ts +59 -0
- package/engine/gates/test-checks.ts +161 -0
- package/engine/gates/test-runner.ts +164 -5
- package/engine/gates/test-slots.ts +48 -0
- package/engine/git/git-adapter.ts +55 -396
- package/engine/git/git-commits.ts +132 -0
- package/engine/git/git-join.ts +139 -0
- package/engine/git/git-read.ts +123 -0
- package/engine/git/git-run.ts +32 -0
- package/engine/git/git-worktrees.ts +143 -0
- package/engine/git/outside-links.ts +55 -0
- package/engine/guards/after-turn-check.ts +31 -5
- package/engine/guards/guard-hook.ts +60 -12
- package/engine/guards/guard-roles.ts +68 -0
- package/engine/guards/helper-rules.ts +92 -0
- package/engine/guards/permission-rules.ts +161 -0
- package/engine/guards/role-rules-helpers.ts +48 -0
- package/engine/guards/role-rules.ts +34 -606
- package/engine/guards/shell-rules.ts +344 -0
- package/engine/guards/worktree-git.ts +208 -0
- package/engine/guards/worktree-snapshot.ts +412 -0
- package/engine/guards/worktree-state.ts +28 -825
- package/engine/guards/worktree-undo.ts +260 -0
- package/engine/guards/write-rules.ts +149 -0
- package/engine/index.ts +30 -9
- package/engine/intake/criteria-checks.ts +114 -0
- package/engine/intake/intake-checks.ts +164 -30
- package/engine/intake/intake-schemas.ts +37 -2
- package/engine/jev/clef-client.ts +259 -0
- package/engine/jev/decision-model-credentials.ts +212 -0
- package/engine/jev/jev-jobs.ts +144 -60
- package/engine/jev/jev-schemas.ts +86 -5
- package/engine/jev/jev-shadow-helpers.ts +218 -0
- package/engine/jev/jev-shadow.ts +109 -64
- package/engine/jev/model-pick.ts +275 -0
- package/engine/journal/agent-entries.ts +167 -0
- package/engine/journal/agent-message-entries.ts +72 -0
- package/engine/journal/agent-turn-replay.ts +203 -0
- package/engine/journal/already-done-entries.ts +55 -0
- package/engine/journal/commit-entries.ts +56 -0
- package/engine/journal/cross-ticket-replay.ts +222 -0
- package/engine/journal/decision-model-entries.ts +86 -0
- package/engine/journal/entry-fields.ts +10 -0
- package/engine/journal/final-review-entries.ts +128 -0
- package/engine/journal/final-review-replay.ts +324 -0
- package/engine/journal/final-review-round.ts +320 -0
- package/engine/journal/final-review-state.ts +180 -0
- package/engine/journal/gate-entries.ts +81 -0
- package/engine/journal/intake-entries.ts +87 -0
- package/engine/journal/join-entries.ts +126 -0
- package/engine/journal/journal-entry-schema.ts +218 -0
- package/engine/journal/journal-kinds.ts +97 -0
- package/engine/journal/journal-record-schema.ts +218 -0
- package/engine/journal/journal-record.ts +43 -1400
- package/engine/journal/journal.ts +6 -1
- package/engine/journal/limit-entries.ts +90 -0
- package/engine/journal/memory-entries.ts +124 -0
- package/engine/journal/memory-replay.ts +157 -0
- package/engine/journal/plan-replay.ts +139 -0
- package/engine/journal/pull-request-entries.ts +41 -0
- package/engine/journal/replay.ts +157 -2144
- package/engine/journal/reply-replay.ts +144 -0
- package/engine/journal/repo-wait-entries.ts +53 -0
- package/engine/journal/run-entries.ts +172 -0
- package/engine/journal/run-state.ts +264 -0
- package/engine/journal/shared-failure-replay.ts +133 -0
- package/engine/journal/stuck-entries.ts +193 -0
- package/engine/journal/ticket-progress.ts +392 -0
- package/engine/journal/ticket-replay.ts +411 -0
- package/engine/journal/ticket-result-replay.ts +364 -0
- package/engine/journal/ticket-retry-replay.ts +142 -0
- package/engine/journal/worktree-entries.ts +122 -0
- package/engine/models/model-score.ts +512 -0
- package/engine/shell/run-command.ts +187 -18
- package/engine/testing/agent-turn-fixtures.ts +287 -0
- package/engine/testing/already-done-fixtures.ts +97 -0
- package/engine/testing/build-fixtures.ts +68 -973
- package/engine/testing/checkouts-fixtures.ts +48 -0
- package/engine/testing/final-review-fixtures.ts +62 -1
- package/engine/testing/final-review-steps.ts +143 -0
- package/engine/testing/limit-fixtures.ts +85 -0
- package/engine/testing/live-file-fixtures.ts +57 -0
- package/engine/testing/practice-repo.ts +10 -4
- package/engine/testing/practice-run.ts +1 -1
- package/engine/testing/reply-fixtures.ts +88 -0
- package/engine/testing/run-budget-fixtures.ts +165 -0
- package/engine/testing/run-fixtures.ts +219 -0
- package/engine/testing/sdk-play.ts +177 -0
- package/engine/testing/setup-repo.ts +194 -0
- package/engine/testing/step-record-fixtures.ts +55 -0
- package/engine/testing/ticket-step-fixtures.ts +272 -0
- package/engine/testing/wait-scenarios.ts +186 -0
- package/engine/tracker/gh-retry.ts +217 -0
- package/engine/tracker/github-tracker.ts +310 -90
- package/engine/tracker/in-memory-tracker.ts +8 -4
- package/engine/tracker/tracker.ts +131 -6
- package/package.json +2 -1
- package/skills/luca-retro/SKILL.md +74 -0
- package/skills/luca-retro/scripts/retro-summary.ts +1356 -0
- package/skills/luca-unstick/SKILL.md +153 -0
- package/skills/luca-unstick/scripts/stuck-summary.ts +1063 -0
- package/engine/jev/jev-client.ts +0 -173
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { mkdtemp, rm } from 'node:fs/promises'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
|
|
5
|
+
import { afterAll, beforeAll, expect } from 'bun:test'
|
|
6
|
+
|
|
7
|
+
import type { DecisionModelClient } from './clef-client'
|
|
8
|
+
import type { JevAnswer, JevQuestion, JevRequest } from './jev-schemas'
|
|
9
|
+
|
|
10
|
+
import type { ScriptedTurn } from '../agents/scripted-launcher'
|
|
11
|
+
import type { JournalRecord } from '../journal/journal-record'
|
|
12
|
+
import { runPractice, type PracticeRun } from '../testing/practice-repo'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Helpers shared by the end-to-end shadow-mode tests
|
|
16
|
+
* (`jev-shadow.e2e.test.ts` and `jev-shadow-findings.e2e.test.ts`): the fake
|
|
17
|
+
* decision model that disagrees with everything, and the run it is compared
|
|
18
|
+
* against.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** A shadow-mode record: an ask, its reply, or the decision model turning off. */
|
|
22
|
+
export const isJev = (record: JournalRecord): boolean =>
|
|
23
|
+
record.kind.startsWith('jev_') || record.kind === 'decision_model_off'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Launches with test-run timings (" [7.00ms]") removed and commit shas
|
|
27
|
+
* (which carry the time) masked: a fix-loop follow-up quotes test output,
|
|
28
|
+
* whose timings differ between two runs, and bun leaves the timing off a
|
|
29
|
+
* test that runs very fast, so a timing is dropped whether or not it's there.
|
|
30
|
+
*/
|
|
31
|
+
const withoutTimings = (launches: PracticeRun['launches']) =>
|
|
32
|
+
launches.map((launch) => ({
|
|
33
|
+
...launch,
|
|
34
|
+
prompt: launch.prompt
|
|
35
|
+
.replace(/ ?\[\d+(\.\d+)?m?s\]/g, '')
|
|
36
|
+
.replace(/\b[0-9a-f]{40}\b/g, '[sha]'),
|
|
37
|
+
}))
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Record kinds in order, but the final review's lenses run at once, so
|
|
41
|
+
* the kinds between its start and its pass are sorted.
|
|
42
|
+
*/
|
|
43
|
+
const kindsInOrder = (records: JournalRecord[]): string[] => {
|
|
44
|
+
const kinds = records
|
|
45
|
+
.filter((record) => !isJev(record))
|
|
46
|
+
.map(({ kind }) => kind)
|
|
47
|
+
const from = kinds.indexOf('final_review_started')
|
|
48
|
+
const to = kinds.indexOf('final_review_passed')
|
|
49
|
+
if (from === -1 || to === -1) return kinds
|
|
50
|
+
return [
|
|
51
|
+
...kinds.slice(0, from + 1),
|
|
52
|
+
...kinds.slice(from + 1, to).toSorted(),
|
|
53
|
+
...kinds.slice(to),
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Launches in order, but the final review's lenses (which start at once, in
|
|
59
|
+
* any order) sorted by role, with their session numbers masked.
|
|
60
|
+
*/
|
|
61
|
+
const launchesInOrder = (launches: PracticeRun['launches']) => {
|
|
62
|
+
const lens = (launch: PracticeRun['launches'][number]) =>
|
|
63
|
+
launch.role.endsWith('-lens')
|
|
64
|
+
return [
|
|
65
|
+
...launches.filter((launch) => !lens(launch)),
|
|
66
|
+
...launches
|
|
67
|
+
.filter(lens)
|
|
68
|
+
.map((launch) => ({
|
|
69
|
+
...launch,
|
|
70
|
+
session_id: launch.session_id.replace(/-\d+$/, '-[n]'),
|
|
71
|
+
}))
|
|
72
|
+
.toSorted((left, right) => left.role.localeCompare(right.role)),
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The run as it would look without Jev: the same steps, prompts, and PR. */
|
|
77
|
+
export const expectSameRun = ({
|
|
78
|
+
run,
|
|
79
|
+
without,
|
|
80
|
+
}: {
|
|
81
|
+
run: PracticeRun
|
|
82
|
+
without: PracticeRun
|
|
83
|
+
}) => {
|
|
84
|
+
expect(run.action).toEqual(without.action)
|
|
85
|
+
expect(kindsInOrder(run.records)).toEqual(kindsInOrder(without.records))
|
|
86
|
+
expect(withoutTimings(launchesInOrder(run.launches))).toEqual(
|
|
87
|
+
withoutTimings(launchesInOrder(without.launches))
|
|
88
|
+
)
|
|
89
|
+
expect(run.tracker.pullRequests()).toEqual(without.tracker.pullRequests())
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The model id the fake decision models say they are. */
|
|
93
|
+
export const FAKE_MODEL = '@cf/cloudflare/clef'
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* A Jev that answers every question, and never as the engine would: the
|
|
97
|
+
* last option of each choice, the top of each score, yes to every skill.
|
|
98
|
+
*/
|
|
99
|
+
const disagreeingAnswer = (question: JevQuestion): JevAnswer => {
|
|
100
|
+
switch (question.type) {
|
|
101
|
+
case 'choice': {
|
|
102
|
+
const choice = Object.keys(question.criteria).at(-1) ?? null
|
|
103
|
+
return {
|
|
104
|
+
value: choice,
|
|
105
|
+
confidence: 0.7,
|
|
106
|
+
raw: { choice, confidence: 0.7 },
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
case 'score': {
|
|
110
|
+
const score = question.criteria.at(-1) ?? null
|
|
111
|
+
return {
|
|
112
|
+
value: score,
|
|
113
|
+
confidence: 0.6,
|
|
114
|
+
raw: { score, confidence: 0.6 },
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
case 'noul':
|
|
118
|
+
return { value: 0.95, confidence: null, raw: { probability: 0.95 } }
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Whether a request has a choice Clef refuses: fewer than two options (#546). */
|
|
123
|
+
export const hasOneOptionChoice = (request: JevRequest): boolean =>
|
|
124
|
+
Object.values(request.questions).some(
|
|
125
|
+
(question) =>
|
|
126
|
+
question.type === 'choice' &&
|
|
127
|
+
Object.keys(question.criteria).length < 2
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
const disagreeingJev = (): DecisionModelClient & {
|
|
131
|
+
signals: AbortSignal[]
|
|
132
|
+
requests: JevRequest[]
|
|
133
|
+
} => {
|
|
134
|
+
const signals: AbortSignal[] = []
|
|
135
|
+
const requests: JevRequest[] = []
|
|
136
|
+
return {
|
|
137
|
+
signals,
|
|
138
|
+
requests,
|
|
139
|
+
model: FAKE_MODEL,
|
|
140
|
+
ask: async ({ request, signal }) => {
|
|
141
|
+
signals.push(signal)
|
|
142
|
+
requests.push(request)
|
|
143
|
+
return {
|
|
144
|
+
ok: true,
|
|
145
|
+
answers: Object.fromEntries(
|
|
146
|
+
Object.entries(request.questions).map(([id, question]) => [
|
|
147
|
+
id,
|
|
148
|
+
disagreeingAnswer(question),
|
|
149
|
+
])
|
|
150
|
+
),
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
type Asked = Extract<JournalRecord, { kind: 'jev_asked' }>
|
|
157
|
+
type Answered = Extract<JournalRecord, { kind: 'jev_answered' }>
|
|
158
|
+
type Failed = Extract<JournalRecord, { kind: 'jev_failed' }>
|
|
159
|
+
|
|
160
|
+
export const asked = (records: JournalRecord[]): Asked[] =>
|
|
161
|
+
records.filter((record): record is Asked => record.kind === 'jev_asked')
|
|
162
|
+
|
|
163
|
+
/** The record right after each ask: its answer or its failure. */
|
|
164
|
+
export const replyTo = (
|
|
165
|
+
records: JournalRecord[],
|
|
166
|
+
ask: Asked
|
|
167
|
+
): Answered | Failed | undefined => {
|
|
168
|
+
const next = records[records.indexOf(ask) + 1]
|
|
169
|
+
return next?.kind === 'jev_answered' || next?.kind === 'jev_failed'
|
|
170
|
+
? next
|
|
171
|
+
: undefined
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export const SKILL_IDS = [
|
|
175
|
+
'tdd',
|
|
176
|
+
'systematic_debugging',
|
|
177
|
+
'codebase_design',
|
|
178
|
+
'domain_modeling',
|
|
179
|
+
]
|
|
180
|
+
|
|
181
|
+
/** A practice run with Jev, and the same run without it, built once. */
|
|
182
|
+
export const shareRuns = ({ turns }: { turns: () => ScriptedTurn[] }) => {
|
|
183
|
+
const shared: {
|
|
184
|
+
roots: string[]
|
|
185
|
+
jev?: ReturnType<typeof disagreeingJev>
|
|
186
|
+
run?: PracticeRun
|
|
187
|
+
without?: PracticeRun
|
|
188
|
+
} = { roots: [] }
|
|
189
|
+
beforeAll(async () => {
|
|
190
|
+
const root = await mkdtemp(join(tmpdir(), 'luca-engine-jev-'))
|
|
191
|
+
shared.roots.push(root)
|
|
192
|
+
const jev = disagreeingJev()
|
|
193
|
+
shared.jev = jev
|
|
194
|
+
shared.run = await runPractice({
|
|
195
|
+
root: join(root, 'with-jev'),
|
|
196
|
+
turns: turns(),
|
|
197
|
+
jev: { client: jev },
|
|
198
|
+
})
|
|
199
|
+
shared.without = await runPractice({
|
|
200
|
+
root: join(root, 'without-jev'),
|
|
201
|
+
turns: turns(),
|
|
202
|
+
})
|
|
203
|
+
}, 120_000)
|
|
204
|
+
afterAll(async () => {
|
|
205
|
+
await Promise.all(
|
|
206
|
+
shared.roots.map((root) =>
|
|
207
|
+
rm(root, { recursive: true, force: true })
|
|
208
|
+
)
|
|
209
|
+
)
|
|
210
|
+
})
|
|
211
|
+
return () => {
|
|
212
|
+
const { jev, run, without } = shared
|
|
213
|
+
if (jev === undefined || run === undefined || without === undefined) {
|
|
214
|
+
throw new Error('The shared runs were not built.')
|
|
215
|
+
}
|
|
216
|
+
return { jev, run, without }
|
|
217
|
+
}
|
|
218
|
+
}
|
package/engine/jev/jev-shadow.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type { DecisionModelClient, DecisionModelReply } from './clef-client'
|
|
4
4
|
import type { JevAsk } from './jev-jobs'
|
|
5
|
-
import { JevAnswerSchema } from './jev-schemas'
|
|
5
|
+
import { ClefRequestSchema, JevAnswerSchema } from './jev-schemas'
|
|
6
6
|
|
|
7
7
|
import type { Journal } from '../journal/journal'
|
|
8
8
|
|
|
9
|
-
/** How long the engine waits for one
|
|
9
|
+
/** How long the engine waits for one decision model answer before it moves on. */
|
|
10
10
|
export const DEFAULT_JEV_TIMEOUT_MS = 10_000
|
|
11
11
|
|
|
12
12
|
const JevAnswersSchema = z.record(z.string(), JevAnswerSchema)
|
|
@@ -15,26 +15,33 @@ const JevAnswersSchema = z.record(z.string(), JevAnswerSchema)
|
|
|
15
15
|
* A reply whose answers don't fit the journal's schema (only a broken client
|
|
16
16
|
* sends one) becomes an error reply, so appending it can never throw.
|
|
17
17
|
*/
|
|
18
|
-
const checkedReply = (reply:
|
|
18
|
+
const checkedReply = (reply: DecisionModelReply): DecisionModelReply => {
|
|
19
19
|
if (!reply.ok) return reply
|
|
20
20
|
const parsed = JevAnswersSchema.safeParse(reply.answers)
|
|
21
|
-
if (parsed.success)
|
|
21
|
+
if (parsed.success) {
|
|
22
|
+
return { ok: true, answers: parsed.data, model: reply.model }
|
|
23
|
+
}
|
|
22
24
|
return {
|
|
23
25
|
ok: false,
|
|
24
26
|
reason: 'error',
|
|
25
|
-
error: `
|
|
27
|
+
error: `The decision model's answers do not fit their schema:\n${z.prettifyError(parsed.error)}`,
|
|
26
28
|
}
|
|
27
29
|
}
|
|
28
30
|
|
|
29
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* The decision model, how long to wait for it, and where to print the line
|
|
33
|
+
* when it turns off mid-run, as `runEngine` takes them.
|
|
34
|
+
*/
|
|
30
35
|
export type JevShadow = {
|
|
31
|
-
client:
|
|
36
|
+
client: DecisionModelClient
|
|
32
37
|
/** Defaults to `DEFAULT_JEV_TIMEOUT_MS`. */
|
|
33
38
|
timeout_ms?: number
|
|
39
|
+
/** Gets one plain line when the decision model turns off mid-run. */
|
|
40
|
+
log?: (line: string) => void
|
|
34
41
|
}
|
|
35
42
|
|
|
36
43
|
/**
|
|
37
|
-
* Calls
|
|
44
|
+
* Calls the decision model once, and always comes back: a throw becomes an error reply, and
|
|
38
45
|
* a call past the timeout becomes a timeout reply even if the client ignores
|
|
39
46
|
* the abort signal.
|
|
40
47
|
*/
|
|
@@ -43,19 +50,19 @@ const askWithTimeout = async ({
|
|
|
43
50
|
ask,
|
|
44
51
|
timeout_ms,
|
|
45
52
|
}: {
|
|
46
|
-
jev:
|
|
53
|
+
jev: DecisionModelClient
|
|
47
54
|
ask: JevAsk
|
|
48
55
|
timeout_ms: number
|
|
49
|
-
}): Promise<
|
|
56
|
+
}): Promise<DecisionModelReply> => {
|
|
50
57
|
const controller = new AbortController()
|
|
51
58
|
let timer: ReturnType<typeof setTimeout> | undefined
|
|
52
|
-
const timedOut = new Promise<
|
|
59
|
+
const timedOut = new Promise<DecisionModelReply>((resolve) => {
|
|
53
60
|
timer = setTimeout(() => {
|
|
54
61
|
controller.abort()
|
|
55
62
|
resolve({
|
|
56
63
|
ok: false,
|
|
57
64
|
reason: 'timeout',
|
|
58
|
-
error: `
|
|
65
|
+
error: `The decision model did not answer within ${timeout_ms} ms.`,
|
|
59
66
|
})
|
|
60
67
|
}, timeout_ms)
|
|
61
68
|
})
|
|
@@ -64,10 +71,10 @@ const askWithTimeout = async ({
|
|
|
64
71
|
jev.ask({ request: ask.request, signal: controller.signal })
|
|
65
72
|
)
|
|
66
73
|
.catch(
|
|
67
|
-
(error: unknown):
|
|
74
|
+
(error: unknown): DecisionModelReply => ({
|
|
68
75
|
ok: false,
|
|
69
76
|
reason: 'error',
|
|
70
|
-
error: `
|
|
77
|
+
error: `The decision model threw: ${String(error)}`,
|
|
71
78
|
})
|
|
72
79
|
)
|
|
73
80
|
try {
|
|
@@ -77,73 +84,111 @@ const askWithTimeout = async ({
|
|
|
77
84
|
}
|
|
78
85
|
}
|
|
79
86
|
|
|
87
|
+
/** Asks the decision model about a step's asks, in shadow mode. */
|
|
88
|
+
export type ShadowAsker = (asks: JevAsk[]) => Promise<void>
|
|
89
|
+
|
|
80
90
|
/**
|
|
81
|
-
* Asks
|
|
82
|
-
* engine's fixed choice
|
|
83
|
-
* `
|
|
84
|
-
*
|
|
91
|
+
* Asks the decision model in **shadow mode** for one run: for each ask,
|
|
92
|
+
* journals `jev_asked` with the engine's fixed choice and the model, then
|
|
93
|
+
* `jev_answered` (with the model that answered) or `jev_failed`, linked back
|
|
94
|
+
* by `asked_seq`. The answers change nothing here; the launch step reads an
|
|
95
|
+
* `agent_model` answer whose ask is `acted` (#603). An ask outside Clef's limits
|
|
96
|
+
* (`ClefRequestSchema`) is skipped: not sent and not journaled. An error,
|
|
97
|
+
* timeout, or missing credentials is journaled and never thrown, so the run
|
|
98
|
+
* goes on.
|
|
99
|
+
*
|
|
100
|
+
* The first ask Cloudflare turns down (`rejected`: HTTP 401 or 403) turns
|
|
101
|
+
* the decision model **off** for the rest of the run: one
|
|
102
|
+
* `decision_model_off { reason: 'rejected' }` record and one line to `log`,
|
|
103
|
+
* and no ask after it is journaled or sent. The off switch lives in this
|
|
104
|
+
* asker, so each run (each `runEngine`) has its own. A timeout or an error
|
|
105
|
+
* doesn't turn it off; the next ask tries again.
|
|
85
106
|
*
|
|
86
107
|
* @example
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
* journal,
|
|
90
|
-
* asks: jevAsksBefore({ action, state }),
|
|
91
|
-
* })
|
|
108
|
+
* const ask = shadowAsker({ jev: { client: createClefClient({ credentials, model }) }, journal })
|
|
109
|
+
* await ask(jevAsksBefore({ action, state }))
|
|
92
110
|
*/
|
|
93
|
-
export const
|
|
111
|
+
export const shadowAsker = ({
|
|
94
112
|
jev,
|
|
95
113
|
journal,
|
|
96
|
-
asks,
|
|
97
|
-
timeout_ms,
|
|
98
114
|
}: {
|
|
99
|
-
jev:
|
|
115
|
+
jev: JevShadow
|
|
100
116
|
journal: Journal
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
role,
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
117
|
+
}): ShadowAsker => {
|
|
118
|
+
const { client, timeout_ms, log } = jev
|
|
119
|
+
let off = false
|
|
120
|
+
return async (asks) => {
|
|
121
|
+
for (const ask of asks) {
|
|
122
|
+
if (off) return
|
|
123
|
+
// Clef refuses it (a one-option choice, say), and the answer is
|
|
124
|
+
// obvious: no call and no record (#546).
|
|
125
|
+
if (!ClefRequestSchema.safeParse(ask.request).success) continue
|
|
126
|
+
const { job, ticket, role, request, fixed, acted } = ask
|
|
127
|
+
const asked = journal.append({
|
|
128
|
+
kind: 'jev_asked',
|
|
129
|
+
ticket,
|
|
130
|
+
role,
|
|
131
|
+
// Shadow mode unless the ask says its answer is acted on.
|
|
132
|
+
content: {
|
|
133
|
+
job,
|
|
134
|
+
request,
|
|
135
|
+
fixed,
|
|
136
|
+
model: client.model,
|
|
137
|
+
acted: acted ?? false,
|
|
138
|
+
},
|
|
119
139
|
})
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
140
|
+
const started = performance.now()
|
|
141
|
+
const reply = checkedReply(
|
|
142
|
+
await askWithTimeout({
|
|
143
|
+
jev: client,
|
|
144
|
+
ask,
|
|
145
|
+
timeout_ms: timeout_ms ?? DEFAULT_JEV_TIMEOUT_MS,
|
|
146
|
+
})
|
|
147
|
+
)
|
|
148
|
+
const ms = Math.round(performance.now() - started)
|
|
149
|
+
if (reply.ok) {
|
|
150
|
+
journal.append({
|
|
151
|
+
kind: 'jev_answered',
|
|
152
|
+
ticket,
|
|
153
|
+
role,
|
|
154
|
+
content: {
|
|
155
|
+
job,
|
|
156
|
+
asked_seq: asked.seq,
|
|
157
|
+
answers: reply.answers,
|
|
158
|
+
ms,
|
|
159
|
+
model: reply.model ?? client.model,
|
|
160
|
+
},
|
|
161
|
+
})
|
|
162
|
+
continue
|
|
163
|
+
}
|
|
123
164
|
journal.append({
|
|
124
|
-
kind: '
|
|
165
|
+
kind: 'jev_failed',
|
|
125
166
|
ticket,
|
|
126
167
|
role,
|
|
127
168
|
content: {
|
|
128
169
|
job,
|
|
129
170
|
asked_seq: asked.seq,
|
|
130
|
-
|
|
171
|
+
reason: reply.reason,
|
|
172
|
+
error: reply.error,
|
|
131
173
|
ms,
|
|
132
174
|
},
|
|
133
175
|
})
|
|
134
|
-
|
|
176
|
+
// Asks run at once on other tickets; only the first turns it off.
|
|
177
|
+
if (reply.reason === 'rejected' && !off) {
|
|
178
|
+
off = true
|
|
179
|
+
const detail = `${reply.error} No more asks this run.`
|
|
180
|
+
journal.append({
|
|
181
|
+
kind: 'decision_model_off',
|
|
182
|
+
ticket: null,
|
|
183
|
+
role: null,
|
|
184
|
+
content: {
|
|
185
|
+
model: client.model,
|
|
186
|
+
reason: 'rejected',
|
|
187
|
+
detail,
|
|
188
|
+
},
|
|
189
|
+
})
|
|
190
|
+
log?.(`decision model off: ${detail}`)
|
|
191
|
+
}
|
|
135
192
|
}
|
|
136
|
-
journal.append({
|
|
137
|
-
kind: 'jev_failed',
|
|
138
|
-
ticket,
|
|
139
|
-
role,
|
|
140
|
-
content: {
|
|
141
|
-
job,
|
|
142
|
-
asked_seq: asked.seq,
|
|
143
|
-
reason: reply.reason,
|
|
144
|
-
error: reply.error,
|
|
145
|
-
ms,
|
|
146
|
-
},
|
|
147
|
-
})
|
|
148
193
|
}
|
|
149
194
|
}
|