@alecsibilia/luca 14.0.0-alpha.11 → 14.0.0-alpha.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/board/client/board-panel.tsx +1 -1
- package/board/package.json +1 -1
- package/board/server/board-vocabulary.ts +9 -1
- package/board/server/luca-version.ts +1 -1
- package/board/server/make-rows.ts +7 -2
- package/board/server/reduce-board.ts +4 -0
- package/board/shared/board-state.ts +3 -2
- package/engine/cli/computer-checks.ts +39 -3
- package/engine/cli/doctor-command.ts +1 -0
- package/engine/cli/doctor.ts +7 -1
- package/engine/cli/init-command.ts +1 -0
- package/engine/cli/init.ts +6 -0
- package/engine/cli/luca-run.ts +18 -6
- package/engine/cli/run-modes.ts +79 -22
- package/engine/cli/upgrade-command.ts +1 -0
- package/engine/config/config-reload.ts +24 -3
- package/engine/config/engine-config.ts +51 -1
- package/engine/core/execute-build.ts +8 -1
- package/engine/core/execute.ts +29 -24
- package/engine/gates/gate-runner.ts +17 -11
- package/engine/gates/test-runner.ts +21 -3
- package/engine/index.ts +25 -8
- package/engine/jev/clef-client.ts +259 -0
- package/engine/jev/decision-model-credentials.ts +212 -0
- package/engine/jev/jev-schemas.ts +43 -4
- package/engine/jev/jev-shadow.ts +95 -63
- package/engine/journal/journal-record.ts +36 -3
- package/engine/journal/replay.ts +4 -1
- package/engine/shell/run-command.ts +26 -0
- package/package.json +1 -1
- package/skills/luca-retro/SKILL.md +2 -2
- package/skills/luca-unstick/SKILL.md +1 -1
- package/engine/jev/jev-client.ts +0 -173
package/CHANGELOG.md
CHANGED
|
@@ -1327,7 +1327,7 @@ export const BoardPanel = ({
|
|
|
1327
1327
|
) : null}
|
|
1328
1328
|
{state.jev.asked > 0 ? (
|
|
1329
1329
|
<Text style={styles.small}>
|
|
1330
|
-
|
|
1330
|
+
Decision model, in shadow mode: {state.jev.asked} asked,{' '}
|
|
1331
1331
|
{state.jev.answered} answered, {state.jev.failed} without an
|
|
1332
1332
|
answer. The engine doesn't act on them.
|
|
1333
1333
|
</Text>
|
package/board/package.json
CHANGED
|
@@ -304,10 +304,17 @@ export const BOARD_VOCABULARY = {
|
|
|
304
304
|
worktrees_removed: z.looseObject({
|
|
305
305
|
paths: z.array(z.string()).catch([]),
|
|
306
306
|
}),
|
|
307
|
-
// Jev in shadow mode:
|
|
307
|
+
// The decision model (Clef; Jev in older journals) in shadow mode:
|
|
308
|
+
// counted, never acted on.
|
|
308
309
|
jev_asked: JevJobSchema,
|
|
309
310
|
jev_answered: JevJobSchema,
|
|
310
311
|
jev_failed: JevJobSchema,
|
|
312
|
+
/**
|
|
313
|
+
* The decision model is off for the rest of the run (#534): no
|
|
314
|
+
* credentials, or Cloudflare turned them down. The board reads only the
|
|
315
|
+
* plain-words `detail`.
|
|
316
|
+
*/
|
|
317
|
+
decision_model_off: z.looseObject({ detail: z.string().catch('') }),
|
|
311
318
|
// Agent messages: sent inside an agent's turn, so they move nothing.
|
|
312
319
|
agent_message: z.looseObject({
|
|
313
320
|
from: z.string(),
|
|
@@ -490,6 +497,7 @@ const BoardEntrySchema = z.discriminatedUnion('kind', [
|
|
|
490
497
|
entry({ kind: 'jev_asked' }),
|
|
491
498
|
entry({ kind: 'jev_answered' }),
|
|
492
499
|
entry({ kind: 'jev_failed' }),
|
|
500
|
+
entry({ kind: 'decision_model_off' }),
|
|
493
501
|
entry({ kind: 'agent_message' }),
|
|
494
502
|
entry({ kind: 'agent_message_delivered' }),
|
|
495
503
|
entry({ kind: 'shared_git_changed' }),
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** The Luca version of this board, stamped when the package was packed. */
|
|
2
|
-
export const LUCA_VERSION: string = "14.0.0-alpha.
|
|
2
|
+
export const LUCA_VERSION: string = "14.0.0-alpha.13"
|
|
@@ -227,8 +227,8 @@ const nothingToDoText = ({
|
|
|
227
227
|
|
|
228
228
|
/**
|
|
229
229
|
* What a record means in one line, or `null` for records that are noise in
|
|
230
|
-
* a chat (snapshots, session summaries, a clean leftover scan,
|
|
231
|
-
* mode, ...).
|
|
230
|
+
* a chat (snapshots, session summaries, a clean leftover scan, the decision
|
|
231
|
+
* model's asks in shadow mode, ...).
|
|
232
232
|
*/
|
|
233
233
|
export const describeRecord = ({
|
|
234
234
|
before,
|
|
@@ -257,6 +257,11 @@ export const describeRecord = ({
|
|
|
257
257
|
text: `Config reloaded: ${record.content.changes.map(({ field }) => field).join(', ')} changed.`,
|
|
258
258
|
tone: 'info',
|
|
259
259
|
})
|
|
260
|
+
case 'decision_model_off':
|
|
261
|
+
return event({
|
|
262
|
+
text: `Decision model off: ${record.content.detail}`,
|
|
263
|
+
tone: 'info',
|
|
264
|
+
})
|
|
260
265
|
case 'config_reload_refused':
|
|
261
266
|
return event({
|
|
262
267
|
text: `Config not reloaded, so the run keeps its own: ${record.content.reason.split('\n')[0] ?? ''}`,
|
|
@@ -114,6 +114,7 @@ const QUIET_KINDS = new Set([
|
|
|
114
114
|
'jev_asked',
|
|
115
115
|
'jev_answered',
|
|
116
116
|
'jev_failed',
|
|
117
|
+
'decision_model_off',
|
|
117
118
|
'agent_message',
|
|
118
119
|
'agent_message_delivered',
|
|
119
120
|
'shared_git_changed',
|
|
@@ -1298,6 +1299,9 @@ const applyKind = ({
|
|
|
1298
1299
|
...state,
|
|
1299
1300
|
jev: { ...state.jev, failed: state.jev.failed + 1 },
|
|
1300
1301
|
}
|
|
1302
|
+
// The decision model turning off is only a chat line (#534).
|
|
1303
|
+
case 'decision_model_off':
|
|
1304
|
+
return state
|
|
1301
1305
|
case 'agent_message': {
|
|
1302
1306
|
const refused = record.content.status === 'refused'
|
|
1303
1307
|
return {
|
|
@@ -482,8 +482,9 @@ export const RunInfoSchema = z.object({
|
|
|
482
482
|
export type RunInfo = z.infer<typeof RunInfoSchema>
|
|
483
483
|
|
|
484
484
|
/**
|
|
485
|
-
* Jev in shadow mode: how often
|
|
486
|
-
* engine
|
|
485
|
+
* The decision model (Clef; Jev in older journals) in shadow mode: how often
|
|
486
|
+
* the engine asked it and what came back. The engine never acts on the
|
|
487
|
+
* answers, so the board only counts them.
|
|
487
488
|
*/
|
|
488
489
|
export const JevCountsSchema = z.object({
|
|
489
490
|
asked: z.number().int().min(0),
|
|
@@ -26,14 +26,20 @@ import { claudeSkillsDir, skillDrift } from './luca-skills'
|
|
|
26
26
|
import { isRight, rightEntry, userMuninnEntry } from './muninn-entry'
|
|
27
27
|
|
|
28
28
|
import { LUCA_PACKAGE } from '../config/luca-version'
|
|
29
|
+
import {
|
|
30
|
+
loadDecisionModelCredentials,
|
|
31
|
+
missingKeyLines,
|
|
32
|
+
type Env,
|
|
33
|
+
} from '../jev/decision-model-credentials'
|
|
34
|
+
import { DEFAULT_DECISION_MODEL } from '../jev/jev-schemas'
|
|
29
35
|
import { READY_LABEL } from '../tracker/tracker'
|
|
30
36
|
|
|
31
37
|
/**
|
|
32
38
|
* `luca doctor`'s checks of this computer, read-only: Bun, the `luca`
|
|
33
39
|
* copies on the PATH, Claude Code, the `gh` login, Paseo and its plugins,
|
|
34
40
|
* the board, MuninnDB and Claude Code's `muninn` entry, the planning
|
|
35
|
-
* skills,
|
|
36
|
-
*
|
|
41
|
+
* skills, Luca's own skills, and the decision model's credentials. `luca
|
|
42
|
+
* init` and `luca upgrade` end with them too. No token is ever shown.
|
|
37
43
|
*/
|
|
38
44
|
|
|
39
45
|
/** The oldest tool versions Luca works with. */
|
|
@@ -367,12 +373,32 @@ const checkLucaSkills = async ({
|
|
|
367
373
|
)
|
|
368
374
|
}
|
|
369
375
|
|
|
376
|
+
/**
|
|
377
|
+
* The decision model's Cloudflare credentials (#534), from Luca's own env
|
|
378
|
+
* file and the process env. With none it is off: a warning, not a problem,
|
|
379
|
+
* since runs go on without it. Never shows the token.
|
|
380
|
+
*/
|
|
381
|
+
const checkDecisionModel = async ({
|
|
382
|
+
env,
|
|
383
|
+
home,
|
|
384
|
+
}: {
|
|
385
|
+
env: Env
|
|
386
|
+
home: string
|
|
387
|
+
}) => {
|
|
388
|
+
const credentials = await loadDecisionModelCredentials({ env, home })
|
|
389
|
+
if (credentials.ok) return ok(`Clef set up (${DEFAULT_DECISION_MODEL})`)
|
|
390
|
+
return warning({
|
|
391
|
+
detail: `Decision model off: no credentials (${credentials.missing.join(' and ')} not set in ${credentials.file}).`,
|
|
392
|
+
fix: `Add these lines to ${credentials.file}: ${missingKeyLines(credentials).join(', ')} (a Workers AI token from the Cloudflare dashboard), or leave the decision model off.`,
|
|
393
|
+
})
|
|
394
|
+
}
|
|
395
|
+
|
|
370
396
|
/**
|
|
371
397
|
* Runs the computer checks, in order, read-only. Never throws: a check that
|
|
372
398
|
* fails becomes a problem.
|
|
373
399
|
*
|
|
374
400
|
* @example
|
|
375
|
-
* const checks = await computerChecks({ home, luca_version, board_dir, skills_dir, engine_path, bun_path, computer, muninn, muninn_health, claude, paseo })
|
|
401
|
+
* const checks = await computerChecks({ home, luca_version, board_dir, skills_dir, engine_path, bun_path, computer, muninn, muninn_health, claude, paseo, env: process.env })
|
|
376
402
|
*/
|
|
377
403
|
export const computerChecks = async ({
|
|
378
404
|
home,
|
|
@@ -386,6 +412,7 @@ export const computerChecks = async ({
|
|
|
386
412
|
muninn_health,
|
|
387
413
|
claude,
|
|
388
414
|
paseo,
|
|
415
|
+
env,
|
|
389
416
|
}: {
|
|
390
417
|
home: string
|
|
391
418
|
/** The installed Luca's version. */
|
|
@@ -400,6 +427,11 @@ export const computerChecks = async ({
|
|
|
400
427
|
muninn_health: MuninnHealth
|
|
401
428
|
claude: ClaudeMcp
|
|
402
429
|
paseo: Paseo
|
|
430
|
+
/**
|
|
431
|
+
* The process env, for the decision model's keys and `XDG_CONFIG_HOME`.
|
|
432
|
+
* Left out, none: only Luca's env file in `home` counts.
|
|
433
|
+
*/
|
|
434
|
+
env?: Env
|
|
403
435
|
}): Promise<DoctorCheck[]> => {
|
|
404
436
|
// With Paseo down, its plugins and the board aren't asked about.
|
|
405
437
|
let paseo_up = true
|
|
@@ -446,6 +478,10 @@ export const computerChecks = async ({
|
|
|
446
478
|
'luca_skills',
|
|
447
479
|
() => checkLucaSkills({ home, skills_dir, luca_version }),
|
|
448
480
|
],
|
|
481
|
+
[
|
|
482
|
+
'decision_model',
|
|
483
|
+
() => checkDecisionModel({ env: env ?? {}, home }),
|
|
484
|
+
],
|
|
449
485
|
],
|
|
450
486
|
})
|
|
451
487
|
}
|
package/engine/cli/doctor.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
} from './v13-leftovers'
|
|
26
26
|
|
|
27
27
|
import type { V13Manifest } from '../doctor/v13-manifest'
|
|
28
|
+
import type { Env } from '../jev/decision-model-credentials'
|
|
28
29
|
import type { MemoryClient } from '../memory/memory-client'
|
|
29
30
|
import { runCommand } from '../shell/run-command'
|
|
30
31
|
|
|
@@ -32,7 +33,8 @@ import { runCommand } from '../shell/run-command'
|
|
|
32
33
|
* `luca doctor [--fix]`: checks this computer, the repo when run inside
|
|
33
34
|
* one, and what old Luca v13 left behind, and prints each check as OK, or
|
|
34
35
|
* the problem and its exact fix. It exits 1 when any check is a problem;
|
|
35
|
-
* warnings (memory off, planning-skill drift)
|
|
36
|
+
* warnings (memory off, the decision model off, planning-skill drift)
|
|
37
|
+
* don't fail it. When it finds
|
|
36
38
|
* v13 leftovers, it links the migration guide.
|
|
37
39
|
*
|
|
38
40
|
* `--fix` fixes what's safe, without asking: it cleans up v13's leftovers
|
|
@@ -270,6 +272,7 @@ export const runDoctor = async ({
|
|
|
270
272
|
repo,
|
|
271
273
|
tmp_dir,
|
|
272
274
|
v13_manifest,
|
|
275
|
+
env,
|
|
273
276
|
log,
|
|
274
277
|
}: LucaInstall & {
|
|
275
278
|
home: string
|
|
@@ -285,6 +288,8 @@ export const runDoctor = async ({
|
|
|
285
288
|
tmp_dir: string
|
|
286
289
|
/** The v13 fingerprint list; defaults to the committed one. */
|
|
287
290
|
v13_manifest?: V13Manifest
|
|
291
|
+
/** The process env, for the decision model's check. Left out, none. */
|
|
292
|
+
env?: Env
|
|
288
293
|
log: (line: string) => void
|
|
289
294
|
}): Promise<DoctorEnd> => {
|
|
290
295
|
const check = async (): Promise<DoctorCheck[]> => [
|
|
@@ -300,6 +305,7 @@ export const runDoctor = async ({
|
|
|
300
305
|
muninn_health,
|
|
301
306
|
claude,
|
|
302
307
|
paseo,
|
|
308
|
+
env,
|
|
303
309
|
})),
|
|
304
310
|
...(repo === null
|
|
305
311
|
? []
|
package/engine/cli/init.ts
CHANGED
|
@@ -23,6 +23,8 @@ import { installLucaSkills } from './luca-skills'
|
|
|
23
23
|
import { ensureMuninnEntry, hideToken } from './muninn-entry'
|
|
24
24
|
import { runSetup } from './setup'
|
|
25
25
|
|
|
26
|
+
import type { Env } from '../jev/decision-model-credentials'
|
|
27
|
+
|
|
26
28
|
/**
|
|
27
29
|
* `luca init`: sets up this computer for Luca, once, and is safe to run
|
|
28
30
|
* again. It has three parts, each run even when an earlier one failed, and
|
|
@@ -297,6 +299,7 @@ export const runInit = async ({
|
|
|
297
299
|
skills_dir,
|
|
298
300
|
engine_path,
|
|
299
301
|
bun_path,
|
|
302
|
+
env,
|
|
300
303
|
log,
|
|
301
304
|
}: LucaInstall & {
|
|
302
305
|
home: string
|
|
@@ -312,6 +315,8 @@ export const runInit = async ({
|
|
|
312
315
|
skills: SkillsTool
|
|
313
316
|
ask: Ask
|
|
314
317
|
computer: Computer
|
|
318
|
+
/** The process env, for the decision model's check. Left out, none. */
|
|
319
|
+
env?: Env
|
|
315
320
|
log: (line: string) => void
|
|
316
321
|
}): Promise<InitEnd> => {
|
|
317
322
|
const memory = await setUpMemory({
|
|
@@ -356,6 +361,7 @@ export const runInit = async ({
|
|
|
356
361
|
muninn_health,
|
|
357
362
|
claude,
|
|
358
363
|
paseo,
|
|
364
|
+
env,
|
|
359
365
|
})
|
|
360
366
|
for (const line of formatChecks({ checks: doctor })) log(line)
|
|
361
367
|
const setup =
|
package/engine/cli/luca-run.ts
CHANGED
|
@@ -7,9 +7,13 @@
|
|
|
7
7
|
* --run-id <id> --board-plugin luca-board (token in $LUCA_BOARD_TOKEN)
|
|
8
8
|
*
|
|
9
9
|
* A real run builds with real Claude agents (`createClaudeLauncher`: Claude
|
|
10
|
-
* Opus 5.5, every guard on, paid by your Claude plan) and asks
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* Opus 5.5, every guard on, paid by your Claude plan) and asks the decision
|
|
11
|
+
* model, Cloudflare's Clef, in shadow mode (#534). Its credentials,
|
|
12
|
+
* `CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_API_TOKEN`, come from Luca's own
|
|
13
|
+
* env file, `~/.config/luca/.env` (or `$XDG_CONFIG_HOME/luca/.env`), with
|
|
14
|
+
* the process env winning key by key; never from the repo's `.env`. With
|
|
15
|
+
* none, the decision model is off for the run: one line and one
|
|
16
|
+
* `decision_model_off` record, and no asks. Memory (#370) is
|
|
13
17
|
* MuninnDB over MCP, found through `LUCA_MUNINN_URL` and `LUCA_MUNINN_TOKEN`
|
|
14
18
|
* or Claude Code's `mcpServers.muninn` in `~/.claude.json`; with neither,
|
|
15
19
|
* the run goes on without memory.
|
|
@@ -39,7 +43,7 @@ import {
|
|
|
39
43
|
import { createClaudeLauncher } from '../agents/claude-launcher'
|
|
40
44
|
import { createBoardSync, type BoardSync } from '../board/board-sync'
|
|
41
45
|
import { createPaseoBoardLink } from '../board/paseo-board-link'
|
|
42
|
-
import {
|
|
46
|
+
import { loadDecisionModelCredentials } from '../jev/decision-model-credentials'
|
|
43
47
|
import { defaultRunsDir } from '../journal/journal'
|
|
44
48
|
import {
|
|
45
49
|
defaultSharedReadingsPath,
|
|
@@ -76,6 +80,14 @@ const memoryOf = async (): Promise<MemoryDeps | undefined> => {
|
|
|
76
80
|
return { client: createMuninnMcpClient({ settings: found.settings }) }
|
|
77
81
|
}
|
|
78
82
|
|
|
83
|
+
/** The decision model's setup: its credentials from Luca's own env file. */
|
|
84
|
+
const decisionModelSetup = async () => ({
|
|
85
|
+
credentials: await loadDecisionModelCredentials({
|
|
86
|
+
env: process.env,
|
|
87
|
+
home: homedir(),
|
|
88
|
+
}),
|
|
89
|
+
})
|
|
90
|
+
|
|
79
91
|
/**
|
|
80
92
|
* The usage line (#434): the `luca-board` plugin's lines, and the readings
|
|
81
93
|
* every run shares, both in Luca's state folder.
|
|
@@ -119,7 +131,7 @@ const run = async ({
|
|
|
119
131
|
tracker: async ({ repo }) =>
|
|
120
132
|
createGitHubTracker({ repo: await githubRepoOf({ repo }) }),
|
|
121
133
|
launcher: createClaudeLauncher({}),
|
|
122
|
-
|
|
134
|
+
decision_model: await decisionModelSetup(),
|
|
123
135
|
memory: await memoryOf(),
|
|
124
136
|
usage: usageLine(),
|
|
125
137
|
board,
|
|
@@ -137,7 +149,7 @@ const run = async ({
|
|
|
137
149
|
runs_dir: defaultRunsDir(),
|
|
138
150
|
tracker,
|
|
139
151
|
launcher: createClaudeLauncher({}),
|
|
140
|
-
|
|
152
|
+
decision_model: await decisionModelSetup(),
|
|
141
153
|
memory: await memoryOf(),
|
|
142
154
|
usage: usageLine(),
|
|
143
155
|
board,
|
package/engine/cli/run-modes.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { z } from 'zod'
|
|
|
9
9
|
import type { AgentLauncher } from '../agents/agent-launcher'
|
|
10
10
|
import { createScriptedLauncher } from '../agents/scripted-launcher'
|
|
11
11
|
import type { BoardSync } from '../board/board-sync'
|
|
12
|
-
import { loadEngineConfig } from '../config/engine-config'
|
|
12
|
+
import { decisionModelOf, loadEngineConfig } from '../config/engine-config'
|
|
13
13
|
import { lucaVersion } from '../config/luca-version'
|
|
14
14
|
import { decide, type EngineAction } from '../core/decide'
|
|
15
15
|
import {
|
|
@@ -21,10 +21,16 @@ import {
|
|
|
21
21
|
} from '../core/execute'
|
|
22
22
|
import { nothingToDoText } from '../core/left-out'
|
|
23
23
|
import { createGitAdapter } from '../git/git-adapter'
|
|
24
|
-
import {
|
|
24
|
+
import { createClefClient, type DecisionModelFetch } from '../jev/clef-client'
|
|
25
|
+
import {
|
|
26
|
+
noCredentialsText,
|
|
27
|
+
type DecisionModelCredentials,
|
|
28
|
+
} from '../jev/decision-model-credentials'
|
|
29
|
+
import { DEFAULT_DECISION_MODEL } from '../jev/jev-schemas'
|
|
25
30
|
import type { JevShadow } from '../jev/jev-shadow'
|
|
26
31
|
import { createJournal, runJournalPath, type Journal } from '../journal/journal'
|
|
27
32
|
import type { JournalRecord } from '../journal/journal-record'
|
|
33
|
+
import { replayRun } from '../journal/replay'
|
|
28
34
|
import type { UsageLineDeps } from '../limits/usage-line'
|
|
29
35
|
import type { MemoryDeps } from '../memory/memory-client'
|
|
30
36
|
import {
|
|
@@ -242,11 +248,67 @@ export const configReloadText = ({
|
|
|
242
248
|
}
|
|
243
249
|
}
|
|
244
250
|
|
|
251
|
+
/**
|
|
252
|
+
* What a real run needs to ask the decision model (#534): its Cloudflare
|
|
253
|
+
* credentials (`loadDecisionModelCredentials`), and for tests a fake
|
|
254
|
+
* `fetch` and a shorter timeout. The model comes from the run's config.
|
|
255
|
+
*/
|
|
256
|
+
export type DecisionModelSetup = {
|
|
257
|
+
credentials: DecisionModelCredentials
|
|
258
|
+
/** Defaults to the global `fetch`. */
|
|
259
|
+
fetch?: DecisionModelFetch
|
|
260
|
+
/** Defaults to `DEFAULT_JEV_TIMEOUT_MS`. */
|
|
261
|
+
timeout_ms?: number
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* The decision model for this run, from `setup` and the model in the run's
|
|
266
|
+
* own config (its `run_started`, so a resume keeps it). With no credentials
|
|
267
|
+
* it is off for the run: one `decision_model_off { reason: 'no_credentials' }`
|
|
268
|
+
* record and one line naming Luca's env file and the keys to add, and no
|
|
269
|
+
* shadow, so nothing is ever asked. Never logs the token.
|
|
270
|
+
*/
|
|
271
|
+
const openDecisionModel = ({
|
|
272
|
+
setup,
|
|
273
|
+
journal,
|
|
274
|
+
log,
|
|
275
|
+
}: {
|
|
276
|
+
setup: DecisionModelSetup | undefined
|
|
277
|
+
journal: Journal
|
|
278
|
+
log: (line: string) => void
|
|
279
|
+
}): JevShadow | undefined => {
|
|
280
|
+
if (setup === undefined) return undefined
|
|
281
|
+
const { config } = replayRun({ records: journal.read() })
|
|
282
|
+
const model =
|
|
283
|
+
config === null ? DEFAULT_DECISION_MODEL : decisionModelOf({ config })
|
|
284
|
+
const { credentials } = setup
|
|
285
|
+
if (!credentials.ok) {
|
|
286
|
+
const detail = noCredentialsText(credentials)
|
|
287
|
+
journal.append({
|
|
288
|
+
kind: 'decision_model_off',
|
|
289
|
+
ticket: null,
|
|
290
|
+
role: null,
|
|
291
|
+
content: { model, reason: 'no_credentials', detail },
|
|
292
|
+
})
|
|
293
|
+
log(`[luca-run] decision model off: ${detail}`)
|
|
294
|
+
return undefined
|
|
295
|
+
}
|
|
296
|
+
log(`[luca-run] decision model: Clef (${model}), in shadow mode`)
|
|
297
|
+
return {
|
|
298
|
+
client: createClefClient({ credentials, model, fetch: setup.fetch }),
|
|
299
|
+
timeout_ms: setup.timeout_ms,
|
|
300
|
+
log: (line) => {
|
|
301
|
+
log(`[luca-run] ${line}`)
|
|
302
|
+
},
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
245
306
|
/**
|
|
246
307
|
* A real run of `spec_number` on `repo`: loads the repo's engine config,
|
|
247
308
|
* opens (or resumes) the run's journal at `<runs_dir>/<run_id>`, and runs the
|
|
248
|
-
* engine with the board kept in step, the agents from `launcher`, and
|
|
249
|
-
* in shadow mode if given
|
|
309
|
+
* engine with the board kept in step, the agents from `launcher`, and the
|
|
310
|
+
* decision model in shadow mode if given (`decision_model`; with no
|
|
311
|
+
* credentials it is off for the run, see `openDecisionModel`). With `memory`, a new run turns memory on (#370),
|
|
250
312
|
* with the engine config's `muninn.vault` as the project vault (none:
|
|
251
313
|
* `default` only); a resumed run keeps what its journal says. A new run
|
|
252
314
|
* stops on a bad config; a resume takes the config's build fields again
|
|
@@ -258,7 +320,8 @@ export const configReloadText = ({
|
|
|
258
320
|
* const end = await runSpec({
|
|
259
321
|
* spec_number: 374, repo: '/code/app', run_id, base_branch: null,
|
|
260
322
|
* runs_dir: defaultRunsDir(), tracker: createGitHubTracker({ repo: 'acme/app' }),
|
|
261
|
-
* launcher: createClaudeLauncher({}),
|
|
323
|
+
* launcher: createClaudeLauncher({}),
|
|
324
|
+
* decision_model: { credentials: await loadDecisionModelCredentials({ env: process.env, home: homedir() }) },
|
|
262
325
|
* board, log: console.log,
|
|
263
326
|
* })
|
|
264
327
|
*/
|
|
@@ -270,7 +333,7 @@ export const runSpec = async ({
|
|
|
270
333
|
runs_dir,
|
|
271
334
|
tracker,
|
|
272
335
|
launcher,
|
|
273
|
-
|
|
336
|
+
decision_model,
|
|
274
337
|
memory,
|
|
275
338
|
usage,
|
|
276
339
|
board,
|
|
@@ -285,8 +348,8 @@ export const runSpec = async ({
|
|
|
285
348
|
runs_dir: string
|
|
286
349
|
tracker: Tracker
|
|
287
350
|
launcher: RunLauncher
|
|
288
|
-
/**
|
|
289
|
-
|
|
351
|
+
/** The decision model's setup (#534). Leave it out to run without one. */
|
|
352
|
+
decision_model?: DecisionModelSetup
|
|
290
353
|
/** MuninnDB (#370). Leave it out to run without memory. */
|
|
291
354
|
memory?: MemoryDeps
|
|
292
355
|
/** The usage line (#434). Leave it out to run without one. */
|
|
@@ -331,6 +394,7 @@ export const runSpec = async ({
|
|
|
331
394
|
const reload = recordConfigReload({ journal, loaded })
|
|
332
395
|
if (reload !== null) log(`[luca-run] ${configReloadText({ reload })}`)
|
|
333
396
|
}
|
|
397
|
+
const jev = openDecisionModel({ setup: decision_model, journal, log })
|
|
334
398
|
return driveRun({
|
|
335
399
|
journal,
|
|
336
400
|
run_id,
|
|
@@ -550,7 +614,7 @@ export const resumeRun = async ({
|
|
|
550
614
|
repo,
|
|
551
615
|
tracker,
|
|
552
616
|
launcher,
|
|
553
|
-
|
|
617
|
+
decision_model,
|
|
554
618
|
memory,
|
|
555
619
|
usage,
|
|
556
620
|
board,
|
|
@@ -563,8 +627,8 @@ export const resumeRun = async ({
|
|
|
563
627
|
repo: string | null
|
|
564
628
|
tracker: ({ repo }: { repo: string }) => Tracker | Promise<Tracker>
|
|
565
629
|
launcher: RunLauncher
|
|
566
|
-
/**
|
|
567
|
-
|
|
630
|
+
/** The decision model's setup (#534). Leave it out to go on without one. */
|
|
631
|
+
decision_model?: DecisionModelSetup
|
|
568
632
|
/**
|
|
569
633
|
* MuninnDB (#370), used when the run's `run_started` turned memory on.
|
|
570
634
|
* Leave it out to go on without memory.
|
|
@@ -603,7 +667,7 @@ export const resumeRun = async ({
|
|
|
603
667
|
runs_dir,
|
|
604
668
|
tracker: made,
|
|
605
669
|
launcher,
|
|
606
|
-
|
|
670
|
+
decision_model,
|
|
607
671
|
memory,
|
|
608
672
|
usage,
|
|
609
673
|
board,
|
|
@@ -635,19 +699,12 @@ const slowLauncher = ({
|
|
|
635
699
|
closeSession: (args) => launcher.closeSession(args),
|
|
636
700
|
})
|
|
637
701
|
|
|
638
|
-
/**
|
|
639
|
-
* The demo's Jev: the real client with no key, so every ask is journaled as
|
|
640
|
-
* `jev_failed` (`missing_key`) and nothing is sent, as in a real run without
|
|
641
|
-
* `TYPESAFE_API_KEY`.
|
|
642
|
-
*/
|
|
643
|
-
const OFFLINE_JEV: JevShadow = { client: createTypeSafeJev({ api_key: '' }) }
|
|
644
|
-
|
|
645
702
|
/**
|
|
646
703
|
* A practice run, safe to try the board with: a throwaway repo with a local
|
|
647
704
|
* bare origin in a temp folder, the practice spec with two tickets (#12
|
|
648
705
|
* blocked by #11) in an in-memory tracker, and scripted agents that take
|
|
649
|
-
* `turn_delay_ms` per turn. No GitHub, no models, no network:
|
|
650
|
-
*
|
|
706
|
+
* `turn_delay_ms` per turn. No GitHub, no models, no network: the decision
|
|
707
|
+
* model is off (its credentials are never read), and memory (#370) is a fake MuninnDB seeded with
|
|
651
708
|
* a few memories, so the board shows searches, the learner, and a save
|
|
652
709
|
* with nothing leaving the machine. The temp folder (which also holds the run's
|
|
653
710
|
* journal) is removed at the end.
|
|
@@ -681,6 +738,7 @@ export const runDemo = async ({
|
|
|
681
738
|
log(`[luca-run] repo: ${repo}`)
|
|
682
739
|
log(`[luca-run] origin: ${origin}`)
|
|
683
740
|
log(`[luca-run] journal: ${journal.file}`)
|
|
741
|
+
log('[luca-run] decision model off: the demo never asks it.')
|
|
684
742
|
const loaded = await loadEngineConfig({ repo_root: repo })
|
|
685
743
|
if (!loaded.ok) throw new Error(loaded.error)
|
|
686
744
|
const tracker = demoTracker()
|
|
@@ -714,7 +772,6 @@ export const runDemo = async ({
|
|
|
714
772
|
tracker,
|
|
715
773
|
git: createGitAdapter({ repo_root: repo }),
|
|
716
774
|
launcher,
|
|
717
|
-
jev: OFFLINE_JEV,
|
|
718
775
|
memory,
|
|
719
776
|
board: board ?? undefined,
|
|
720
777
|
}),
|
|
@@ -7,20 +7,24 @@ import type { EngineConfig } from './engine-config'
|
|
|
7
7
|
/**
|
|
8
8
|
* The config fields a resumed run takes from the repo's
|
|
9
9
|
* `.luca/config.json` again (#516). They say how the engine builds a
|
|
10
|
-
* checkout before its tests,
|
|
11
|
-
* them halfway without making its earlier
|
|
12
|
-
* a slow `prepare` for a fast cached one
|
|
10
|
+
* checkout before its tests, or how long a test may run, not what the
|
|
11
|
+
* tests are, so a run can change them halfway without making its earlier
|
|
12
|
+
* results wrong: a repo that swaps a slow `prepare` for a fast cached one
|
|
13
|
+
* gets it on the next resume.
|
|
13
14
|
*
|
|
14
15
|
* - `prepare`: the command run before each test run in a checkout.
|
|
15
16
|
* - `prepare_timeout_ms`: how long that command may run.
|
|
16
17
|
* - `prepare_concurrency`: how many prepare runs may go at once. The
|
|
17
18
|
* prepare slots take the limit on each call (`prepareCheck`), so a new
|
|
18
19
|
* one counts from the next prepare run.
|
|
20
|
+
* - `test_timeout_ms`: how long each test command may run (#542). Each run
|
|
21
|
+
* of a test command reads it then, so a new one counts from the next.
|
|
19
22
|
*/
|
|
20
23
|
export const RELOADED_FIELDS = [
|
|
21
24
|
'prepare',
|
|
22
25
|
'prepare_timeout_ms',
|
|
23
26
|
'prepare_concurrency',
|
|
27
|
+
'test_timeout_ms',
|
|
24
28
|
] as const
|
|
25
29
|
|
|
26
30
|
/**
|
|
@@ -43,6 +47,8 @@ export const RELOADED_FIELDS = [
|
|
|
43
47
|
* - `muninn`: the run's memory vault is fixed at start (`run_started.memory`).
|
|
44
48
|
* - `run_budget_tokens`: the budget the run's tokens are counted against,
|
|
45
49
|
* and the board's, from `run_started`; a raise is a `retry` reply.
|
|
50
|
+
* - `decision_model`: the model shadow mode asks (#534), so a run's asks
|
|
51
|
+
* are all of one model and can be scored together.
|
|
46
52
|
*/
|
|
47
53
|
export const FROZEN_FIELDS = [
|
|
48
54
|
'checks',
|
|
@@ -51,6 +57,7 @@ export const FROZEN_FIELDS = [
|
|
|
51
57
|
'rule_files',
|
|
52
58
|
'muninn',
|
|
53
59
|
'run_budget_tokens',
|
|
60
|
+
'decision_model',
|
|
54
61
|
] as const
|
|
55
62
|
|
|
56
63
|
const positiveInt = z.number().int().positive().nullable()
|
|
@@ -76,6 +83,11 @@ export const ConfigChangeSchema = z.discriminatedUnion('field', [
|
|
|
76
83
|
from: positiveInt,
|
|
77
84
|
to: positiveInt,
|
|
78
85
|
}),
|
|
86
|
+
z.object({
|
|
87
|
+
field: z.literal('test_timeout_ms'),
|
|
88
|
+
from: positiveInt,
|
|
89
|
+
to: positiveInt,
|
|
90
|
+
}),
|
|
79
91
|
])
|
|
80
92
|
|
|
81
93
|
export type ConfigChange = z.infer<typeof ConfigChangeSchema>
|
|
@@ -112,6 +124,11 @@ export const configChanges = ({
|
|
|
112
124
|
from: current.prepare_concurrency ?? null,
|
|
113
125
|
to: next.prepare_concurrency ?? null,
|
|
114
126
|
},
|
|
127
|
+
{
|
|
128
|
+
field: 'test_timeout_ms',
|
|
129
|
+
from: current.test_timeout_ms ?? null,
|
|
130
|
+
to: next.test_timeout_ms ?? null,
|
|
131
|
+
},
|
|
115
132
|
]
|
|
116
133
|
return all.filter(({ from, to }) => from !== to)
|
|
117
134
|
}
|
|
@@ -136,6 +153,10 @@ const applyChange = ({
|
|
|
136
153
|
return change.to === null
|
|
137
154
|
? omit(config, 'prepare_concurrency')
|
|
138
155
|
: { ...config, prepare_concurrency: change.to }
|
|
156
|
+
case 'test_timeout_ms':
|
|
157
|
+
return change.to === null
|
|
158
|
+
? omit(config, 'test_timeout_ms')
|
|
159
|
+
: { ...config, test_timeout_ms: change.to }
|
|
139
160
|
}
|
|
140
161
|
}
|
|
141
162
|
|