@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,212 @@
|
|
|
1
|
+
import { join } from 'node:path'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The decision model's Cloudflare credentials (#534), from **Luca's own env
|
|
5
|
+
* file**, `$XDG_CONFIG_HOME/luca/.env` (or `~/.config/luca/.env`). The engine
|
|
6
|
+
* reads it itself, so it works the same for board-started and
|
|
7
|
+
* terminal-started runs. It never reads a repo's `.env`. A key set in the
|
|
8
|
+
* process env wins over the file, key by key, for tests and CI.
|
|
9
|
+
*
|
|
10
|
+
* ```
|
|
11
|
+
* CLOUDFLARE_ACCOUNT_ID=your-account-id
|
|
12
|
+
* CLOUDFLARE_API_TOKEN=your-workers-ai-token
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* `CLOUDFLARE_AUTH_TOKEN` (the name Cloudflare's Clef page uses) works as the
|
|
16
|
+
* token too; `CLOUDFLARE_API_TOKEN` wins when both are set. Nothing here
|
|
17
|
+
* ever puts the token in a summary or a log line.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** The Cloudflare account the decision model runs on. */
|
|
21
|
+
export const CLOUDFLARE_ACCOUNT_ID = 'CLOUDFLARE_ACCOUNT_ID'
|
|
22
|
+
|
|
23
|
+
/** The Workers AI token, the name wrangler and Cloudflare's tools use. */
|
|
24
|
+
export const CLOUDFLARE_API_TOKEN = 'CLOUDFLARE_API_TOKEN'
|
|
25
|
+
|
|
26
|
+
/** Another name for the token, from Cloudflare's Clef page. */
|
|
27
|
+
export const CLOUDFLARE_AUTH_TOKEN = 'CLOUDFLARE_AUTH_TOKEN'
|
|
28
|
+
|
|
29
|
+
/** The keys the decision model needs, in the order they are named. */
|
|
30
|
+
export const DECISION_MODEL_KEYS = [
|
|
31
|
+
CLOUDFLARE_ACCOUNT_ID,
|
|
32
|
+
CLOUDFLARE_API_TOKEN,
|
|
33
|
+
] as const
|
|
34
|
+
|
|
35
|
+
export type DecisionModelKey = (typeof DECISION_MODEL_KEYS)[number]
|
|
36
|
+
|
|
37
|
+
/** A process env, or a stand-in for one. */
|
|
38
|
+
export type Env = Record<string, string | undefined>
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The credentials, or the keys that are missing. `file` is Luca's env file,
|
|
42
|
+
* read or not.
|
|
43
|
+
*/
|
|
44
|
+
export type DecisionModelCredentials =
|
|
45
|
+
| { ok: true; account_id: string; api_token: string; file: string }
|
|
46
|
+
| { ok: false; missing: DecisionModelKey[]; file: string }
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Luca's own env file: `$XDG_CONFIG_HOME/luca/.env` when `XDG_CONFIG_HOME`
|
|
50
|
+
* is set and not empty, else `<home>/.config/luca/.env`. Pure.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* lucaEnvFile({ env: {}, home: '/Users/me' }) // '/Users/me/.config/luca/.env'
|
|
54
|
+
*/
|
|
55
|
+
export const lucaEnvFile = ({
|
|
56
|
+
env,
|
|
57
|
+
home,
|
|
58
|
+
}: {
|
|
59
|
+
env: Env
|
|
60
|
+
home: string
|
|
61
|
+
}): string => {
|
|
62
|
+
const xdg = env.XDG_CONFIG_HOME ?? ''
|
|
63
|
+
return join(xdg === '' ? join(home, '.config') : xdg, 'luca', '.env')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** A value without one pair of matching quotes around it. */
|
|
67
|
+
const unquote = (value: string): string => {
|
|
68
|
+
const first = value.at(0)
|
|
69
|
+
return value.length >= 2 &&
|
|
70
|
+
(first === '"' || first === "'") &&
|
|
71
|
+
value.at(-1) === first
|
|
72
|
+
? value.slice(1, -1)
|
|
73
|
+
: value
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The keys and values of an env file: `KEY=VALUE` lines, with an optional
|
|
78
|
+
* `export ` in front and the value trimmed and taken out of one pair of
|
|
79
|
+
* single or double quotes. Blank lines, `#` comments, and lines with no `=`
|
|
80
|
+
* or no key are skipped. No escapes and no `${...}`. Pure.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* parseEnvFile('# keys\nexport A="one two"\nB=2') // { A: 'one two', B: '2' }
|
|
84
|
+
*/
|
|
85
|
+
export const parseEnvFile = (text: string): Record<string, string> => {
|
|
86
|
+
const values: Record<string, string> = {}
|
|
87
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
88
|
+
const line = raw.trim()
|
|
89
|
+
if (line === '' || line.startsWith('#')) continue
|
|
90
|
+
const at = line.indexOf('=')
|
|
91
|
+
if (at === -1) continue
|
|
92
|
+
const key = line
|
|
93
|
+
.slice(0, at)
|
|
94
|
+
.trim()
|
|
95
|
+
.replace(/^export\s+/, '')
|
|
96
|
+
.trim()
|
|
97
|
+
if (key === '') continue
|
|
98
|
+
values[key] = unquote(line.slice(at + 1).trim())
|
|
99
|
+
}
|
|
100
|
+
return values
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** A file's text, or `null` when it is missing or can't be read. */
|
|
104
|
+
const readText = async (path: string): Promise<string | null> => {
|
|
105
|
+
try {
|
|
106
|
+
const file = Bun.file(path)
|
|
107
|
+
return (await file.exists()) ? await file.text() : null
|
|
108
|
+
} catch {
|
|
109
|
+
return null
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The first of `values` that is set and not empty, or `null`. */
|
|
114
|
+
const firstSet = (values: (string | undefined)[]): string | null =>
|
|
115
|
+
values.find((value) => value !== undefined && value !== '') ?? null
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The decision model's credentials: each key from the process env, else
|
|
119
|
+
* from Luca's env file (`lucaEnvFile`). An empty value is missing. Never
|
|
120
|
+
* throws: a missing or unreadable file reads as empty.
|
|
121
|
+
*
|
|
122
|
+
* @param env - The process env, such as `process.env`. Tests pass their own.
|
|
123
|
+
* @param home - The home folder, such as `homedir()`. Tests pass a temp one.
|
|
124
|
+
* @param read_file - Reads the file; `null` for none. Defaults to Bun's.
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* const credentials = await loadDecisionModelCredentials({ env: process.env, home: homedir() })
|
|
128
|
+
* if (!credentials.ok) console.log(`Add ${credentials.missing.join(', ')} to ${credentials.file}`)
|
|
129
|
+
*/
|
|
130
|
+
export const loadDecisionModelCredentials = async ({
|
|
131
|
+
env,
|
|
132
|
+
home,
|
|
133
|
+
read_file,
|
|
134
|
+
}: {
|
|
135
|
+
env: Env
|
|
136
|
+
home: string
|
|
137
|
+
read_file?: (path: string) => Promise<string | null>
|
|
138
|
+
}): Promise<DecisionModelCredentials> => {
|
|
139
|
+
const file = lucaEnvFile({ env, home })
|
|
140
|
+
const text = await (read_file ?? readText)(file).catch(() => null)
|
|
141
|
+
const values = text === null ? {} : parseEnvFile(text)
|
|
142
|
+
const account_id = firstSet([
|
|
143
|
+
env[CLOUDFLARE_ACCOUNT_ID],
|
|
144
|
+
values[CLOUDFLARE_ACCOUNT_ID],
|
|
145
|
+
])
|
|
146
|
+
const api_token = firstSet([
|
|
147
|
+
env[CLOUDFLARE_API_TOKEN],
|
|
148
|
+
env[CLOUDFLARE_AUTH_TOKEN],
|
|
149
|
+
values[CLOUDFLARE_API_TOKEN],
|
|
150
|
+
values[CLOUDFLARE_AUTH_TOKEN],
|
|
151
|
+
])
|
|
152
|
+
if (account_id !== null && api_token !== null) {
|
|
153
|
+
return { ok: true, account_id, api_token, file }
|
|
154
|
+
}
|
|
155
|
+
const missing = DECISION_MODEL_KEYS.filter((key) =>
|
|
156
|
+
key === CLOUDFLARE_ACCOUNT_ID ? account_id === null : api_token === null
|
|
157
|
+
)
|
|
158
|
+
return { ok: false, missing, file }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Which keys are set and which are missing, and the file: safe to print,
|
|
163
|
+
* since it never holds a value. Pure.
|
|
164
|
+
*
|
|
165
|
+
* @example
|
|
166
|
+
* describeDecisionModelCredentials(credentials)
|
|
167
|
+
* // 'CLOUDFLARE_ACCOUNT_ID set, CLOUDFLARE_API_TOKEN missing (Luca's env file: /Users/me/.config/luca/.env)'
|
|
168
|
+
*/
|
|
169
|
+
export const describeDecisionModelCredentials = (
|
|
170
|
+
credentials: DecisionModelCredentials
|
|
171
|
+
): string => {
|
|
172
|
+
const missing: string[] = credentials.ok ? [] : credentials.missing
|
|
173
|
+
const keys = DECISION_MODEL_KEYS.map(
|
|
174
|
+
(key) => `${key} ${missing.includes(key) ? 'missing' : 'set'}`
|
|
175
|
+
)
|
|
176
|
+
return `${keys.join(', ')} (Luca's env file: ${credentials.file})`
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* The lines to add to Luca's env file for the missing keys, with
|
|
181
|
+
* placeholders. Pure.
|
|
182
|
+
*
|
|
183
|
+
* @example
|
|
184
|
+
* missingKeyLines({ missing: ['CLOUDFLARE_API_TOKEN'] }) // ['CLOUDFLARE_API_TOKEN=your-workers-ai-token']
|
|
185
|
+
*/
|
|
186
|
+
export const missingKeyLines = ({
|
|
187
|
+
missing,
|
|
188
|
+
}: {
|
|
189
|
+
missing: DecisionModelKey[]
|
|
190
|
+
}): string[] =>
|
|
191
|
+
missing.map((key) =>
|
|
192
|
+
key === CLOUDFLARE_ACCOUNT_ID
|
|
193
|
+
? `${key}=your-account-id`
|
|
194
|
+
: `${key}=your-workers-ai-token`
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* One plain sentence saying the decision model has no credentials: the
|
|
199
|
+
* file and the keys to add. Never holds a value. Pure.
|
|
200
|
+
*
|
|
201
|
+
* @example
|
|
202
|
+
* noCredentialsText({ missing: ['CLOUDFLARE_API_TOKEN'], file: '/Users/me/.config/luca/.env' })
|
|
203
|
+
* // 'No Cloudflare credentials: add CLOUDFLARE_API_TOKEN to /Users/me/.config/luca/.env.'
|
|
204
|
+
*/
|
|
205
|
+
export const noCredentialsText = ({
|
|
206
|
+
missing,
|
|
207
|
+
file,
|
|
208
|
+
}: {
|
|
209
|
+
missing: DecisionModelKey[]
|
|
210
|
+
file: string
|
|
211
|
+
}): string =>
|
|
212
|
+
`No Cloudflare credentials: add ${missing.join(' and ')} to ${file}.`
|
package/engine/jev/jev-jobs.ts
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
|
+
import chunk from 'lodash/chunk'
|
|
1
2
|
import fromPairs from 'lodash/fromPairs'
|
|
2
3
|
|
|
3
|
-
import
|
|
4
|
+
import {
|
|
5
|
+
CLEF_MAX_QUESTIONS,
|
|
6
|
+
type JevFixed,
|
|
7
|
+
type JevJob,
|
|
8
|
+
type JevQuestion,
|
|
9
|
+
type JevRequest,
|
|
10
|
+
} from './jev-schemas'
|
|
11
|
+
import { offeredModels, type RoutedRole } from './model-pick'
|
|
4
12
|
|
|
5
13
|
import type { Finding } from '../agents/role-results'
|
|
14
|
+
import { DEFAULT_MODEL, modelsOf } from '../config/engine-config'
|
|
6
15
|
import type { EngineAction } from '../core/decide'
|
|
7
16
|
import type { TicketSnapshot } from '../intake/intake-schemas'
|
|
8
17
|
import type { JournalRecord } from '../journal/journal-record'
|
|
@@ -23,17 +32,15 @@ export type JevAsk = {
|
|
|
23
32
|
role: string | null
|
|
24
33
|
request: JevRequest
|
|
25
34
|
fixed: JevFixed
|
|
35
|
+
/**
|
|
36
|
+
* Whether the answer can change what the engine does (#603); left out,
|
|
37
|
+
* a shadow ask.
|
|
38
|
+
*/
|
|
39
|
+
acted?: boolean
|
|
26
40
|
}
|
|
27
41
|
|
|
28
|
-
/** The
|
|
29
|
-
export const
|
|
30
|
-
'claude-opus-5-5',
|
|
31
|
-
'claude-sonnet-5',
|
|
32
|
-
'claude-haiku-4-5',
|
|
33
|
-
] as const
|
|
34
|
-
|
|
35
|
-
/** The model the engine really uses for every ticket. */
|
|
36
|
-
export const JEV_FIXED_MODEL = 'claude-opus-5-5'
|
|
42
|
+
/** The model the engine falls back on, and every reviewer's. */
|
|
43
|
+
export const JEV_FIXED_MODEL = DEFAULT_MODEL
|
|
37
44
|
|
|
38
45
|
/**
|
|
39
46
|
* The stock skills Jev may give an agent. The engine gives agents none, so
|
|
@@ -133,42 +140,74 @@ const createTicketWorktreeAsks = ({
|
|
|
133
140
|
})
|
|
134
141
|
.join('\n')
|
|
135
142
|
const base = ticketState({ state, ticket })
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
143
|
+
// With one ticket left the order is obvious, and Clef refuses a
|
|
144
|
+
// one-option choice (#546), so there's nothing to ask.
|
|
145
|
+
const orderAsk: JevAsk[] =
|
|
146
|
+
candidates.length < 2
|
|
147
|
+
? []
|
|
148
|
+
: [
|
|
149
|
+
{
|
|
150
|
+
job: 'ticket_order',
|
|
151
|
+
ticket,
|
|
152
|
+
role: null,
|
|
153
|
+
request: {
|
|
154
|
+
state: { ...base, candidates: candidateList },
|
|
155
|
+
questions: {
|
|
156
|
+
next: choice({
|
|
157
|
+
instructions:
|
|
158
|
+
'Which of these tickets should be built next?',
|
|
159
|
+
options: candidates.map(({ number }) =>
|
|
160
|
+
ticketKey(number)
|
|
161
|
+
),
|
|
162
|
+
}),
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
fixed: { next: ticketKey(ticket) },
|
|
166
|
+
},
|
|
167
|
+
]
|
|
168
|
+
return orderAsk
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Which model a fresh agent of a routed role should run on: the ticket's
|
|
173
|
+
* facts, the role, and which try this is (#602). It offers the models at or
|
|
174
|
+
* above the role's floor (#603), and is acted on when that floor is below
|
|
175
|
+
* Opus; a floor of Opus is asked with all three, in shadow mode.
|
|
176
|
+
*/
|
|
177
|
+
const agentModelAsk = ({
|
|
178
|
+
state,
|
|
179
|
+
ticket,
|
|
180
|
+
role,
|
|
181
|
+
attempt,
|
|
182
|
+
}: {
|
|
183
|
+
state: RunState
|
|
184
|
+
ticket: number | null
|
|
185
|
+
role: RoutedRole
|
|
186
|
+
attempt: number
|
|
187
|
+
}): JevAsk => {
|
|
188
|
+
const models =
|
|
189
|
+
state.config === null ? null : modelsOf({ config: state.config })
|
|
190
|
+
const floor = models?.floors[role] ?? JEV_FIXED_MODEL
|
|
191
|
+
return {
|
|
192
|
+
job: 'agent_model',
|
|
193
|
+
ticket,
|
|
194
|
+
role,
|
|
195
|
+
request: {
|
|
196
|
+
state: {
|
|
197
|
+
...ticketState({ state, ticket }),
|
|
198
|
+
role,
|
|
199
|
+
try: String(attempt),
|
|
152
200
|
},
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
role: null,
|
|
159
|
-
request: {
|
|
160
|
-
state: base,
|
|
161
|
-
questions: {
|
|
162
|
-
model: choice({
|
|
163
|
-
instructions:
|
|
164
|
-
'Which model should build this ticket? Pick the cheapest that will get it right.',
|
|
165
|
-
options: JEV_MODEL_OPTIONS,
|
|
166
|
-
}),
|
|
167
|
-
},
|
|
201
|
+
questions: {
|
|
202
|
+
model: choice({
|
|
203
|
+
instructions: `Which model should the ${role} agent run on? Pick the cheapest that will get it right.`,
|
|
204
|
+
options: offeredModels({ floor }),
|
|
205
|
+
}),
|
|
168
206
|
},
|
|
169
|
-
fixed: { model: JEV_FIXED_MODEL },
|
|
170
207
|
},
|
|
171
|
-
|
|
208
|
+
fixed: { model: JEV_FIXED_MODEL },
|
|
209
|
+
acted: floor !== JEV_FIXED_MODEL,
|
|
210
|
+
}
|
|
172
211
|
}
|
|
173
212
|
|
|
174
213
|
const agentSkillsAsk = ({
|
|
@@ -200,9 +239,19 @@ const agentSkillsAsk = ({
|
|
|
200
239
|
),
|
|
201
240
|
})
|
|
202
241
|
|
|
242
|
+
/** The roles whose fresh agents get an `agent_model` ask on a ticket. */
|
|
243
|
+
const ROUTED_TICKET_ROLES: readonly RoutedRole[] = [
|
|
244
|
+
'test-writer',
|
|
245
|
+
'implementer',
|
|
246
|
+
]
|
|
247
|
+
|
|
248
|
+
const isRoutedTicketRole = (role: string): role is RoutedRole =>
|
|
249
|
+
ROUTED_TICKET_ROLES.some((routed) => routed === role)
|
|
250
|
+
|
|
203
251
|
/**
|
|
204
|
-
* The asks to make before the engine carries out `action`: ticket order
|
|
205
|
-
*
|
|
252
|
+
* The asks to make before the engine carries out `action`: ticket order
|
|
253
|
+
* before a ticket starts, skills before each agent, and the model before
|
|
254
|
+
* each fresh test-writer, implementer, and learner.
|
|
206
255
|
*/
|
|
207
256
|
export const jevAsksBefore = ({
|
|
208
257
|
action,
|
|
@@ -214,12 +263,30 @@ export const jevAsksBefore = ({
|
|
|
214
263
|
switch (action.type) {
|
|
215
264
|
case 'create_ticket_worktree':
|
|
216
265
|
return createTicketWorktreeAsks({ state, ticket: action.ticket })
|
|
217
|
-
case 'launch_agent':
|
|
266
|
+
case 'launch_agent': {
|
|
267
|
+
const { ticket, role } = action
|
|
268
|
+
const failed = state.tickets[ticket]?.failed_tries[role] ?? 0
|
|
269
|
+
return [
|
|
270
|
+
...(isRoutedTicketRole(role)
|
|
271
|
+
? [
|
|
272
|
+
agentModelAsk({
|
|
273
|
+
state,
|
|
274
|
+
ticket,
|
|
275
|
+
role,
|
|
276
|
+
attempt: failed + 1,
|
|
277
|
+
}),
|
|
278
|
+
]
|
|
279
|
+
: []),
|
|
280
|
+
agentSkillsAsk({ state, ticket, role }),
|
|
281
|
+
]
|
|
282
|
+
}
|
|
283
|
+
case 'launch_learner':
|
|
218
284
|
return [
|
|
219
|
-
|
|
285
|
+
agentModelAsk({
|
|
220
286
|
state,
|
|
221
|
-
ticket:
|
|
222
|
-
role:
|
|
287
|
+
ticket: null,
|
|
288
|
+
role: 'learner',
|
|
289
|
+
attempt: state.memory.learner.failed_tries + 1,
|
|
223
290
|
}),
|
|
224
291
|
]
|
|
225
292
|
default:
|
|
@@ -306,17 +373,33 @@ const findingText = ({ title, detail, file }: Finding): string =>
|
|
|
306
373
|
.filter((part) => part !== null && part !== '')
|
|
307
374
|
.join('\n')
|
|
308
375
|
|
|
376
|
+
/**
|
|
377
|
+
* The severity of each finding a ticket reviewer reported, at most
|
|
378
|
+
* `CLEF_MAX_QUESTIONS` findings to an ask, since Clef takes no more.
|
|
379
|
+
*/
|
|
380
|
+
const findingSeverityAsks = ({
|
|
381
|
+
record,
|
|
382
|
+
state,
|
|
383
|
+
}: {
|
|
384
|
+
record: JournalRecord
|
|
385
|
+
state: RunState
|
|
386
|
+
}): JevAsk[] => {
|
|
387
|
+
if (record.kind !== 'agent_finished') return []
|
|
388
|
+
if (record.content.role !== 'ticket-reviewer') return []
|
|
389
|
+
return chunk(record.content.result.findings, CLEF_MAX_QUESTIONS).map(
|
|
390
|
+
(findings) => findingSeverityAsk({ record, state, findings })
|
|
391
|
+
)
|
|
392
|
+
}
|
|
393
|
+
|
|
309
394
|
const findingSeverityAsk = ({
|
|
310
395
|
record,
|
|
311
396
|
state,
|
|
397
|
+
findings,
|
|
312
398
|
}: {
|
|
313
399
|
record: JournalRecord
|
|
314
400
|
state: RunState
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
if (record.content.role !== 'ticket-reviewer') return null
|
|
318
|
-
const { findings } = record.content.result
|
|
319
|
-
if (findings.length === 0) return null
|
|
401
|
+
findings: Finding[]
|
|
402
|
+
}): JevAsk => {
|
|
320
403
|
const key = (finding: Finding) => `finding_${questionId(finding.id)}`
|
|
321
404
|
return {
|
|
322
405
|
job: 'finding_severity',
|
|
@@ -361,9 +444,10 @@ export const jevAsksAfter = ({
|
|
|
361
444
|
records: JournalRecord[]
|
|
362
445
|
state: RunState
|
|
363
446
|
}): JevAsk[] =>
|
|
364
|
-
records.flatMap((record) =>
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
447
|
+
records.flatMap((record) => {
|
|
448
|
+
const failure = failureAsk({ record, state })
|
|
449
|
+
return [
|
|
450
|
+
...(failure === null ? [] : [failure]),
|
|
451
|
+
...findingSeverityAsks({ record, state }),
|
|
452
|
+
]
|
|
453
|
+
})
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* The **decision model** (Cloudflare's Clef today; Jev, TypeSafe's labeling
|
|
5
|
+
* model, before #534) runs in **shadow mode**: the engine asks it and
|
|
6
|
+
* journals its answers, but acts on its own fixed choices. These are the
|
|
7
|
+
* shapes of what the engine asks and what it keeps of each answer. The
|
|
8
|
+
* names (and the journal's `jev_*` record kinds) keep the Jev name, so old
|
|
9
|
+
* journals still read.
|
|
7
10
|
*/
|
|
8
11
|
|
|
9
12
|
/** Pick one of the criteria's keys. */
|
|
@@ -44,6 +47,43 @@ export const JevRequestSchema = z.object({
|
|
|
44
47
|
|
|
45
48
|
export type JevRequest = z.infer<typeof JevRequestSchema>
|
|
46
49
|
|
|
50
|
+
/** The most questions Clef takes in one request. */
|
|
51
|
+
export const CLEF_MAX_QUESTIONS = 64
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* A request Clef will take (#546): 1 to 64 questions, 2 to 255 options per
|
|
55
|
+
* choice, and 2 to 10 levels per score. Clef refuses anything else, so the
|
|
56
|
+
* engine checks each ask against this before sending it. Journals keep the
|
|
57
|
+
* looser `JevRequestSchema`, so old asks still read.
|
|
58
|
+
*/
|
|
59
|
+
export const ClefRequestSchema = z.object({
|
|
60
|
+
state: z.record(z.string(), z.string()),
|
|
61
|
+
questions: z
|
|
62
|
+
.record(
|
|
63
|
+
z.string(),
|
|
64
|
+
z.discriminatedUnion('type', [
|
|
65
|
+
JevChoiceQuestionSchema.refine(
|
|
66
|
+
({ criteria }) => {
|
|
67
|
+
const options = Object.keys(criteria).length
|
|
68
|
+
return options >= 2 && options <= 255
|
|
69
|
+
},
|
|
70
|
+
{ message: 'A choice takes 2 to 255 options.' }
|
|
71
|
+
),
|
|
72
|
+
JevScoreQuestionSchema,
|
|
73
|
+
JevNoulQuestionSchema,
|
|
74
|
+
])
|
|
75
|
+
)
|
|
76
|
+
.refine(
|
|
77
|
+
(questions) => {
|
|
78
|
+
const count = Object.keys(questions).length
|
|
79
|
+
return count >= 1 && count <= CLEF_MAX_QUESTIONS
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
message: `A request takes 1 to ${CLEF_MAX_QUESTIONS} questions.`,
|
|
83
|
+
}
|
|
84
|
+
),
|
|
85
|
+
})
|
|
86
|
+
|
|
47
87
|
/**
|
|
48
88
|
* What the engine keeps of one answer. `value` is the choice, score, or
|
|
49
89
|
* probability Jev gave; `raw` is the answer exactly as it came back.
|
|
@@ -57,9 +97,14 @@ export const JevAnswerSchema = z.object({
|
|
|
57
97
|
|
|
58
98
|
export type JevAnswer = z.infer<typeof JevAnswerSchema>
|
|
59
99
|
|
|
60
|
-
/**
|
|
100
|
+
/**
|
|
101
|
+
* The jobs the engine asks Jev about. `ticket_model` (one model per ticket)
|
|
102
|
+
* is no longer asked; `agent_model` (one per agent) replaced it in #602. It
|
|
103
|
+
* stays so old journals read.
|
|
104
|
+
*/
|
|
61
105
|
export const JevJobSchema = z.enum([
|
|
62
106
|
'ticket_model',
|
|
107
|
+
'agent_model',
|
|
63
108
|
'agent_skills',
|
|
64
109
|
'ticket_order',
|
|
65
110
|
'failure_kind',
|
|
@@ -68,9 +113,17 @@ export const JevJobSchema = z.enum([
|
|
|
68
113
|
|
|
69
114
|
export type JevJob = z.infer<typeof JevJobSchema>
|
|
70
115
|
|
|
71
|
-
/**
|
|
116
|
+
/**
|
|
117
|
+
* Why a decision model call gave no answers. New calls write
|
|
118
|
+
* `missing_credentials` (no Cloudflare account id or token), `rejected`
|
|
119
|
+
* (Cloudflare turned the token down: HTTP 401 or 403), `timeout`, or
|
|
120
|
+
* `error`. `missing_key` is Jev's old reason (it had no key), kept so old
|
|
121
|
+
* journals read.
|
|
122
|
+
*/
|
|
72
123
|
export const JevFailureReasonSchema = z.enum([
|
|
73
124
|
'missing_key',
|
|
125
|
+
'missing_credentials',
|
|
126
|
+
'rejected',
|
|
74
127
|
'timeout',
|
|
75
128
|
'error',
|
|
76
129
|
])
|
|
@@ -87,3 +140,31 @@ export const JevFixedSchema = z.record(
|
|
|
87
140
|
)
|
|
88
141
|
|
|
89
142
|
export type JevFixed = z.infer<typeof JevFixedSchema>
|
|
143
|
+
|
|
144
|
+
/** The Clef models on Workers AI the engine can ask (#534). */
|
|
145
|
+
export const DECISION_MODEL_IDS = [
|
|
146
|
+
'@cf/cloudflare/clef',
|
|
147
|
+
'@cf/cloudflare/clef-flash',
|
|
148
|
+
] as const
|
|
149
|
+
|
|
150
|
+
/** A decision model's Workers AI id. */
|
|
151
|
+
export const DecisionModelIdSchema = z.enum(DECISION_MODEL_IDS)
|
|
152
|
+
|
|
153
|
+
export type DecisionModelId = z.infer<typeof DecisionModelIdSchema>
|
|
154
|
+
|
|
155
|
+
/** The model the engine asks when the config doesn't say: Clef, the accurate one. */
|
|
156
|
+
export const DEFAULT_DECISION_MODEL: DecisionModelId = '@cf/cloudflare/clef'
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Why the decision model is off for a run: `no_credentials` (Luca's env
|
|
160
|
+
* file and the process env have no Cloudflare account id or token), or
|
|
161
|
+
* `rejected` (Cloudflare turned the token down on an ask).
|
|
162
|
+
*/
|
|
163
|
+
export const DecisionModelOffReasonSchema = z.enum([
|
|
164
|
+
'no_credentials',
|
|
165
|
+
'rejected',
|
|
166
|
+
])
|
|
167
|
+
|
|
168
|
+
export type DecisionModelOffReason = z.infer<
|
|
169
|
+
typeof DecisionModelOffReasonSchema
|
|
170
|
+
>
|