@garuda-agent/garuda 0.18.0 → 0.18.2
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 +156 -0
- package/README.md +3 -2
- package/dist/{acpCommand-IUZPLPLI.js → acpCommand-PUJHFWYU.js} +10 -8
- package/dist/advise-C777SZ5M.js +433 -0
- package/dist/brief-RWKESOPT.js +91 -0
- package/dist/buildCheck-HB46NWLX.js +19 -0
- package/dist/buildInfo-Z4P7QAQL.js +8 -0
- package/dist/chunk-2OYTNNUJ.js +216 -0
- package/dist/chunk-4BELTWP4.js +156 -0
- package/dist/chunk-4QRTTRVK.js +88 -0
- package/dist/chunk-6JQTPW3P.js +88 -0
- package/dist/chunk-A5DLAXB4.js +1780 -0
- package/dist/{chunk-FARS4R67.js → chunk-AX3A5KPH.js} +18 -1
- package/dist/chunk-CFDRP6XW.js +9 -0
- package/dist/chunk-E6YRCDSF.js +9 -0
- package/dist/{chunk-OQ26W5L4.js → chunk-HP6EGFHV.js} +132 -75
- package/dist/chunk-L55VUSX3.js +54 -0
- package/dist/{chunk-B4SJY7KT.js → chunk-LMKGSYJC.js} +77 -8
- package/dist/{chunk-Q4P3W657.js → chunk-LWNMEGDF.js} +10 -1124
- package/dist/{chunk-23FXBNBB.js → chunk-MNE7HHX2.js} +2 -1
- package/dist/{chunk-UKIE4AAM.js → chunk-NLS7GMF7.js} +11 -105
- package/dist/{chunk-6XHON3Q6.js → chunk-QXTHEFKP.js} +3 -3
- package/dist/{chunk-4BVCYCQT.js → chunk-R7RCW4BQ.js} +9 -7
- package/dist/{chunk-NHWHG3SW.js → chunk-STVFWSIL.js} +350 -57
- package/dist/{chunk-5B5HDTUR.js → chunk-TWQ7V2L6.js} +2 -2
- package/dist/chunk-ULAJPMY7.js +107 -0
- package/dist/chunk-VISKX36R.js +306 -0
- package/dist/{chunk-55CFEEIN.js → chunk-WK5NE6RY.js} +2 -2
- package/dist/chunk-Y62J7PTD.js +90 -0
- package/dist/chunk-YAQ6QLKB.js +40 -0
- package/dist/{chunk-FOPBOB7W.js → chunk-YEH2OONM.js} +6 -4
- package/dist/{chunk-7TH3NHCQ.js → chunk-ZKT4P2PC.js} +2 -2
- package/dist/cli/index.js +242 -52
- package/dist/{create-W6BV75QX.js → create-OACSGJ37.js} +5 -5
- package/dist/{evalCommand-SKJQ6CW2.js → evalCommand-MRO6ENCA.js} +77 -39
- package/dist/{evalTune-CZRCPSZ3.js → evalTune-VL5RHYOG.js} +24 -18
- package/dist/{export-SJ3DUP6W.js → export-ZPF3AMO6.js} +27 -4
- package/dist/exportHtml-ISISG5HG.js +21 -0
- package/dist/{inkChat-WYYAFZXD.js → inkChat-UDFX6GH6.js} +256 -41
- package/dist/{install-TKWN5AYH.js → install-4ST5BNRG.js} +3 -3
- package/dist/{jobCommand-2ODQLHB3.js → jobCommand-ILOTYOX3.js} +8 -7
- package/dist/{launchd-3A3ONEW5.js → launchd-QG6R5RAK.js} +4 -4
- package/dist/{lspCommand-ZZ6R4PA2.js → lspCommand-X4FTTONR.js} +3 -3
- package/dist/{manager-TCYTA5XU.js → manager-YH7722C2.js} +3 -3
- package/dist/{nightCommand-WJU6VZA6.js → nightCommand-XPWT6CQZ.js} +8 -7
- package/dist/receiptView-YFDHV253.js +19 -0
- package/dist/{remove-42WQNVPX.js → remove-STX5UO7X.js} +3 -3
- package/dist/{run-PTMRQCIU.js → run-W4TK52WT.js} +5 -5
- package/dist/runner-EBF6EOYM.js +97 -0
- package/dist/{server-VEYIJHEU.js → server-ASM73QVY.js} +3 -3
- package/dist/tour-CEVMAC5S.js +18 -0
- package/dist/tune-KSNKBPNJ.js +51 -0
- package/dist/uiState-YFJQ43D7.js +16 -0
- package/package.json +1 -1
- package/dist/chunk-4JSUUDFQ.js +0 -118
- package/dist/chunk-C57GHLKP.js +0 -9
- package/dist/chunk-DOCDCQVM.js +0 -484
- package/dist/tune-YVI2KWQF.js +0 -31
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,161 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.18.2 (2026-11-06)
|
|
4
|
+
|
|
5
|
+
A fix release for repo evals (`garuda eval -s repo`, `garuda advise`). On macOS, 0.18.1 failed
|
|
6
|
+
every check of a project whose tools live in `~/Library` (pnpm does), and Vite projects failed
|
|
7
|
+
before a test ran. It also makes `garuda advise` faster and cheaper to repeat: runs in parallel,
|
|
8
|
+
reuse of earlier runs, a cost estimate from real requests, and the command to go on after a stop
|
|
9
|
+
at the cap. A provider outage no longer counts against a model.
|
|
10
|
+
|
|
11
|
+
- Fix: repo-task evals (`-s repo`, `garuda advise`) on macOS failed every check in 0.18.1 when the
|
|
12
|
+
project's tools live in `~/Library`. pnpm runs the version that a project asks for from
|
|
13
|
+
`~/Library/pnpm`, and the 0.18.1 rules denied all of the home folder except hidden entries. The
|
|
14
|
+
check stopped with "EPERM: process.cwd failed", so both models passed 0 of 30 in the user's run.
|
|
15
|
+
`~/Library` is readable again in these runs; Mail, Messages, Safari, iCloud Drive, the other
|
|
16
|
+
personal parts and the keychains stay denied for every sandboxed command. Run 0.18.1 repo evals
|
|
17
|
+
again.
|
|
18
|
+
- `advise` gives no advice when neither model passes a task: the check itself may fail.
|
|
19
|
+
- Evals: a timeout while the model's API is silent is a new failure kind, `provider`. It applies
|
|
20
|
+
when a request was open at the limit, and the stream had broken and been retried or the API had
|
|
21
|
+
sent nothing for 2 minutes. Such a run is not scored, and it runs once more by itself; its
|
|
22
|
+
spending is kept. In the user's Sonnet run, 4 of 5 timeouts were such stalls, and they counted
|
|
23
|
+
against the model. A timeout also shows its steps now, not 0.
|
|
24
|
+
- Fix: in repo-task evals, Vite could not write its compiled `vitest.config.ts` to
|
|
25
|
+
`node_modules/.vite-temp`, because the task's `node_modules` links to the project's and was
|
|
26
|
+
read-only. In the user's suite 10 of 30 tasks failed for both models before a test ran.
|
|
27
|
+
Commands may now write `.vite-temp`, `.vite` and `.cache` there, and nothing else.
|
|
28
|
+
- `garuda eval --from-git` checks each kept commit a third time, as an eval runs it (a copy, in the
|
|
29
|
+
sandbox), and leaves out a commit whose tests fail there. Before, it checked only on the host,
|
|
30
|
+
and a task could pass the build but never an eval (2 of 30 in the user's suite: tests that start
|
|
31
|
+
their own sandbox). Build your suite again.
|
|
32
|
+
- The Homebrew formula in the repository names 0.18.1 and its tarball hash.
|
|
33
|
+
- `garuda advise` runs 4 tasks at a time (`--parallel <n>`); each result line starts with the
|
|
34
|
+
model and a count, as in `[haiku-5-5 12/30] ✓ fix-parser: pass`. The first real run, one at a
|
|
35
|
+
time, took 64 minutes for 30 tasks.
|
|
36
|
+
- `advise` reuses an earlier run of a model when the model, the task hashes, `--repeat`, `--seed`
|
|
37
|
+
and the Garuda version and build commit are the same and every run is there. Runs of 0.18.0 and
|
|
38
|
+
before are not reused: they could read your checkout (the 0.18.1 fix).
|
|
39
|
+
- The cost estimate comes from the mean cost of earlier runs of the model on the same tasks. With
|
|
40
|
+
none, it prices each request of the other model's runs (from their session files) at this
|
|
41
|
+
model's prices; else the token guess. Each line says which. (A first version scaled by the ratio
|
|
42
|
+
of list prices: 20 for Sonnet 5.5 over Haiku 5.5, but Haiku costs 5 times as much for a prompt
|
|
43
|
+
over 100 000 tokens. On a real run it estimated $19.16 where the requests give $12.62.)
|
|
44
|
+
- The repriced estimate is scaled by what both models really cost on shared repo tasks before:
|
|
45
|
+
Sonnet 5.5 needs fewer steps and tokens than Haiku 5.5, and cost $6.30 where Haiku's requests at
|
|
46
|
+
its prices gave $12.62 (× 0.52). Without the factor, the user's 90-run estimate was $33.62; with
|
|
47
|
+
it, $17.37. Runs where no task passed, and built-in suites, are left out.
|
|
48
|
+
- When the estimate or the first model's real cost passes `--max-cost`, `advise` prints the
|
|
49
|
+
command that goes on (`garuda advise --models a,b --repeat R --seed S --max-cost N`). The first
|
|
50
|
+
model's run is kept and reused.
|
|
51
|
+
- `advise` stops when a run fails to start (for example a refused key), not at a missing report.
|
|
52
|
+
|
|
53
|
+
## 0.18.1 (2026-11-06)
|
|
54
|
+
|
|
55
|
+
A security release. In 0.18.0 and earlier, a command in the OS sandbox could read your personal
|
|
56
|
+
folders (Documents, Downloads, Pictures and the Photos library, Mail and the like), and on macOS it
|
|
57
|
+
could reach denied paths through `/System/Volumes/Data`. Update to 0.18.1. It also has the UX work
|
|
58
|
+
of docs/lld/ux.md (receipts, a network question, `/rewind`, word-level diffs, the morning brief,
|
|
59
|
+
`/export --html`, `garuda advise` and more) and eval failure kinds.
|
|
60
|
+
|
|
61
|
+
- Security: sandboxed commands can no longer read your personal folders: Desktop, Documents,
|
|
62
|
+
Downloads, Movies, Music, Pictures (with the Photos library), Public, and Mail, Messages, Safari,
|
|
63
|
+
Calendars, Contacts, iCloud Drive and other cloud folders in `~/Library`. A project inside one of
|
|
64
|
+
them stays readable, the rest of that folder does not. On macOS each read and write rule also
|
|
65
|
+
covers the same path under `/System/Volumes/Data`, where a command could reach it before. Found
|
|
66
|
+
in a live run: a model's `find /` in an eval made macOS ask the user to let iTerm into Downloads
|
|
67
|
+
and Photos.
|
|
68
|
+
- Evals: a repo task now runs in a copy of the project with no history, not a git worktree, and
|
|
69
|
+
its commands cannot read the project's checkout (except `node_modules` and venvs) or the rest of
|
|
70
|
+
the home folder except hidden entries. In a live run, a model read the user's checkout, which
|
|
71
|
+
holds the commit that the task asks for. Repo results from before this change may have read the
|
|
72
|
+
answer; run them again.
|
|
73
|
+
- UX, patch 7 (docs/lld/ux.md U11, U8): `/export --html [file]` writes the session as one page:
|
|
74
|
+
your requests, the answers, each tool call that opens to its output (an edit opens to its
|
|
75
|
+
change), and the receipts. Every text is escaped; the page has no script and no link, and its
|
|
76
|
+
own policy forbids them. `garuda advise` runs the repo suite (`garuda eval --from-git`) on two
|
|
77
|
+
models (default Haiku 5.5 and Sonnet 5.5, 2 runs a task, at most $2), compares them as
|
|
78
|
+
`eval compare` does, and advises: the model that passes clearly more, or on a tie the cheaper
|
|
79
|
+
one. It asks first with a cost guess, and writes `advice.txt` and `report.html`.
|
|
80
|
+
- UX, patch 6 (docs/lld/ux.md U7, U9): the morning brief. The first chat after jobs ran shows them
|
|
81
|
+
once: each job's verdict, its tests ("tests 120 → 126" when the runner's summary gives counts)
|
|
82
|
+
and its branch, and the total cost. `.garuda/jobs/brief.seen` keeps the time of the last brief.
|
|
83
|
+
The first-run tour: the first chat in a project with code names its stack ("TypeScript with
|
|
84
|
+
vitest (pnpm)") and offers three first tasks; typing 1, 2 or 3 runs one. It shows once per
|
|
85
|
+
project (`~/.garuda/ui.json` keeps a hash of the folder, not its path).
|
|
86
|
+
- UX, patch 5 (docs/lld/ux.md U5): diffs mark the changed words of a changed line (inverse video),
|
|
87
|
+
in approvals, in hunk review and in `/diff`. From 140 columns a diff is side by side, with the
|
|
88
|
+
line numbers of both files. An approval that can review hunks says so in its first line:
|
|
89
|
+
"3 hunks · h reviews them one by one". Syntax colours are not in this patch (decision D10).
|
|
90
|
+
Only a real diff (with a hunk header) gets the marks and the two columns; other previews, such
|
|
91
|
+
as the project-settings question, keep their simple colours (the first live run showed that
|
|
92
|
+
question twice, side by side).
|
|
93
|
+
- UX, patch 4 (docs/lld/ux.md U6): `/rewind` lists the turns that undo can reach (files, cost, age).
|
|
94
|
+
`/rewind <n>` goes back to the start of turn n in one step, with one question; `/redo` brings the
|
|
95
|
+
turns back one at a time. `/rewind fork <n>` starts a new session with the conversation up to
|
|
96
|
+
turn n and the files as they were then; the old session does not change. A new `fork` record
|
|
97
|
+
starts such a session.
|
|
98
|
+
- UX, patch 3 (docs/lld/ux.md U4): in the chat, calls of one read-only tool in a row become one
|
|
99
|
+
line, for example `● Read 5 files docs/architecture.md, SECURITY.md, +3`; a lone call keeps its
|
|
100
|
+
line and its result. Edits, commands and failed calls always keep a line of their own. Ctrl-O
|
|
101
|
+
still shows the last call's output. The model's plan (`todo_write`) is drawn live above the
|
|
102
|
+
input during the turn and printed once at the end, instead of after every update.
|
|
103
|
+
- The chat's wordmark has a drop shadow (a 3D look), drawn twice as large from 130 columns, with
|
|
104
|
+
the card as wide as the wordmark; a tall screen gets some space above it. A screen reader gets no
|
|
105
|
+
wordmark (the card says the name).
|
|
106
|
+
- The chat starts on a clear screen, with the banner centred, and you type in a visible chat box
|
|
107
|
+
with a hint ("Type a task · / for commands · @ for files"). The old screen goes into the
|
|
108
|
+
scrollback, not away; `"ui": { "clearOnStart": false }` in settings keeps it on screen. Garuda
|
|
109
|
+
stays in the terminal's normal screen, so its scrollback, search and copy still work. The box's
|
|
110
|
+
border dims while a turn runs. With `GARUDA_SCREEN_READER=1`, or under 40 columns, there is no box
|
|
111
|
+
and the banner is not centred.
|
|
112
|
+
- UX, patch 2 (docs/lld/ux.md U2): a chat with no network allowlist starts Garuda's proxy with no
|
|
113
|
+
host on it ("watch mode"). A command in the sandbox that needs a host now asks for it (once, this
|
|
114
|
+
session, or no) instead of failing with a DNS error; an empty list needs no consent. After "this
|
|
115
|
+
session", Garuda says which line in `network.allow` keeps it for the project (a preset such as
|
|
116
|
+
`npm` when the host belongs to one). The receipt names a blocked host. `-p`, jobs and evals keep
|
|
117
|
+
no network, as before.
|
|
118
|
+
- The chat shows your requests with a bar (`▌`) on every line, and Garuda's answer starts with `✦`,
|
|
119
|
+
so requests and answers differ at a glance. With `GARUDA_SCREEN_READER=1` they read "You:" and
|
|
120
|
+
"Garuda:".
|
|
121
|
+
- The chat warns at start when `dist/` is older than its checkout ("run pnpm build first"), as the
|
|
122
|
+
evals do. `garudaCommit` and `staleBuildNote` moved to `src/buildCheck.ts`.
|
|
123
|
+
- Docs: `architecture.md`, `hld.md` and the docs index describe Garuda as it is now. The release
|
|
124
|
+
history in their prose (version lists, "since 0.x", test counts per release) is gone; the
|
|
125
|
+
CHANGELOG keeps it.
|
|
126
|
+
- UX, patch 1 (docs/lld/ux.md): a receipt after each turn that changed files, ran tests, ran a command outside the sandbox, or had a host blocked: the
|
|
127
|
+
files with their lines (from the undo snapshots), the last test run with its count for vitest,
|
|
128
|
+
jest, node:test, pytest, Maven and `go test -v`, the commands in and outside the sandbox, blocked
|
|
129
|
+
hosts, what `/undo` puts back, and the session's cost per kept change. A turn that only answered
|
|
130
|
+
keeps the usage line, also when it ran read-only commands in the sandbox (the first live run: a
|
|
131
|
+
`find` gave a receipt with no news). The session records the receipt (`receipt`), `/export` shows it, and
|
|
132
|
+
`-p --json` has it as `receipt`. Under 60 columns, in a pipe and with `GARUDA_SCREEN_READER=1` it
|
|
133
|
+
is plain lines.
|
|
134
|
+
- The chat asks how to go on after the step limit: go on for as many steps again, summarise what is
|
|
135
|
+
left, or stop. After the same tool call 3 times it says to type a hint, and no longer names
|
|
136
|
+
`--resume`.
|
|
137
|
+
- The chat's footer shows the context as a bar and a number (yellow from 70%, red from 80%). The key
|
|
138
|
+
hints show in the first 5 chats; then the footer says `? keys`, and `?` on an empty line lists them.
|
|
139
|
+
- Design: the UX note (docs/lld/ux.md), with mock-ups and no code yet. A receipt after each
|
|
140
|
+
turn (files, tests, sandbox, undo, cost per kept change), a blocked host as a one-key fix,
|
|
141
|
+
next steps after a stop, a live plan, better diffs, rewind and fork, a morning brief from the
|
|
142
|
+
night shift, `garuda advise` (which model for this repo), a first-run tour, a quieter footer,
|
|
143
|
+
HTML export, and reduced motion and screen-reader modes. Ten decisions for the user.
|
|
144
|
+
- Blog: "Garuda against Claude Code, with intervals": the eval method, the Claude Code
|
|
145
|
+
baseline, the faults the live runs found in the tasks, the Graphify A/B, the tuned prompt, and
|
|
146
|
+
the limits.
|
|
147
|
+
- The Homebrew formula in the repository names 0.18.0 and its tarball hash.
|
|
148
|
+
- Failure kinds: the eval report gives one line, "Failures by kind", that counts the failed runs
|
|
149
|
+
as `check` (the check failed), `protected-files`, `step-limit`, `token-limit`,
|
|
150
|
+
`repeated-calls`, `refusal`, `timeout`, `error` or `setup` (the key, the connection or the MCP
|
|
151
|
+
server), and names up to three runs of each kind. A limit or a timeout wins over the check that
|
|
152
|
+
failed after it. `report.json` has the same in `evidence.failures`, with each failed run's
|
|
153
|
+
steps, tokens and time.
|
|
154
|
+
- The reports of the runs in the blog post are in `docs/quality-baseline/evals/0.18/`, with a
|
|
155
|
+
README that maps each run to the post and gives the `eval compare` commands. Local paths are
|
|
156
|
+
removed. A test checks that the files hold no local path and that `compare` gives the post's
|
|
157
|
+
numbers. The post links to them. Biome and the docs site leave the folder out.
|
|
158
|
+
|
|
3
159
|
## 0.18.0 (2026-11-04)
|
|
4
160
|
|
|
5
161
|
- Biome 2.6.2 (with @inquirer/prompts 8.7.4, react 19.3.1 and smol-toml 1.9.1, from Dependabot's
|
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ Website and docs: https://madhu-sv.github.io/garuda/
|
|
|
7
7
|
Garuda is a coding agent for the terminal, and for editors over the Agent Client Protocol. It reads
|
|
8
8
|
and edits your code, runs commands in an OS sandbox, and keeps an audit log of its permission
|
|
9
9
|
decisions. It works with Claude and with OpenAI-compatible models, local ones too. This is version
|
|
10
|
-
0.18.
|
|
10
|
+
0.18.2. Release notes: [CHANGELOG.md](CHANGELOG.md).
|
|
11
11
|
|
|
12
12
|
## What Garuda does on your machine
|
|
13
13
|
|
|
@@ -15,7 +15,8 @@ decisions. It works with Claude and with OpenAI-compatible models, local ones to
|
|
|
15
15
|
- `write_file` and `edit_file` show a diff first. You allow once, allow for this session, or deny.
|
|
16
16
|
- Commands (`bash`) run in an OS sandbox when your machine has one: Seatbelt on macOS, bubblewrap
|
|
17
17
|
on Linux. There they run with no question, but they can write only in the project, temp folders
|
|
18
|
-
and package caches, cannot read secrets in your home folder (such as `~/.ssh`)
|
|
18
|
+
and package caches, cannot read secrets in your home folder (such as `~/.ssh`) or your personal folders (Documents,
|
|
19
|
+
Downloads, Pictures and the like, outside the project), and have no
|
|
19
20
|
network unless you open an allowlist. So a command can change project files without a question;
|
|
20
21
|
`/undo` restores the files in its snapshot (ignored files, such as build output, are not in it).
|
|
21
22
|
- A command outside the sandbox asks first, unless one of your allow rules covers it. The strict
|
|
@@ -9,27 +9,29 @@ import {
|
|
|
9
9
|
import {
|
|
10
10
|
Runtime,
|
|
11
11
|
auditDirFor
|
|
12
|
-
} from "./chunk-
|
|
13
|
-
import "./chunk-65NUEOI7.js";
|
|
12
|
+
} from "./chunk-STVFWSIL.js";
|
|
14
13
|
import "./chunk-G2AX7MGS.js";
|
|
14
|
+
import "./chunk-ULAJPMY7.js";
|
|
15
|
+
import "./chunk-65NUEOI7.js";
|
|
16
|
+
import "./chunk-4QRTTRVK.js";
|
|
15
17
|
import "./chunk-A2QAIRQL.js";
|
|
16
18
|
import {
|
|
17
19
|
FileSessionStore
|
|
18
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-NLS7GMF7.js";
|
|
19
21
|
import "./chunk-7KBOIPUT.js";
|
|
20
22
|
import {
|
|
21
23
|
loadSearchConfig
|
|
22
24
|
} from "./chunk-B25VL4RU.js";
|
|
23
|
-
import "./chunk-
|
|
25
|
+
import "./chunk-MNE7HHX2.js";
|
|
24
26
|
import {
|
|
25
27
|
VERSION
|
|
26
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-CFDRP6XW.js";
|
|
27
29
|
import "./chunk-5DEWP3YX.js";
|
|
28
30
|
import "./chunk-PZS3V6P3.js";
|
|
29
31
|
import "./chunk-QANRZW7W.js";
|
|
30
32
|
import "./chunk-ATW7BNIG.js";
|
|
31
33
|
import "./chunk-AOGFYUKG.js";
|
|
32
|
-
import "./chunk-
|
|
34
|
+
import "./chunk-LMKGSYJC.js";
|
|
33
35
|
import "./chunk-BHZ6G65V.js";
|
|
34
36
|
import "./chunk-P6XWWIEK.js";
|
|
35
37
|
import "./chunk-UIRJOQQS.js";
|
|
@@ -52,7 +54,7 @@ async function acpCommand(options) {
|
|
|
52
54
|
}
|
|
53
55
|
const output = protocolOutput(process.stdout, process.stderr);
|
|
54
56
|
const input = Readable.toWeb(process.stdin);
|
|
55
|
-
const { acpServer, setupNeeded } = await import("./server-
|
|
57
|
+
const { acpServer, setupNeeded } = await import("./server-ASM73QVY.js");
|
|
56
58
|
const server = acpServer({
|
|
57
59
|
version: VERSION,
|
|
58
60
|
// Not on native Windows: the setup there could not keep a key private (no 0600).
|
|
@@ -190,4 +192,4 @@ export {
|
|
|
190
192
|
prepare,
|
|
191
193
|
protocolOutput
|
|
192
194
|
};
|
|
193
|
-
//# sourceMappingURL=acpCommand-
|
|
195
|
+
//# sourceMappingURL=acpCommand-PUJHFWYU.js.map
|
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
taskHash
|
|
4
|
+
} from "./chunk-6JQTPW3P.js";
|
|
5
|
+
import {
|
|
6
|
+
garudaCommit
|
|
7
|
+
} from "./chunk-YAQ6QLKB.js";
|
|
8
|
+
import {
|
|
9
|
+
escapeHtml
|
|
10
|
+
} from "./chunk-4BELTWP4.js";
|
|
11
|
+
import {
|
|
12
|
+
BUILD_COMMIT
|
|
13
|
+
} from "./chunk-E6YRCDSF.js";
|
|
14
|
+
import {
|
|
15
|
+
compareReports,
|
|
16
|
+
costPerPass,
|
|
17
|
+
formatComparison,
|
|
18
|
+
loadReport,
|
|
19
|
+
repoEvalTasks
|
|
20
|
+
} from "./chunk-A5DLAXB4.js";
|
|
21
|
+
import {
|
|
22
|
+
loadModelsConfig,
|
|
23
|
+
resolveModel
|
|
24
|
+
} from "./chunk-2FIV2VZP.js";
|
|
25
|
+
import "./chunk-AX3A5KPH.js";
|
|
26
|
+
import "./chunk-2OYTNNUJ.js";
|
|
27
|
+
import "./chunk-NXJ4XLB5.js";
|
|
28
|
+
import "./chunk-STVFWSIL.js";
|
|
29
|
+
import "./chunk-G2AX7MGS.js";
|
|
30
|
+
import "./chunk-ULAJPMY7.js";
|
|
31
|
+
import "./chunk-65NUEOI7.js";
|
|
32
|
+
import "./chunk-4QRTTRVK.js";
|
|
33
|
+
import "./chunk-A2QAIRQL.js";
|
|
34
|
+
import {
|
|
35
|
+
newSessionId
|
|
36
|
+
} from "./chunk-NLS7GMF7.js";
|
|
37
|
+
import {
|
|
38
|
+
costOf,
|
|
39
|
+
responseCost
|
|
40
|
+
} from "./chunk-7KBOIPUT.js";
|
|
41
|
+
import "./chunk-B25VL4RU.js";
|
|
42
|
+
import "./chunk-MNE7HHX2.js";
|
|
43
|
+
import {
|
|
44
|
+
VERSION
|
|
45
|
+
} from "./chunk-CFDRP6XW.js";
|
|
46
|
+
import "./chunk-5DEWP3YX.js";
|
|
47
|
+
import "./chunk-PZS3V6P3.js";
|
|
48
|
+
import "./chunk-QANRZW7W.js";
|
|
49
|
+
import "./chunk-ATW7BNIG.js";
|
|
50
|
+
import "./chunk-WK5NE6RY.js";
|
|
51
|
+
import "./chunk-AOGFYUKG.js";
|
|
52
|
+
import "./chunk-QXTHEFKP.js";
|
|
53
|
+
import "./chunk-LMKGSYJC.js";
|
|
54
|
+
import "./chunk-BHZ6G65V.js";
|
|
55
|
+
import "./chunk-P6XWWIEK.js";
|
|
56
|
+
import "./chunk-UIRJOQQS.js";
|
|
57
|
+
import "./chunk-4LFJFDCF.js";
|
|
58
|
+
import "./chunk-3NJNVZTK.js";
|
|
59
|
+
|
|
60
|
+
// src/cli/advise.ts
|
|
61
|
+
import { mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "fs";
|
|
62
|
+
import { basename, join } from "path";
|
|
63
|
+
import { createInterface } from "readline/promises";
|
|
64
|
+
var ADVISE_MODELS = ["claude-haiku-5-5", "claude-sonnet-5-5"];
|
|
65
|
+
var ADVISE_REPEAT = 2;
|
|
66
|
+
var ADVISE_MAX_COST = 2;
|
|
67
|
+
var GUESS_INPUT_TOKENS = 6e4;
|
|
68
|
+
var GUESS_OUTPUT_TOKENS = 1500;
|
|
69
|
+
function estimateUsd(price, runs) {
|
|
70
|
+
if (price === void 0) return void 0;
|
|
71
|
+
return runs * (GUESS_INPUT_TOKENS * price.input + GUESS_OUTPUT_TOKENS * price.output) / 1e6;
|
|
72
|
+
}
|
|
73
|
+
function adviceText(a, b, c, tasks) {
|
|
74
|
+
const rough = tasks < 10 ? ` With ${tasks} task${tasks === 1 ? "" : "s"}, this is a rough guide.` : "";
|
|
75
|
+
if (c.verdict === "B passes more")
|
|
76
|
+
return `Advice: ${b} for this repo. It passes more often.${rough}`;
|
|
77
|
+
if (c.verdict === "B passes less")
|
|
78
|
+
return `Advice: ${a} for this repo. It passes more often.${rough}`;
|
|
79
|
+
if (c.rateA === 0 && c.rateB === 0) {
|
|
80
|
+
return `Advice: none. Neither model passed a task, so the check itself may fail: read a run's "reason" in report.json before you trust this.`;
|
|
81
|
+
}
|
|
82
|
+
const ratio = c.costRatio;
|
|
83
|
+
if (ratio === void 0) {
|
|
84
|
+
return `Advice: no clear difference in passes, and the cost could not be compared.${rough}`;
|
|
85
|
+
}
|
|
86
|
+
const times = (x) => `${x.toFixed(1)} times`;
|
|
87
|
+
if (ratio.verdict === "B costs less per accepted change") {
|
|
88
|
+
return `Advice: ${b} for this repo. It passes as often, and ${a} costs ${times(1 / ratio.value)} as much per accepted change.${rough}`;
|
|
89
|
+
}
|
|
90
|
+
if (ratio.verdict === "B costs more per accepted change") {
|
|
91
|
+
return `Advice: ${a} for this repo. It passes as often, and ${b} costs ${times(ratio.value)} as much per accepted change.${rough}`;
|
|
92
|
+
}
|
|
93
|
+
const cheaper = ratio.value <= 1 ? b : a;
|
|
94
|
+
return `Advice: either. No clear difference in passes or in cost; ${cheaper} was cheaper in these runs.${rough}`;
|
|
95
|
+
}
|
|
96
|
+
function adviceHtml(comparison, advice) {
|
|
97
|
+
return [
|
|
98
|
+
"<!doctype html>",
|
|
99
|
+
'<html lang="en"><head><meta charset="utf-8">',
|
|
100
|
+
`<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'">`,
|
|
101
|
+
"<title>Garuda advise</title>",
|
|
102
|
+
"<style>body{font:15px/1.5 system-ui,sans-serif;max-width:900px;margin:32px auto;padding:0 16px}",
|
|
103
|
+
"pre{font:13px/1.45 ui-monospace,monospace;white-space:pre-wrap}.advice{font-weight:600}</style>",
|
|
104
|
+
"</head><body><h1>Which model for this repository</h1>",
|
|
105
|
+
`<p class="advice">${escapeHtml(advice)}</p>`,
|
|
106
|
+
`<pre>${escapeHtml(comparison)}</pre>`,
|
|
107
|
+
"</body></html>",
|
|
108
|
+
""
|
|
109
|
+
].join("\n");
|
|
110
|
+
}
|
|
111
|
+
var ADVISE_PARALLEL = 4;
|
|
112
|
+
function pastRuns(evalsDir) {
|
|
113
|
+
const folders = [];
|
|
114
|
+
try {
|
|
115
|
+
for (const name of readdirSync(evalsDir)) {
|
|
116
|
+
const path = join(evalsDir, name);
|
|
117
|
+
folders.push(path, join(path, "a"), join(path, "b"));
|
|
118
|
+
}
|
|
119
|
+
} catch {
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
const out = [];
|
|
123
|
+
for (const folder of folders) {
|
|
124
|
+
const file = join(folder, "report.json");
|
|
125
|
+
try {
|
|
126
|
+
const data = JSON.parse(readFileSync(file, "utf8"));
|
|
127
|
+
const garuda = data.garuda;
|
|
128
|
+
out.push({
|
|
129
|
+
path: folder,
|
|
130
|
+
model: typeof data.spec === "string" ? data.spec : typeof data.model === "string" ? data.model : void 0,
|
|
131
|
+
version: typeof garuda?.version === "string" ? garuda.version : void 0,
|
|
132
|
+
commit: typeof garuda?.commit === "string" ? garuda.commit : void 0,
|
|
133
|
+
suite: typeof data.suite === "string" ? data.suite : void 0,
|
|
134
|
+
repeat: typeof data.repeat === "number" ? data.repeat : void 0,
|
|
135
|
+
seed: typeof data.seed === "number" ? data.seed : void 0,
|
|
136
|
+
tasks: typeof data.tasks === "object" && data.tasks !== null ? data.tasks : void 0,
|
|
137
|
+
results: Array.isArray(data.results) ? data.results : [],
|
|
138
|
+
mtimeMs: statSync(file).mtimeMs
|
|
139
|
+
});
|
|
140
|
+
} catch {
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return out.sort((x, y) => y.mtimeMs - x.mtimeMs);
|
|
144
|
+
}
|
|
145
|
+
var sameTasks = (a, b) => a !== void 0 && Object.keys(a).length === Object.keys(b).length && Object.entries(b).every(([id, hash]) => a[id] === hash);
|
|
146
|
+
function reusableRun(runs, want) {
|
|
147
|
+
const count = Object.keys(want.tasks).length * want.repeat;
|
|
148
|
+
return runs.find(
|
|
149
|
+
(r) => r.model === want.model && r.version === want.version && r.commit === want.commit && r.suite === "repo" && r.repeat === want.repeat && r.seed === want.seed && sameTasks(r.tasks, want.tasks) && r.results.length === count
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
function pastCostPerRun(runs, model, tasks) {
|
|
153
|
+
const results = runs.filter((r) => r.model === model && sameTasks(r.tasks, tasks)).flatMap((r) => r.results);
|
|
154
|
+
if (results.length === 0 || results.some((r) => r.costUsd === void 0)) return void 0;
|
|
155
|
+
return results.reduce((sum, r) => sum + (r.costUsd ?? 0), 0) / results.length;
|
|
156
|
+
}
|
|
157
|
+
function repricedSessionCost(file, price) {
|
|
158
|
+
let text;
|
|
159
|
+
try {
|
|
160
|
+
text = readFileSync(file, "utf8");
|
|
161
|
+
} catch {
|
|
162
|
+
return void 0;
|
|
163
|
+
}
|
|
164
|
+
let usd = 0;
|
|
165
|
+
for (const line of text.split("\n")) {
|
|
166
|
+
if (line.trim() === "") continue;
|
|
167
|
+
let record;
|
|
168
|
+
try {
|
|
169
|
+
record = JSON.parse(line);
|
|
170
|
+
} catch {
|
|
171
|
+
return void 0;
|
|
172
|
+
}
|
|
173
|
+
if (record.type === "unreported" || record.type === "child_debit") return void 0;
|
|
174
|
+
if (line.includes('"subagent":{')) return void 0;
|
|
175
|
+
if (record.type === "assistant") {
|
|
176
|
+
const cost = responseCost(record.response, price);
|
|
177
|
+
if (cost === void 0) return void 0;
|
|
178
|
+
usd += cost;
|
|
179
|
+
}
|
|
180
|
+
if (record.type === "side_usage") usd += costOf(record.usage, price);
|
|
181
|
+
}
|
|
182
|
+
return usd;
|
|
183
|
+
}
|
|
184
|
+
function repricedCostPerRun(runs, model, tasks, price) {
|
|
185
|
+
if (price === void 0) return void 0;
|
|
186
|
+
const costs = [];
|
|
187
|
+
for (const run of runs.filter((r) => r.model === model && sameTasks(r.tasks, tasks))) {
|
|
188
|
+
for (const result of run.results) {
|
|
189
|
+
const file = result.sessionFile;
|
|
190
|
+
if (file === void 0) {
|
|
191
|
+
costs.push(void 0);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
const near = join(run.path, basename(file));
|
|
195
|
+
costs.push(repricedSessionCost(near, price) ?? repricedSessionCost(file, price));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const known = costs.filter((c) => c !== void 0);
|
|
199
|
+
if (known.length === 0 || known.length * 2 < costs.length) return void 0;
|
|
200
|
+
return known.reduce((sum, c) => sum + c, 0) / known.length;
|
|
201
|
+
}
|
|
202
|
+
function repriceFactor(runs, source, target, price) {
|
|
203
|
+
const useful = runs.filter((r) => r.suite === "repo" && r.results.some((x) => x.passed));
|
|
204
|
+
const sets = [];
|
|
205
|
+
for (const run of useful.filter((r) => r.model === source && r.tasks !== void 0)) {
|
|
206
|
+
const tasks = run.tasks;
|
|
207
|
+
if (sets.some((t) => sameTasks(t, tasks))) continue;
|
|
208
|
+
if (useful.some((r) => r.model === target && sameTasks(r.tasks, tasks))) sets.push(tasks);
|
|
209
|
+
}
|
|
210
|
+
const ratios = [];
|
|
211
|
+
for (const tasks of sets) {
|
|
212
|
+
const real = pastCostPerRun(useful, target, tasks);
|
|
213
|
+
const repriced = repricedCostPerRun(useful, source, tasks, price);
|
|
214
|
+
if (real !== void 0 && repriced !== void 0 && repriced > 0) ratios.push(real / repriced);
|
|
215
|
+
}
|
|
216
|
+
if (ratios.length === 0) return void 0;
|
|
217
|
+
return { value: ratios.reduce((sum, r) => sum + r, 0) / ratios.length, sets: ratios.length };
|
|
218
|
+
}
|
|
219
|
+
function costEstimate(runs, own, repriced, price) {
|
|
220
|
+
if (own !== void 0) return { usd: own * runs, how: "from earlier runs" };
|
|
221
|
+
if (repriced.perRun !== void 0) {
|
|
222
|
+
const f = repriced.factor;
|
|
223
|
+
return {
|
|
224
|
+
usd: repriced.perRun * (f?.value ?? 1) * runs,
|
|
225
|
+
how: f === void 0 ? `from ${repriced.from}'s requests at this model's prices` : `from ${repriced.from}'s requests at this model's prices, \xD7 ${f.value.toFixed(2)} as in ${f.sets} earlier comparison${f.sets === 1 ? "" : "s"}`
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
return { usd: estimateUsd(price, runs), how: "a guess" };
|
|
229
|
+
}
|
|
230
|
+
var sumKnown = (values) => values.some((v) => v === void 0) ? void 0 : values.reduce((sum, v) => sum + (v ?? 0), 0);
|
|
231
|
+
function continueCommand(a, b, repeat, seed, maxCost) {
|
|
232
|
+
return `garuda advise --models ${a},${b} --repeat ${repeat} --seed ${seed} --max-cost ${Math.ceil(maxCost)}`;
|
|
233
|
+
}
|
|
234
|
+
async function runAdvise(options) {
|
|
235
|
+
const [a, b] = options.models ?? ADVISE_MODELS;
|
|
236
|
+
if (a === void 0 || b === void 0 || options.models?.length === 1 || (options.models?.length ?? 2) > 2) {
|
|
237
|
+
process.stderr.write("Give two models: --models <a>,<b>.\n");
|
|
238
|
+
return 1;
|
|
239
|
+
}
|
|
240
|
+
const repeat = options.repeat ?? ADVISE_REPEAT;
|
|
241
|
+
const maxCost = options.maxCost ?? ADVISE_MAX_COST;
|
|
242
|
+
const seed = options.seed ?? 1;
|
|
243
|
+
const parallel = options.parallel ?? ADVISE_PARALLEL;
|
|
244
|
+
if (!Number.isInteger(repeat) || repeat < 1 || !(maxCost > 0) || !Number.isInteger(parallel) || parallel < 1) {
|
|
245
|
+
process.stderr.write(
|
|
246
|
+
"--repeat and --parallel must be whole numbers of 1 or more, and --max-cost above 0.\n"
|
|
247
|
+
);
|
|
248
|
+
return 1;
|
|
249
|
+
}
|
|
250
|
+
const { repoSuite, runEvalCommand } = await import("./evalCommand-MRO6ENCA.js");
|
|
251
|
+
const suite = await repoSuite();
|
|
252
|
+
if (typeof suite === "string") {
|
|
253
|
+
process.stderr.write(`${suite}
|
|
254
|
+
`);
|
|
255
|
+
return 1;
|
|
256
|
+
}
|
|
257
|
+
const repoTasks = repoEvalTasks(suite);
|
|
258
|
+
const tasks = repoTasks.length;
|
|
259
|
+
const hashes = Object.fromEntries(repoTasks.map((t) => [t.id, taskHash(t)]));
|
|
260
|
+
const models = await loadModelsConfig();
|
|
261
|
+
if (models.problem !== void 0) {
|
|
262
|
+
process.stderr.write(`${models.problem}
|
|
263
|
+
`);
|
|
264
|
+
return 1;
|
|
265
|
+
}
|
|
266
|
+
let prices;
|
|
267
|
+
try {
|
|
268
|
+
prices = [resolveModel(a, models.config).info.price, resolveModel(b, models.config).info.price];
|
|
269
|
+
} catch (error) {
|
|
270
|
+
process.stderr.write(`${error.message}
|
|
271
|
+
`);
|
|
272
|
+
return 1;
|
|
273
|
+
}
|
|
274
|
+
const evalsDir = join(process.cwd(), ".garuda", "evals");
|
|
275
|
+
const past = pastRuns(evalsDir);
|
|
276
|
+
const commit = BUILD_COMMIT ?? await garudaCommit();
|
|
277
|
+
const want = { repeat, seed, tasks: hashes, version: VERSION, commit };
|
|
278
|
+
const reuse = [
|
|
279
|
+
reusableRun(past, { ...want, model: a }),
|
|
280
|
+
reusableRun(past, { ...want, model: b })
|
|
281
|
+
];
|
|
282
|
+
const runs = tasks * repeat;
|
|
283
|
+
const perRun = [pastCostPerRun(past, a, hashes), pastCostPerRun(past, b, hashes)];
|
|
284
|
+
const repriced = (i, from) => perRun[i] === void 0 ? repricedCostPerRun(past, from, hashes, prices[i]) : void 0;
|
|
285
|
+
const factors = [
|
|
286
|
+
perRun[0] === void 0 ? repriceFactor(past, b, a, prices[0]) : void 0,
|
|
287
|
+
perRun[1] === void 0 ? repriceFactor(past, a, b, prices[1]) : void 0
|
|
288
|
+
];
|
|
289
|
+
const estimates = [
|
|
290
|
+
costEstimate(
|
|
291
|
+
runs,
|
|
292
|
+
perRun[0],
|
|
293
|
+
{ perRun: repriced(0, b), from: b, factor: factors[0] },
|
|
294
|
+
prices[0]
|
|
295
|
+
),
|
|
296
|
+
costEstimate(
|
|
297
|
+
runs,
|
|
298
|
+
perRun[1],
|
|
299
|
+
{ perRun: repriced(1, a), from: a, factor: factors[1] },
|
|
300
|
+
prices[1]
|
|
301
|
+
)
|
|
302
|
+
];
|
|
303
|
+
const line = (model, i) => {
|
|
304
|
+
const r = reuse[i];
|
|
305
|
+
const e = estimates[i];
|
|
306
|
+
return r !== void 0 ? ` ${model}: reuses ${r.path} (Garuda ${r.version}, ${r.results.length} runs)` : ` ${model}: ${runs} runs, ${e?.usd === void 0 ? "cost unknown" : `about $${e.usd.toFixed(2)} (${e.how})`}`;
|
|
307
|
+
};
|
|
308
|
+
const toSpend = sumKnown(
|
|
309
|
+
[0, 1].filter((i) => reuse[i] === void 0).map((i) => estimates[i]?.usd)
|
|
310
|
+
);
|
|
311
|
+
process.stderr.write(
|
|
312
|
+
[
|
|
313
|
+
`Your repo: ${tasks} task${tasks === 1 ? "" : "s"} from its history (tests that fail before the commit and pass after it).`,
|
|
314
|
+
`${repeat} run${repeat === 1 ? "" : "s"} a task, ${parallel} at a time, seed ${seed}:`,
|
|
315
|
+
line(a, 0),
|
|
316
|
+
line(b, 1),
|
|
317
|
+
`Cap: --max-cost $${maxCost}; it stops before a model that would pass it.`,
|
|
318
|
+
""
|
|
319
|
+
].join("\n")
|
|
320
|
+
);
|
|
321
|
+
if (toSpend !== void 0 && toSpend > maxCost) {
|
|
322
|
+
process.stderr.write(
|
|
323
|
+
`The estimate passes the cap. To go on: ${continueCommand(a, b, repeat, seed, toSpend * 1.25)}
|
|
324
|
+
`
|
|
325
|
+
);
|
|
326
|
+
return 1;
|
|
327
|
+
}
|
|
328
|
+
if (options.yes !== true && (reuse[0] === void 0 || reuse[1] === void 0)) {
|
|
329
|
+
if (!process.stdin.isTTY) {
|
|
330
|
+
process.stderr.write("Add --yes to start without a question.\n");
|
|
331
|
+
return 1;
|
|
332
|
+
}
|
|
333
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
334
|
+
const answer = await rl.question("Start? (y/N) ");
|
|
335
|
+
rl.close();
|
|
336
|
+
if (!/^y(es)?$/i.test(answer.trim())) return 1;
|
|
337
|
+
}
|
|
338
|
+
const dir = join(evalsDir, `advise-${newSessionId()}`);
|
|
339
|
+
mkdirSync(dir, { recursive: true });
|
|
340
|
+
const runModel = async (model, label, i) => {
|
|
341
|
+
const earlier = reuse[i];
|
|
342
|
+
if (earlier !== void 0) {
|
|
343
|
+
process.stderr.write(`Reusing ${earlier.path} for ${model}.
|
|
344
|
+
`);
|
|
345
|
+
return earlier.path;
|
|
346
|
+
}
|
|
347
|
+
const outDir = join(dir, label);
|
|
348
|
+
const code = await runEvalCommand({
|
|
349
|
+
model,
|
|
350
|
+
suite: "repo",
|
|
351
|
+
repeat,
|
|
352
|
+
shuffle: true,
|
|
353
|
+
seed,
|
|
354
|
+
parallel,
|
|
355
|
+
outDir,
|
|
356
|
+
progress: model.replace(/^claude-/, "")
|
|
357
|
+
});
|
|
358
|
+
return code === 1 ? void 0 : outDir;
|
|
359
|
+
};
|
|
360
|
+
const pathA = await runModel(a, "a", 0);
|
|
361
|
+
if (pathA === void 0) return 1;
|
|
362
|
+
const first = await loadReport(pathA);
|
|
363
|
+
if (reuse[0] === void 0 && reuse[1] === void 0) {
|
|
364
|
+
const spent = costPerPass(first.results);
|
|
365
|
+
const spentA = spent.kind === "unknown" ? void 0 : spent.totalUsd;
|
|
366
|
+
const nextB = costEstimate(
|
|
367
|
+
runs,
|
|
368
|
+
perRun[1],
|
|
369
|
+
{
|
|
370
|
+
perRun: repricedCostPerRun(pastRuns(evalsDir), a, hashes, prices[1]),
|
|
371
|
+
from: a,
|
|
372
|
+
factor: factors[1]
|
|
373
|
+
},
|
|
374
|
+
prices[1]
|
|
375
|
+
).usd;
|
|
376
|
+
if (spentA !== void 0 && nextB !== void 0 && spentA + nextB > maxCost) {
|
|
377
|
+
process.stderr.write(
|
|
378
|
+
[
|
|
379
|
+
`Stopped before ${b}: ${a} cost $${spentA.toFixed(2)}, and ${b} would cost about $${nextB.toFixed(2)}, past $${maxCost}.`,
|
|
380
|
+
`${a}'s run is kept and will be reused. To go on: ${continueCommand(a, b, repeat, seed, nextB * 1.25)}`,
|
|
381
|
+
""
|
|
382
|
+
].join("\n")
|
|
383
|
+
);
|
|
384
|
+
return 1;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
const pathB = await runModel(b, "b", 1);
|
|
388
|
+
if (pathB === void 0) return 1;
|
|
389
|
+
const second = await loadReport(pathB);
|
|
390
|
+
const result = compareReports(first, second, { seed });
|
|
391
|
+
if ("problems" in result) {
|
|
392
|
+
process.stderr.write(`The two runs cannot be compared:
|
|
393
|
+
${result.problems.join("\n")}
|
|
394
|
+
`);
|
|
395
|
+
return 1;
|
|
396
|
+
}
|
|
397
|
+
const comparison = formatComparison(first, second, result.comparison);
|
|
398
|
+
const advice = adviceText(a, b, result.comparison, tasks);
|
|
399
|
+
writeFileSync(join(dir, "advice.txt"), `${advice}
|
|
400
|
+
|
|
401
|
+
${comparison}
|
|
402
|
+
`);
|
|
403
|
+
writeFileSync(join(dir, "report.html"), adviceHtml(comparison, advice));
|
|
404
|
+
process.stdout.write(`
|
|
405
|
+
${comparison}
|
|
406
|
+
|
|
407
|
+
${advice}
|
|
408
|
+
Report: ${join(dir, "report.html")}
|
|
409
|
+
`);
|
|
410
|
+
return 0;
|
|
411
|
+
}
|
|
412
|
+
export {
|
|
413
|
+
ADVISE_MAX_COST,
|
|
414
|
+
ADVISE_MODELS,
|
|
415
|
+
ADVISE_PARALLEL,
|
|
416
|
+
ADVISE_REPEAT,
|
|
417
|
+
GUESS_INPUT_TOKENS,
|
|
418
|
+
GUESS_OUTPUT_TOKENS,
|
|
419
|
+
adviceHtml,
|
|
420
|
+
adviceText,
|
|
421
|
+
continueCommand,
|
|
422
|
+
costEstimate,
|
|
423
|
+
estimateUsd,
|
|
424
|
+
pastCostPerRun,
|
|
425
|
+
pastRuns,
|
|
426
|
+
repriceFactor,
|
|
427
|
+
repricedCostPerRun,
|
|
428
|
+
repricedSessionCost,
|
|
429
|
+
reusableRun,
|
|
430
|
+
runAdvise,
|
|
431
|
+
sumKnown
|
|
432
|
+
};
|
|
433
|
+
//# sourceMappingURL=advise-C777SZ5M.js.map
|