@workos/quickstudy 0.0.1
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/LICENSE +21 -0
- package/README.md +270 -0
- package/examples/harbor-notes/README.md +40 -0
- package/examples/harbor-notes/evals/create-note/EVAL.ts +14 -0
- package/examples/harbor-notes/evals/create-note/PROMPT.md +9 -0
- package/examples/harbor-notes/evals/create-note/local/README.txt +1 -0
- package/examples/harbor-notes/experiments/scripted.ts +6 -0
- package/examples/harbor-notes/package.json +6 -0
- package/examples/harbor-notes/quickstudy.identity.json +1 -0
- package/examples/harbor-notes/runtime.ts +48 -0
- package/examples/harbor-notes/semantic-example.ts +21 -0
- package/images/agent-runtime/Dockerfile +58 -0
- package/images/egress-proxy/Dockerfile +28 -0
- package/images/mcp-proxy/Dockerfile +30 -0
- package/package.json +53 -0
- package/src/adapters/claude.ts +107 -0
- package/src/adapters/codex.ts +107 -0
- package/src/adapters/echo.ts +57 -0
- package/src/adapters/parse.ts +117 -0
- package/src/adapters/types.ts +152 -0
- package/src/build-info.generated.ts +12 -0
- package/src/cli.ts +787 -0
- package/src/completeness.ts +104 -0
- package/src/diagnose/excerpt.ts +106 -0
- package/src/diagnose/prompt.ts +175 -0
- package/src/diagnose/render.ts +55 -0
- package/src/diagnose/run.ts +290 -0
- package/src/diagnose/select.ts +110 -0
- package/src/diagnose/types.ts +88 -0
- package/src/evals/discovery.ts +173 -0
- package/src/evals/prompt.ts +190 -0
- package/src/evals/result.ts +10 -0
- package/src/evals/types.ts +115 -0
- package/src/execution-policy.ts +71 -0
- package/src/experiments/discovery.ts +76 -0
- package/src/experiments/groups.ts +119 -0
- package/src/experiments/types.ts +116 -0
- package/src/export-types.ts +127 -0
- package/src/export.ts +381 -0
- package/src/hash.ts +74 -0
- package/src/identity-diff.ts +30 -0
- package/src/ids.ts +30 -0
- package/src/index.ts +58 -0
- package/src/isolation/docker.ts +639 -0
- package/src/isolation/image-contexts.generated.ts +927 -0
- package/src/isolation/images.ts +138 -0
- package/src/isolation/mcp-proxy/server.ts +260 -0
- package/src/isolation/mcp.ts +144 -0
- package/src/isolation/proxy/allowlist.ts +148 -0
- package/src/isolation/proxy/server.ts +382 -0
- package/src/llm.ts +132 -0
- package/src/manifest.ts +228 -0
- package/src/model-identity.ts +12 -0
- package/src/plan.ts +55 -0
- package/src/probe.ts +426 -0
- package/src/report/pass-at-k.ts +76 -0
- package/src/report/report.ts +731 -0
- package/src/runner/context.ts +96 -0
- package/src/runner/deadline.ts +37 -0
- package/src/runner/execute.ts +992 -0
- package/src/runner/run-lock.ts +32 -0
- package/src/runner/scheduler.ts +62 -0
- package/src/runner/score-worker.ts +107 -0
- package/src/runner/scorer-worker.ts +61 -0
- package/src/runtime/types.ts +89 -0
- package/src/secrets.ts +151 -0
- package/src/semantic.ts +185 -0
- package/src/serve.ts +52 -0
- package/src/source-identity.ts +76 -0
- package/src/store/artifacts.ts +146 -0
- package/src/store/db.ts +318 -0
- package/src/store/schema.ts +39 -0
- package/src/surface-usage.ts +297 -0
- package/src/ui-bundle.generated.ts +12 -0
- package/ui/dist/index.html +32 -0
package/src/cli.ts
ADDED
|
@@ -0,0 +1,787 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* quickstudy command router.
|
|
4
|
+
*
|
|
5
|
+
* Every load error exits non-zero with file/line context — an eval or
|
|
6
|
+
* experiment is never partially loaded, and nothing (containers, agents)
|
|
7
|
+
* starts before the roots validate and every preflight (provider keys,
|
|
8
|
+
* MCP credentials) passes.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as quickstudyApi from "./index.ts";
|
|
12
|
+
|
|
13
|
+
// Consumer modules (EVAL.ts, experiments/*.ts) import the harness by package
|
|
14
|
+
// name. From source, Bun resolves that self-reference through the root
|
|
15
|
+
// package.json; the compiled binary has no package.json or node_modules to
|
|
16
|
+
// consult when it dynamically imports those files, so the bare specifier must
|
|
17
|
+
// be served by the binary itself. Serving the embedded API also guarantees
|
|
18
|
+
// exactly one copy of the harness at runtime — a disk-resolved second copy
|
|
19
|
+
// would break instanceof checks and identity hashing.
|
|
20
|
+
Bun.plugin({
|
|
21
|
+
name: "quickstudy-self-resolution",
|
|
22
|
+
setup(build) {
|
|
23
|
+
build.module("quickstudy", () => ({ exports: quickstudyApi, loader: "object" }));
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
import { existsSync, mkdtempSync, statSync, watch, writeFileSync } from "node:fs";
|
|
28
|
+
import { tmpdir } from "node:os";
|
|
29
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
30
|
+
import { parseArgs } from "node:util";
|
|
31
|
+
import {
|
|
32
|
+
dockerDaemonReachable,
|
|
33
|
+
removeAttemptContainers,
|
|
34
|
+
removeRunResources,
|
|
35
|
+
} from "./isolation/docker.ts";
|
|
36
|
+
import {
|
|
37
|
+
buildAgentRuntimeImage,
|
|
38
|
+
buildEgressProxyImage,
|
|
39
|
+
buildMcpProxyImage,
|
|
40
|
+
} from "./isolation/images.ts";
|
|
41
|
+
import { EMBEDDED_BUILD_INFO } from "./build-info.generated.ts";
|
|
42
|
+
import {
|
|
43
|
+
buildReport,
|
|
44
|
+
renderReportText,
|
|
45
|
+
ReportError,
|
|
46
|
+
writeReportJson,
|
|
47
|
+
type ReportJson,
|
|
48
|
+
} from "./report/report.ts";
|
|
49
|
+
import { ExportError, exportSite } from "./export.ts";
|
|
50
|
+
import { renderDiagnosisText } from "./diagnose/render.ts";
|
|
51
|
+
import { diagnosisJsonPath, runDiagnose } from "./diagnose/run.ts";
|
|
52
|
+
import { DiagnoseError } from "./diagnose/types.ts";
|
|
53
|
+
import { AnthropicModelClient, ModelOutputError } from "./llm.ts";
|
|
54
|
+
import { discoverEvals, importEvalScorer } from "./evals/discovery.ts";
|
|
55
|
+
import { EvalLoadError } from "./evals/prompt.ts";
|
|
56
|
+
import { discoverExperiments, ExperimentLoadError } from "./experiments/discovery.ts";
|
|
57
|
+
import { compareGroups } from "./experiments/groups.ts";
|
|
58
|
+
import { buildRunManifest } from "./manifest.ts";
|
|
59
|
+
import { filterEvals, planRun } from "./plan.ts";
|
|
60
|
+
import { executeRunV3 } from "./runner/execute.ts";
|
|
61
|
+
import { collectProviderKeys, InvalidProviderKeyError, MissingProviderKeyError } from "./secrets.ts";
|
|
62
|
+
import { serveSite } from "./serve.ts";
|
|
63
|
+
import { UI_INDEX_HTML_BASE64 } from "./ui-bundle.generated.ts";
|
|
64
|
+
import { ArtifactsStore } from "./store/artifacts.ts";
|
|
65
|
+
import { ResultsStore } from "./store/db.ts";
|
|
66
|
+
|
|
67
|
+
const USAGE = `quickstudy — eval × experiment harness for coding agents
|
|
68
|
+
|
|
69
|
+
Usage:
|
|
70
|
+
quickstudy validate Validate the evals and experiments roots
|
|
71
|
+
[--evals-root <dir>] Evals root (default: evals)
|
|
72
|
+
[--experiments-root <dir>] Experiments root (default: experiments)
|
|
73
|
+
quickstudy run [--eval <id>...] Execute evals against experiments
|
|
74
|
+
[--experiment <id>...] Omit either selector to include every discovered item
|
|
75
|
+
[--evals-root <dir>] Evals root (default: evals)
|
|
76
|
+
[--experiments-root <dir>] Experiments root (default: experiments)
|
|
77
|
+
[--suite <s>]... [--framework <f>]... Metadata filters applied before planning
|
|
78
|
+
[--trials <n>] Trials per eval × experiment pair (default: 3)
|
|
79
|
+
[--egress-proxy] Route container egress through the per-attempt allowlist proxy
|
|
80
|
+
[--db <path>] [--results <dir>] Stores (default: results.db, results/)
|
|
81
|
+
quickstudy clean Remove all quickstudy-labeled containers and networks
|
|
82
|
+
quickstudy images build [--pull] Build the agent-runtime, egress-proxy and mcp-proxy images
|
|
83
|
+
[--images-dir <dir>] Build from this checkout's Dockerfiles instead of the embedded copies
|
|
84
|
+
quickstudy report <run-id> Build report.json for a run and print the text report
|
|
85
|
+
[--latest] Report the most recent run instead of naming one
|
|
86
|
+
[--json] Print report.json instead of the text report
|
|
87
|
+
[--strict] Exit non-zero when incomplete or any attempt errored (CI/publication)
|
|
88
|
+
[--db <path>] [--results <dir>] Stores (default: results.db, results/)
|
|
89
|
+
quickstudy diagnose <run-id> LLM-diagnose a run's failing pairs into diagnosis.json
|
|
90
|
+
[--latest] Diagnose the most recent run instead of naming one
|
|
91
|
+
[--db <path>] [--results <dir>] Stores (default: results.db, results/)
|
|
92
|
+
quickstudy export <run-id>... Materialize a run as a self-contained static site
|
|
93
|
+
[--runs <a,b|all>] More run ids (comma-separated), or "all" for every run in the DB
|
|
94
|
+
[--out <dir>] Output directory (default: ./site)
|
|
95
|
+
[--db <path>] [--results <dir>] Stores (default: results.db, results/)
|
|
96
|
+
quickstudy ui Export to a temp dir and serve the UI locally
|
|
97
|
+
[--runs <a,b>] Runs to include (default: every run in the DB)
|
|
98
|
+
[--port <n>] Listen port (default: 4173)
|
|
99
|
+
[--watch] Re-export when the results DB changes
|
|
100
|
+
[--db <path>] [--results <dir>] Stores (default: results.db, results/)
|
|
101
|
+
`;
|
|
102
|
+
|
|
103
|
+
const COMMAND_HELP: Record<string, string> = {
|
|
104
|
+
validate: `Usage: quickstudy validate [--evals-root <dir>] [--experiments-root <dir>]\n\nDiscover and validate the evals root (frontmatter, scorer default exports)\nand the experiments root (defineExperiment shapes, comparison groups).\nDefaults: evals, experiments.`,
|
|
105
|
+
run: `Usage: quickstudy run [--eval <id>...] [--experiment <id>...] [options]\n\nExecute the selected eval × experiment pairs. Omit --eval to select every\ndiscovered eval; omit --experiment to select every discovered experiment.\n\nOptions:\n --eval <id>... --experiment <id>...\n --evals-root <dir> --experiments-root <dir> (defaults: evals, experiments)\n --suite <s>... --framework <f>...\n --trials <n> --egress-proxy\n --concurrency <n> --seed <value> (default concurrency: 1)\n --resume <run-id> --retry-unfinished (explicit error/timeout retries)\n --provision-ms <n> --agent-ms <n> --score-ms <n>\n --export-ms <n> --cleanup-ms <n>\n --db <path> --results <dir>`,
|
|
106
|
+
report: `Usage: quickstudy report <run-id> [options]\n\nBuild report.json into the results dir and print the text report\n(pass \`--latest\` to pick the most recent run; \`--json\` prints the JSON).\n\nOptions:\n --latest --json --strict\n --strict exits non-zero for incomplete runs or any attempt error\n --db <path> --results <dir>`,
|
|
107
|
+
diagnose: `Usage: quickstudy diagnose <run-id> [options]\n\nSend each failing (eval × experiment) pair's harness-selected evidence to a\nstructured model call and write diagnosis.json beside report.json\n(pass \`--latest\` to pick the most recent run). Findings are hypotheses with\nevidence links — never verdicts. Requires ANTHROPIC_API_KEY; override the\nanalyst model with QUICKSTUDY_ANALYST_MODEL.\n\nOptions:\n --latest\n --db <path> --results <dir>`,
|
|
108
|
+
clean: `Usage: quickstudy clean\n\nRemove quickstudy-labeled containers, networks, and sidecars.`,
|
|
109
|
+
export: `Usage: quickstudy export <run-id>... [options]\n\nBuild a self-contained static site.\n\nOptions:\n --runs <a,b|all> --out <dir> --db <path> --results <dir>\n --mode <offline|hosted> (default: offline)\n --expected-trials <n> Publish fully sampled pairs; fail if none\n Writes omissions and retains source diagnostics`,
|
|
110
|
+
ui: `Usage: quickstudy ui [options]\n\nExport stored runs to a temporary directory and serve the local UI.\n\nOptions:\n --runs <a,b> --port <n> --watch --db <path> --results <dir>`,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const IMAGES_HELP = `Usage: quickstudy images <command>\n\nCommands:\n build Build the agent runtime, egress proxy, and MCP proxy images.\n\nRun \`quickstudy images build --help\` for build options.`;
|
|
114
|
+
const IMAGES_BUILD_HELP = `Usage: quickstudy images build [--pull] [--images-dir <dir>]\n\nBuild all quickstudy runtime and proxy images. --pull refreshes base images; --images-dir uses contexts from a checkout.`;
|
|
115
|
+
|
|
116
|
+
function helpRequested(args: string[]): boolean {
|
|
117
|
+
return args.includes("--help") || args.includes("-h");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function fail(message: string): never {
|
|
121
|
+
console.error(`quickstudy: ${message}`);
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function commandValidate(args: string[]): Promise<void> {
|
|
126
|
+
let values: { "evals-root": string; "experiments-root": string };
|
|
127
|
+
let positionals: string[];
|
|
128
|
+
try {
|
|
129
|
+
({ values, positionals } = parseArgs({
|
|
130
|
+
args,
|
|
131
|
+
allowPositionals: true,
|
|
132
|
+
options: {
|
|
133
|
+
"evals-root": { type: "string", default: "evals" },
|
|
134
|
+
"experiments-root": { type: "string", default: "experiments" },
|
|
135
|
+
},
|
|
136
|
+
}));
|
|
137
|
+
} catch (err) {
|
|
138
|
+
fail(`validate: ${err instanceof Error ? err.message : String(err)}\n\n${USAGE}`);
|
|
139
|
+
}
|
|
140
|
+
if (positionals.length > 0) {
|
|
141
|
+
fail(
|
|
142
|
+
`validate: unexpected argument "${positionals[0]}" — validate takes no positionals; ` +
|
|
143
|
+
`point --evals-root/--experiments-root at your directories instead`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const evalsRoot = values["evals-root"];
|
|
148
|
+
const experimentsRoot = values["experiments-root"];
|
|
149
|
+
const haveEvals = existsSync(evalsRoot) && statSync(evalsRoot).isDirectory();
|
|
150
|
+
const haveExperiments = existsSync(experimentsRoot) && statSync(experimentsRoot).isDirectory();
|
|
151
|
+
if (!haveEvals && !haveExperiments) {
|
|
152
|
+
fail(
|
|
153
|
+
`validate: neither an evals root ("${evalsRoot}") nor an experiments root ` +
|
|
154
|
+
`("${experimentsRoot}") exists\n\n${USAGE}`,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
if (haveEvals) {
|
|
158
|
+
const evals = discoverEvals(evalsRoot);
|
|
159
|
+
// A missing/non-function default export is a validation error HERE, not
|
|
160
|
+
// at attempt time.
|
|
161
|
+
for (const loaded of evals) await importEvalScorer(loaded.scorerPath);
|
|
162
|
+
console.log(`evals root "${evalsRoot}" is valid`);
|
|
163
|
+
console.log(` evals: ${evals.length}`);
|
|
164
|
+
for (const loaded of evals) {
|
|
165
|
+
const meta = loaded.metadata;
|
|
166
|
+
console.log(
|
|
167
|
+
` - ${meta.id} (suite: ${meta.suite}` +
|
|
168
|
+
`${meta.product !== undefined ? `; product: ${meta.product}` : ""}` +
|
|
169
|
+
`${meta.framework !== undefined ? `; framework: ${meta.framework}` : ""}` +
|
|
170
|
+
`${loaded.localDir !== null ? "; local/" : ""})`,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (haveExperiments) {
|
|
175
|
+
const experiments = await discoverExperiments(experimentsRoot);
|
|
176
|
+
console.log(`experiments root "${experimentsRoot}" is valid`);
|
|
177
|
+
console.log(` experiments: ${experiments.length}`);
|
|
178
|
+
for (const { experiment } of experiments) {
|
|
179
|
+
console.log(
|
|
180
|
+
` - ${experiment.id} (agent: ${experiment.agent.adapter}; runtime: ${experiment.runtime.kind}` +
|
|
181
|
+
`${experiment.treatment !== undefined ? `; treatment: ${experiment.treatment}` : ""}` +
|
|
182
|
+
`${experiment.comparisonGroup !== undefined ? `; group: ${experiment.comparisonGroup}` : ""})`,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
// Comparison groups: agreement is checked here so an author sees a
|
|
186
|
+
// withheld group at validate time, not after a paid run. Never an error —
|
|
187
|
+
// withhold-and-diff, by design.
|
|
188
|
+
const groups = compareGroups(
|
|
189
|
+
experiments.map((entry) => entry.experiment),
|
|
190
|
+
buildRunManifest({ evals: [], experiments }),
|
|
191
|
+
);
|
|
192
|
+
if (groups.length > 0) {
|
|
193
|
+
console.log(` comparison groups: ${groups.length}`);
|
|
194
|
+
for (const group of groups) {
|
|
195
|
+
console.log(` - ${group.group}: ${group.experimentIds.join(", ")} (comparison ${group.comparison})`);
|
|
196
|
+
for (const detail of group.details) console.log(` ${detail}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function parsePositiveInt(value: string, flag: string): number {
|
|
203
|
+
const parsed = Number(value);
|
|
204
|
+
if (!Number.isInteger(parsed) || parsed < 1) fail(`${flag} must be an integer >= 1, got "${value}"`);
|
|
205
|
+
return parsed;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** The run path: discover, filter, plan, execute, persist, summarize. */
|
|
209
|
+
async function commandRun(args: string[]): Promise<void> {
|
|
210
|
+
let values: RunFlags;
|
|
211
|
+
try {
|
|
212
|
+
values = parseRunFlags(args);
|
|
213
|
+
} catch (err) {
|
|
214
|
+
fail(`run: ${err instanceof Error ? err.message : String(err)}\n\n${USAGE}`);
|
|
215
|
+
}
|
|
216
|
+
if (values.resume) {
|
|
217
|
+
const resumeStore = new ResultsStore(values.db);
|
|
218
|
+
try {
|
|
219
|
+
const previous = resumeStore.getRun(values.resume);
|
|
220
|
+
if (!previous) fail(`run: no run "${values.resume}" to resume`);
|
|
221
|
+
const supplied = (flag: string) => args.some((arg) => arg === flag || arg.startsWith(`${flag}=`));
|
|
222
|
+
if (!supplied("--trials")) values.trials = String(previous.config.trials);
|
|
223
|
+
if (!supplied("--eval")) values.eval = previous.config.evalIds;
|
|
224
|
+
if (!supplied("--experiment")) values.experiment = previous.config.experimentIds;
|
|
225
|
+
if (!supplied("--evals-root")) values["evals-root"] = previous.config.evalsRoot ?? values["evals-root"];
|
|
226
|
+
if (!supplied("--experiments-root")) values["experiments-root"] = previous.config.experimentsRoot ?? values["experiments-root"];
|
|
227
|
+
if (!supplied("--egress-proxy")) values["egress-proxy"] = previous.config.egress?.proxy ?? false;
|
|
228
|
+
const budgets = Object.values(previous.manifest.pairs ?? {})[0]?.budgets;
|
|
229
|
+
for (const [flag, key] of [["provision-ms", "provisionMs"], ["agent-ms", "agentMs"], ["score-ms", "scoreMs"], ["export-ms", "exportMs"], ["cleanup-ms", "cleanupMs"]] as const) {
|
|
230
|
+
if (!supplied(`--${flag}`) && budgets) values[flag] = String(budgets[key]);
|
|
231
|
+
}
|
|
232
|
+
} finally { resumeStore.close(); }
|
|
233
|
+
}
|
|
234
|
+
const trials = parsePositiveInt(values.trials, "--trials");
|
|
235
|
+
const evalsRoot = values["evals-root"];
|
|
236
|
+
const experimentsRoot = values["experiments-root"];
|
|
237
|
+
|
|
238
|
+
let evals;
|
|
239
|
+
try {
|
|
240
|
+
evals = discoverEvals(evalsRoot);
|
|
241
|
+
} catch (err) {
|
|
242
|
+
if (err instanceof Error && /is not a directory/.test(err.message)) {
|
|
243
|
+
fail(`run: ${err.message} (point --evals-root at your evals directory)`);
|
|
244
|
+
}
|
|
245
|
+
throw err;
|
|
246
|
+
}
|
|
247
|
+
let experiments;
|
|
248
|
+
try {
|
|
249
|
+
experiments = await discoverExperiments(experimentsRoot);
|
|
250
|
+
} catch (err) {
|
|
251
|
+
if (err instanceof Error && /is not a directory/.test(err.message)) {
|
|
252
|
+
fail(`run: ${err.message} (point --experiments-root at your experiments directory)`);
|
|
253
|
+
}
|
|
254
|
+
throw err;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const evalSelection = values.eval ?? [];
|
|
258
|
+
for (const id of evalSelection) {
|
|
259
|
+
if (!evals.some((entry) => entry.metadata.id === id)) {
|
|
260
|
+
fail(`run: no eval "${id}" under ${evalsRoot} (available: ${evals.map((entry) => entry.metadata.id).join(", ") || "none"})`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (evalSelection.length > 0) evals = evals.filter((entry) => evalSelection.includes(entry.metadata.id));
|
|
264
|
+
|
|
265
|
+
const experimentSelection = values.experiment ?? [];
|
|
266
|
+
for (const id of experimentSelection) {
|
|
267
|
+
if (!experiments.some((entry) => entry.experiment.id === id)) {
|
|
268
|
+
fail(
|
|
269
|
+
`run: no experiment "${id}" under ${experimentsRoot} ` +
|
|
270
|
+
`(available: ${experiments.map((entry) => entry.experiment.id).join(", ") || "none"})`,
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (experimentSelection.length > 0) {
|
|
275
|
+
experiments = experiments.filter((entry) => experimentSelection.includes(entry.experiment.id));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Metadata filters apply to the eval list BEFORE planning.
|
|
279
|
+
evals = filterEvals(evals, {
|
|
280
|
+
...(values.suite ? { suites: values.suite } : {}),
|
|
281
|
+
...(values.framework ? { frameworks: values.framework } : {}),
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const plan = planRun(evals, experiments.map((entry) => entry.experiment), trials);
|
|
285
|
+
if (plan.length === 0) {
|
|
286
|
+
fail(`run: nothing to run — the selection matched ${evals.length} eval(s) and ${experiments.length} experiment(s)`);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const store = new ResultsStore(values.db);
|
|
290
|
+
const artifacts = new ArtifactsStore(values.results);
|
|
291
|
+
const cancellation = new AbortController();
|
|
292
|
+
const interrupt = () => cancellation.abort(new Error("run interrupted"));
|
|
293
|
+
process.on("SIGINT", interrupt); process.on("SIGTERM", interrupt);
|
|
294
|
+
try {
|
|
295
|
+
const summary = await executeRunV3({
|
|
296
|
+
evals,
|
|
297
|
+
experiments,
|
|
298
|
+
plan,
|
|
299
|
+
store,
|
|
300
|
+
artifacts,
|
|
301
|
+
egressProxy: values["egress-proxy"],
|
|
302
|
+
...(values.resume ? { resumeRunId: values.resume } : {}),
|
|
303
|
+
retryUnfinished: values["retry-unfinished"],
|
|
304
|
+
...(values.concurrency ? { concurrency: parsePositiveInt(values.concurrency, "--concurrency") } : {}),
|
|
305
|
+
...(values.seed !== undefined ? { seed: values.seed } : {}),
|
|
306
|
+
signal: cancellation.signal,
|
|
307
|
+
budgets: Object.fromEntries([["provision-ms", "provisionMs"], ["agent-ms", "agentMs"], ["score-ms", "scoreMs"], ["export-ms", "exportMs"], ["cleanup-ms", "cleanupMs"]].flatMap(([flag, key]) => {
|
|
308
|
+
const value = values[flag as keyof RunFlags]; return typeof value === "string" ? [[key, parsePositiveInt(value, `--${flag}`)]] : [];
|
|
309
|
+
})),
|
|
310
|
+
config: { trials, evalsRoot, experimentsRoot },
|
|
311
|
+
});
|
|
312
|
+
console.log(
|
|
313
|
+
`run ${summary.runId}: ${summary.total} attempts — ${summary.passed} passed, ${summary.failed} failed, ` +
|
|
314
|
+
`${summary.incomplete} incomplete, ${summary.errors} errored`,
|
|
315
|
+
);
|
|
316
|
+
console.log(` results: ${values.db} + ${values.results}/${summary.runId}/`);
|
|
317
|
+
if (summary.errors > 0 || summary.incomplete > 0) process.exitCode = 1;
|
|
318
|
+
} finally {
|
|
319
|
+
process.removeListener("SIGINT", interrupt); process.removeListener("SIGTERM", interrupt);
|
|
320
|
+
store.close();
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async function commandReport(args: string[]): Promise<void> {
|
|
325
|
+
let values: ReportFlags;
|
|
326
|
+
let positionals: string[];
|
|
327
|
+
try {
|
|
328
|
+
({ values, positionals } = parseArgs({
|
|
329
|
+
args,
|
|
330
|
+
allowPositionals: true,
|
|
331
|
+
options: {
|
|
332
|
+
json: { type: "boolean", default: false },
|
|
333
|
+
strict: { type: "boolean", default: false },
|
|
334
|
+
latest: { type: "boolean", default: false },
|
|
335
|
+
db: { type: "string", default: "results.db" },
|
|
336
|
+
results: { type: "string", default: "results" },
|
|
337
|
+
},
|
|
338
|
+
}));
|
|
339
|
+
} catch (err) {
|
|
340
|
+
fail(`report: ${err instanceof Error ? err.message : String(err)}\n\n${USAGE}`);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const requestedRunId = positionals[0];
|
|
344
|
+
if (!requestedRunId && !values.latest) fail(`report: missing <run-id> (or pass --latest)\n\n${USAGE}`);
|
|
345
|
+
|
|
346
|
+
const store = new ResultsStore(values.db);
|
|
347
|
+
try {
|
|
348
|
+
let runId = requestedRunId;
|
|
349
|
+
if (values.latest) {
|
|
350
|
+
const latest = store.latestRun();
|
|
351
|
+
if (latest === null) {
|
|
352
|
+
// Zero-state, not an error: an empty database is a valid starting point.
|
|
353
|
+
console.log("this results database has no runs yet — run `quickstudy run --eval <id> --experiment <id>` first");
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
runId = latest.id;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const report: ReportJson = buildReport({ store, runId: runId as string, resultsDir: values.results });
|
|
360
|
+
const path = writeReportJson(report, values.results);
|
|
361
|
+
console.log(values.json ? JSON.stringify(report, null, 2) : renderReportText(report));
|
|
362
|
+
console.error(`report written: ${path}`);
|
|
363
|
+
if (values.strict) {
|
|
364
|
+
const problems = [
|
|
365
|
+
...(report.incomplete ? ["incomplete"] : []),
|
|
366
|
+
...(report.totals.errors > 0
|
|
367
|
+
? [`contains ${report.totals.errors} attempt error${report.totals.errors === 1 ? "" : "s"}`]
|
|
368
|
+
: []),
|
|
369
|
+
];
|
|
370
|
+
if (problems.length > 0) fail(`run ${report.run.id} is not publishable: ${problems.join("; ")} (--strict)`);
|
|
371
|
+
}
|
|
372
|
+
} finally {
|
|
373
|
+
store.close();
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
interface DiagnoseFlags {
|
|
378
|
+
latest: boolean;
|
|
379
|
+
db: string;
|
|
380
|
+
results: string;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* The explicitly separate, key-gated, token-spending step: report stays
|
|
385
|
+
* offline, diagnose calls the model. A thin flag-parser over `runDiagnose` —
|
|
386
|
+
* the engine (including `--latest` resolution) is the testable surface.
|
|
387
|
+
*/
|
|
388
|
+
async function commandDiagnose(args: string[]): Promise<void> {
|
|
389
|
+
let values: DiagnoseFlags;
|
|
390
|
+
let positionals: string[];
|
|
391
|
+
try {
|
|
392
|
+
({ values, positionals } = parseArgs({
|
|
393
|
+
args,
|
|
394
|
+
allowPositionals: true,
|
|
395
|
+
options: {
|
|
396
|
+
latest: { type: "boolean", default: false },
|
|
397
|
+
db: { type: "string", default: "results.db" },
|
|
398
|
+
results: { type: "string", default: "results" },
|
|
399
|
+
},
|
|
400
|
+
}));
|
|
401
|
+
} catch (err) {
|
|
402
|
+
fail(`diagnose: ${err instanceof Error ? err.message : String(err)}\n\n${USAGE}`);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const requestedRunId = positionals[0];
|
|
406
|
+
if (!requestedRunId && !values.latest) fail(`diagnose: missing <run-id> (or pass --latest)\n\n${USAGE}`);
|
|
407
|
+
|
|
408
|
+
// Key preflight FIRST — before any store read, and long before any spend.
|
|
409
|
+
collectProviderKeys([{ name: "diagnose", requiredHostEnv: ["ANTHROPIC_API_KEY"] }]);
|
|
410
|
+
|
|
411
|
+
const store = new ResultsStore(values.db);
|
|
412
|
+
try {
|
|
413
|
+
if (values.latest && store.latestRun() === null) {
|
|
414
|
+
// Zero-state, not an error: an empty database is a valid starting point.
|
|
415
|
+
console.log("this results database has no runs yet — run `quickstudy run --eval <id> --experiment <id>` first");
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
const diagnosis = await runDiagnose({
|
|
419
|
+
store,
|
|
420
|
+
resultsDir: values.results,
|
|
421
|
+
...(values.latest ? {} : { runId: requestedRunId as string }),
|
|
422
|
+
client: new AnthropicModelClient(),
|
|
423
|
+
log: (line) => console.error(line),
|
|
424
|
+
});
|
|
425
|
+
console.log(renderDiagnosisText(diagnosis, values.results));
|
|
426
|
+
console.error(`diagnosis written: ${diagnosisJsonPath(values.results, diagnosis.run_id)}`);
|
|
427
|
+
if (diagnosis.pairs_diagnosed === 0 && diagnosis.errors.length > 0) {
|
|
428
|
+
fail(
|
|
429
|
+
`every diagnosable pair failed to diagnose (${diagnosis.errors.length} error(s)) — ` +
|
|
430
|
+
`the errors are recorded in diagnosis.json`,
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
} finally {
|
|
434
|
+
store.close();
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async function commandClean(): Promise<void> {
|
|
439
|
+
if (!(await dockerDaemonReachable())) {
|
|
440
|
+
fail(
|
|
441
|
+
"clean: no Docker daemon reachable — nothing to inspect. " +
|
|
442
|
+
"Start your Docker runtime (Docker Desktop, OrbStack, Colima, …), confirm `docker ps` works, then re-run.",
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
// Attempt containers first: a run network cannot be removed while an
|
|
446
|
+
// attempt container is still attached.
|
|
447
|
+
const removed = await removeAttemptContainers();
|
|
448
|
+
const runResources = await removeRunResources();
|
|
449
|
+
|
|
450
|
+
const notes = [
|
|
451
|
+
removed > 0 ? `${removed} attempt container(s)` : "",
|
|
452
|
+
runResources.containers > 0 ? `${runResources.containers} sidecar container(s)` : "",
|
|
453
|
+
runResources.networks > 0 ? `${runResources.networks} run network(s)` : "",
|
|
454
|
+
].filter((note) => note !== "");
|
|
455
|
+
console.log(notes.length === 0 ? "no quickstudy-labeled resources found — already clean" : `removed ${notes.join(", ")}`);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async function commandImages(args: string[]): Promise<void> {
|
|
459
|
+
const [subcommand, ...rest] = args;
|
|
460
|
+
if (subcommand === "--help" || subcommand === "-h") {
|
|
461
|
+
console.log(IMAGES_HELP);
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
if (subcommand !== "build") {
|
|
465
|
+
fail(`images: unknown subcommand "${subcommand ?? ""}" (expected: build)\n\n${USAGE}`);
|
|
466
|
+
}
|
|
467
|
+
if (helpRequested(rest)) {
|
|
468
|
+
console.log(IMAGES_BUILD_HELP);
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
let values: { pull: boolean; "images-dir"?: string };
|
|
472
|
+
try {
|
|
473
|
+
({ values } = parseArgs({
|
|
474
|
+
args: rest,
|
|
475
|
+
options: { pull: { type: "boolean", default: false }, "images-dir": { type: "string" } },
|
|
476
|
+
}));
|
|
477
|
+
} catch (err) {
|
|
478
|
+
fail(`images build: ${err instanceof Error ? err.message : String(err)}\n\n${IMAGES_BUILD_HELP}`);
|
|
479
|
+
}
|
|
480
|
+
if (!(await dockerDaemonReachable())) {
|
|
481
|
+
fail(
|
|
482
|
+
"images build: no Docker daemon reachable. " +
|
|
483
|
+
"Start your Docker runtime (Docker Desktop, OrbStack, Colima, …), confirm `docker ps` works, then re-run.",
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
const imagesRoot = values["images-dir"];
|
|
487
|
+
try {
|
|
488
|
+
const exitCode = await buildAgentRuntimeImage({ pull: values.pull, imagesRoot });
|
|
489
|
+
if (exitCode !== 0) fail(`images build: docker build exited ${exitCode}`);
|
|
490
|
+
const proxyExitCode = await buildEgressProxyImage({ pull: values.pull, imagesRoot });
|
|
491
|
+
if (proxyExitCode !== 0) fail(`images build: egress-proxy docker build exited ${proxyExitCode}`);
|
|
492
|
+
const mcpExitCode = await buildMcpProxyImage({ pull: values.pull, imagesRoot });
|
|
493
|
+
if (mcpExitCode !== 0) fail(`images build: mcp-proxy docker build exited ${mcpExitCode}`);
|
|
494
|
+
} catch (err) {
|
|
495
|
+
// Missing build context (e.g. running the binary outside a checkout).
|
|
496
|
+
fail(`images build: ${err instanceof Error ? err.message : String(err)}`);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/** The prebuilt UI bundle shipped next to the CLI sources (source checkout). */
|
|
501
|
+
function uiBundleDir(): string {
|
|
502
|
+
return join(import.meta.dir, "../ui/dist");
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Resolve a directory containing the UI shell (`index.html`).
|
|
507
|
+
*
|
|
508
|
+
* Source checkout: the on-disk `ui/dist` produced by `bun run ui:build`.
|
|
509
|
+
* Compiled binary: `ui/dist` isn't on disk, so materialize the embedded shell
|
|
510
|
+
* (see `ui-bundle.generated.ts`) into a temp dir and return that.
|
|
511
|
+
*/
|
|
512
|
+
function mustUiBundleDir(): string {
|
|
513
|
+
const bundleDir = uiBundleDir();
|
|
514
|
+
if (existsSync(join(bundleDir, "index.html"))) return bundleDir;
|
|
515
|
+
|
|
516
|
+
if (UI_INDEX_HTML_BASE64.length > 0) {
|
|
517
|
+
const dir = mkdtempSync(join(tmpdir(), "quickstudy-ui-"));
|
|
518
|
+
writeFileSync(join(dir, "index.html"), Buffer.from(UI_INDEX_HTML_BASE64, "base64"));
|
|
519
|
+
return dir;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
fail(
|
|
523
|
+
`the UI bundle is not built (expected ${bundleDir}).\n` +
|
|
524
|
+
"Build it once with `bun run ui:build`, then re-run.",
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/** Positional run ids + comma-separated --runs, de-duplicated in order. */
|
|
529
|
+
function collectRunIds(positionals: string[], runsFlag: string | undefined): string[] {
|
|
530
|
+
const ids = [...positionals, ...(runsFlag ?? "").split(",")].map((id) => id.trim()).filter((id) => id !== "");
|
|
531
|
+
return [...new Set(ids)];
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
async function commandExport(args: string[]): Promise<void> {
|
|
535
|
+
let values: { runs?: string; out: string; db: string; results: string; mode: string; "expected-trials"?: string };
|
|
536
|
+
let positionals: string[];
|
|
537
|
+
try {
|
|
538
|
+
({ values, positionals } = parseArgs({
|
|
539
|
+
args,
|
|
540
|
+
allowPositionals: true,
|
|
541
|
+
options: {
|
|
542
|
+
runs: { type: "string" },
|
|
543
|
+
out: { type: "string", default: "site" },
|
|
544
|
+
mode: { type: "string", default: "offline" },
|
|
545
|
+
"expected-trials": { type: "string" },
|
|
546
|
+
db: { type: "string", default: "results.db" },
|
|
547
|
+
results: { type: "string", default: "results" },
|
|
548
|
+
},
|
|
549
|
+
}));
|
|
550
|
+
} catch (err) {
|
|
551
|
+
fail(`export: ${err instanceof Error ? err.message : String(err)}\n\n${USAGE}`);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const bundleDir = mustUiBundleDir();
|
|
555
|
+
const store = new ResultsStore(values.db);
|
|
556
|
+
try {
|
|
557
|
+
// `--runs all`: every run in the DB. Explicit positionals are redundant
|
|
558
|
+
// but harmless (collectRunIds de-duplicates).
|
|
559
|
+
const runIds =
|
|
560
|
+
values.runs === "all"
|
|
561
|
+
? collectRunIds([...positionals, ...store.listRuns().map((run) => run.id)], undefined)
|
|
562
|
+
: collectRunIds(positionals, values.runs);
|
|
563
|
+
if (runIds.length === 0) {
|
|
564
|
+
const known = store
|
|
565
|
+
.listRuns()
|
|
566
|
+
.slice(0, 5)
|
|
567
|
+
.map((run) => ` ${run.id} (started ${new Date(run.startedAt).toISOString()})`);
|
|
568
|
+
fail(`export: missing <run-id>${known.length > 0 ? `; most recent runs:\n${known.join("\n")}` : " (no runs in this database yet)"}`);
|
|
569
|
+
}
|
|
570
|
+
const summary = await exportSite({
|
|
571
|
+
store,
|
|
572
|
+
resultsDir: values.results,
|
|
573
|
+
runIds,
|
|
574
|
+
outDir: values.out,
|
|
575
|
+
mode: values.mode === "hosted" || values.mode === "offline" ? values.mode : fail("export --mode must be hosted or offline"),
|
|
576
|
+
...(values["expected-trials"] ? { expectedTrials: parsePositiveInt(values["expected-trials"], "--expected-trials") } : {}),
|
|
577
|
+
bundleDir,
|
|
578
|
+
});
|
|
579
|
+
console.log(
|
|
580
|
+
`exported ${summary.runs} run(s), ${summary.attempts} attempt(s) -> ${summary.outDir}` +
|
|
581
|
+
(summary.missingAssets > 0 ? ` (${summary.missingAssets} missing artifact(s) skipped)` : ""),
|
|
582
|
+
);
|
|
583
|
+
console.log(values.mode === "hosted"
|
|
584
|
+
? `serve ${summary.outDir} with a static server to open this hosted export`
|
|
585
|
+
: `open ${join(summary.outDir, "index.html")} — or host the directory anywhere static`);
|
|
586
|
+
} finally {
|
|
587
|
+
store.close();
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
interface UiFlags {
|
|
592
|
+
runs?: string;
|
|
593
|
+
port: string;
|
|
594
|
+
watch: boolean;
|
|
595
|
+
db: string;
|
|
596
|
+
results: string;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
async function commandUi(args: string[]): Promise<void> {
|
|
600
|
+
let values: UiFlags;
|
|
601
|
+
try {
|
|
602
|
+
({ values } = parseArgs({
|
|
603
|
+
args,
|
|
604
|
+
options: {
|
|
605
|
+
runs: { type: "string" },
|
|
606
|
+
port: { type: "string", default: "4173" },
|
|
607
|
+
watch: { type: "boolean", default: false },
|
|
608
|
+
db: { type: "string", default: "results.db" },
|
|
609
|
+
results: { type: "string", default: "results" },
|
|
610
|
+
},
|
|
611
|
+
}));
|
|
612
|
+
} catch (err) {
|
|
613
|
+
fail(`ui: ${err instanceof Error ? err.message : String(err)}\n\n${USAGE}`);
|
|
614
|
+
}
|
|
615
|
+
const port = Number(values.port);
|
|
616
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) fail(`ui: --port must be 0-65535, got "${values.port}"`);
|
|
617
|
+
|
|
618
|
+
const bundleDir = mustUiBundleDir();
|
|
619
|
+
const requested = collectRunIds([], values.runs);
|
|
620
|
+
const outDir = mkdtempSync(join(tmpdir(), "quickstudy-ui-"));
|
|
621
|
+
|
|
622
|
+
const doExport = async (): Promise<void> => {
|
|
623
|
+
const store = new ResultsStore(values.db);
|
|
624
|
+
try {
|
|
625
|
+
const runIds = requested.length > 0 ? requested : store.listRuns().map((run) => run.id);
|
|
626
|
+
if (runIds.length === 0) {
|
|
627
|
+
console.warn(
|
|
628
|
+
"quickstudy: this results database has no runs yet — serving the empty state.\n" +
|
|
629
|
+
" Get started: quickstudy run --eval <id> --experiment <id>",
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
const summary = await exportSite({ store, resultsDir: values.results, runIds, outDir, bundleDir, mode: "hosted" });
|
|
633
|
+
console.log(`exported ${summary.runs} run(s), ${summary.attempts} attempt(s) -> ${outDir}`);
|
|
634
|
+
} finally {
|
|
635
|
+
store.close();
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
await doExport();
|
|
640
|
+
const server = serveSite(outDir, port);
|
|
641
|
+
console.log(`quickstudy ui: ${server.url} (Ctrl-C to stop)`);
|
|
642
|
+
|
|
643
|
+
if (values.watch) {
|
|
644
|
+
// WAL mode writes land in <db>-wal, so watch the directory and filter by
|
|
645
|
+
// the db basename prefix; debounce bursts into one re-export.
|
|
646
|
+
const dbDir = dirname(resolve(values.db));
|
|
647
|
+
const dbName = basename(values.db);
|
|
648
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
649
|
+
watch(dbDir, (_event, fileName) => {
|
|
650
|
+
if (fileName === null || !fileName.startsWith(dbName)) return;
|
|
651
|
+
if (timer !== null) clearTimeout(timer);
|
|
652
|
+
timer = setTimeout(() => {
|
|
653
|
+
doExport().catch((err: unknown) => {
|
|
654
|
+
console.warn(`quickstudy ui: re-export failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
655
|
+
});
|
|
656
|
+
}, 400);
|
|
657
|
+
});
|
|
658
|
+
console.log(`watching ${values.db} — the site re-exports on change`);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// Serve until interrupted.
|
|
662
|
+
await new Promise(() => {});
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
interface ReportFlags {
|
|
666
|
+
json: boolean;
|
|
667
|
+
strict: boolean;
|
|
668
|
+
latest: boolean;
|
|
669
|
+
db: string;
|
|
670
|
+
results: string;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
interface RunFlags {
|
|
674
|
+
resume?: string;
|
|
675
|
+
concurrency?: string;
|
|
676
|
+
seed?: string;
|
|
677
|
+
"retry-unfinished": boolean;
|
|
678
|
+
"provision-ms"?: string;
|
|
679
|
+
"agent-ms"?: string;
|
|
680
|
+
"score-ms"?: string;
|
|
681
|
+
"export-ms"?: string;
|
|
682
|
+
"cleanup-ms"?: string;
|
|
683
|
+
eval?: string[];
|
|
684
|
+
experiment?: string[];
|
|
685
|
+
"evals-root": string;
|
|
686
|
+
"experiments-root": string;
|
|
687
|
+
framework?: string[];
|
|
688
|
+
suite?: string[];
|
|
689
|
+
trials: string;
|
|
690
|
+
"egress-proxy": boolean;
|
|
691
|
+
db: string;
|
|
692
|
+
results: string;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function parseRunFlags(args: string[]): RunFlags {
|
|
696
|
+
const { values } = parseArgs({
|
|
697
|
+
args,
|
|
698
|
+
options: {
|
|
699
|
+
resume: { type: "string" },
|
|
700
|
+
concurrency: { type: "string" },
|
|
701
|
+
seed: { type: "string" },
|
|
702
|
+
"retry-unfinished": { type: "boolean", default: false },
|
|
703
|
+
"provision-ms": { type: "string" }, "agent-ms": { type: "string" },
|
|
704
|
+
"score-ms": { type: "string" }, "export-ms": { type: "string" }, "cleanup-ms": { type: "string" },
|
|
705
|
+
eval: { type: "string", multiple: true },
|
|
706
|
+
experiment: { type: "string", multiple: true },
|
|
707
|
+
"evals-root": { type: "string", default: "evals" },
|
|
708
|
+
"experiments-root": { type: "string", default: "experiments" },
|
|
709
|
+
framework: { type: "string", multiple: true },
|
|
710
|
+
suite: { type: "string", multiple: true },
|
|
711
|
+
trials: { type: "string", default: "3" },
|
|
712
|
+
// Off by default until burned in; it becomes the default later, at
|
|
713
|
+
// which point --no-egress-proxy preserves the direct-network mode.
|
|
714
|
+
"egress-proxy": { type: "boolean", default: false },
|
|
715
|
+
db: { type: "string", default: "results.db" },
|
|
716
|
+
results: { type: "string", default: "results" },
|
|
717
|
+
},
|
|
718
|
+
});
|
|
719
|
+
return values;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
async function main(): Promise<void> {
|
|
723
|
+
const [command, ...rest] = process.argv.slice(2);
|
|
724
|
+
try {
|
|
725
|
+
if (command !== undefined && command !== "images" && helpRequested(rest)) {
|
|
726
|
+
const help = COMMAND_HELP[command];
|
|
727
|
+
if (help !== undefined) {
|
|
728
|
+
console.log(help);
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
switch (command) {
|
|
733
|
+
case "validate":
|
|
734
|
+
await commandValidate(rest);
|
|
735
|
+
return;
|
|
736
|
+
case "run":
|
|
737
|
+
await commandRun(rest);
|
|
738
|
+
return;
|
|
739
|
+
case "report":
|
|
740
|
+
await commandReport(rest);
|
|
741
|
+
return;
|
|
742
|
+
case "diagnose":
|
|
743
|
+
await commandDiagnose(rest);
|
|
744
|
+
return;
|
|
745
|
+
case "clean":
|
|
746
|
+
await commandClean();
|
|
747
|
+
return;
|
|
748
|
+
case "images":
|
|
749
|
+
await commandImages(rest);
|
|
750
|
+
return;
|
|
751
|
+
case "export":
|
|
752
|
+
await commandExport(rest);
|
|
753
|
+
return;
|
|
754
|
+
case "ui":
|
|
755
|
+
await commandUi(rest);
|
|
756
|
+
return;
|
|
757
|
+
case "version":
|
|
758
|
+
case "--version":
|
|
759
|
+
console.log(EMBEDDED_BUILD_INFO.version);
|
|
760
|
+
return;
|
|
761
|
+
case undefined:
|
|
762
|
+
case "help":
|
|
763
|
+
case "--help":
|
|
764
|
+
case "-h":
|
|
765
|
+
console.log(USAGE);
|
|
766
|
+
return;
|
|
767
|
+
default:
|
|
768
|
+
fail(`unknown command "${command}"\n\n${USAGE}`);
|
|
769
|
+
}
|
|
770
|
+
} catch (err) {
|
|
771
|
+
if (
|
|
772
|
+
err instanceof EvalLoadError ||
|
|
773
|
+
err instanceof ExperimentLoadError ||
|
|
774
|
+
err instanceof MissingProviderKeyError ||
|
|
775
|
+
err instanceof InvalidProviderKeyError ||
|
|
776
|
+
err instanceof ReportError ||
|
|
777
|
+
err instanceof ExportError ||
|
|
778
|
+
err instanceof DiagnoseError ||
|
|
779
|
+
err instanceof ModelOutputError
|
|
780
|
+
) {
|
|
781
|
+
fail(err.message);
|
|
782
|
+
}
|
|
783
|
+
throw err;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
await main();
|