thunderjar 0.1.3-preview-e09ffa1 → 0.1.3-preview-79a2982
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/dist/cli.js +267 -62
- package/package.json +1 -1
- package/src/api/declarations.ts +3 -0
package/dist/cli.js
CHANGED
|
@@ -2047,7 +2047,7 @@ var program = new Command;
|
|
|
2047
2047
|
// package.json
|
|
2048
2048
|
var package_default = {
|
|
2049
2049
|
name: "thunderjar",
|
|
2050
|
-
version: "0.1.3-preview-
|
|
2050
|
+
version: "0.1.3-preview-79a2982",
|
|
2051
2051
|
type: "module",
|
|
2052
2052
|
description: "Thunderjar",
|
|
2053
2053
|
license: "MIT",
|
|
@@ -3451,9 +3451,161 @@ function createGenerateCommand() {
|
|
|
3451
3451
|
}));
|
|
3452
3452
|
}
|
|
3453
3453
|
|
|
3454
|
-
// src/cli/commands/init/
|
|
3454
|
+
// src/cli/commands/init/init.ts
|
|
3455
|
+
var {$: $5 } = globalThis.Bun;
|
|
3456
|
+
|
|
3457
|
+
// src/cli/commands/init/starterHarnesses.ts
|
|
3455
3458
|
var declaration = (fn, body) => `import { ${fn} } from "thunderjar";
|
|
3456
3459
|
|
|
3460
|
+
export default ${fn}(${body});
|
|
3461
|
+
`;
|
|
3462
|
+
var models = (comment, entries) => `import { declareHarnessModels } from "thunderjar";
|
|
3463
|
+
|
|
3464
|
+
${comment}
|
|
3465
|
+
export default declareHarnessModels({
|
|
3466
|
+
${entries.map(([name, id]) => ` ${JSON.stringify(name)}: ${JSON.stringify(id)},`).join(`
|
|
3467
|
+
`)}
|
|
3468
|
+
});
|
|
3469
|
+
`;
|
|
3470
|
+
var versions = (version) => declaration("declareHarnessVersions", `{
|
|
3471
|
+
${JSON.stringify(version)}: { version: ${JSON.stringify(version)} },
|
|
3472
|
+
}`);
|
|
3473
|
+
var claudeCodeTokenCosts = `import { claudeCodeTokenCosts, declareTokenCostCollector } from "thunderjar";
|
|
3474
|
+
|
|
3475
|
+
export default declareTokenCostCollector((raw) => claudeCodeTokenCosts(raw));
|
|
3476
|
+
`;
|
|
3477
|
+
var stubTokenCosts = (agent) => `import { declareTokenCostCollector } from "thunderjar";
|
|
3478
|
+
|
|
3479
|
+
// Thunderjar has no reader for ${agent}'s output yet. \`raw\` is what the harness's \`cli\` wrote to
|
|
3480
|
+
// its result path. To collect token costs, read them from it:
|
|
3481
|
+
//
|
|
3482
|
+
// return {
|
|
3483
|
+
// outcome: "collected",
|
|
3484
|
+
// models: [{ model: ctx.modelId, inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens }],
|
|
3485
|
+
// };
|
|
3486
|
+
export default declareTokenCostCollector((_raw, _ctx) => ({ outcome: "unavailable", reason: "notReported" }));
|
|
3487
|
+
`;
|
|
3488
|
+
var installAsRoot = (install) => `\`RUN ${install}
|
|
3489
|
+
RUN mkdir -p /thunderjar && chown -R node:node /workspace /thunderjar
|
|
3490
|
+
USER node\``;
|
|
3491
|
+
var stubReply = ` // Thunderjar has no reader for this agent's reply yet. To measure it, return the agent's final
|
|
3492
|
+
// reply from what \`cli\` wrote to its result path:
|
|
3493
|
+
// collectReply: (raw) => \u2026,`;
|
|
3494
|
+
var claudeCode = `import { claudeCodeReply, declareHarness } from "thunderjar";
|
|
3495
|
+
|
|
3496
|
+
// Claude Code, installed as root, then run as the image's non-root \`node\` user: it won't skip its
|
|
3497
|
+
// permission prompts as root, and nobody is there to answer them.
|
|
3498
|
+
export default declareHarness({
|
|
3499
|
+
applyParameter: (_config, ctx) => \`RUN npm install -g @anthropic-ai/claude-code@\${ctx.version}
|
|
3500
|
+
RUN mkdir -p /thunderjar && chown -R node:node /workspace /thunderjar
|
|
3501
|
+
USER node\`,
|
|
3502
|
+
// The prompt is read from its file, so quotes and newlines reach the agent intact. Turn 0 starts
|
|
3503
|
+
// the session, and any later turn resumes it.
|
|
3504
|
+
cli: (ctx) =>
|
|
3505
|
+
\`claude -p "$(cat \${ctx.promptPath})" --model \${ctx.modelId} --output-format json --dangerously-skip-permissions\` +
|
|
3506
|
+
\` \${ctx.turn === 0 ? "--session-id" : "--resume"} \${ctx.sessionId} > \${ctx.resultPath}\`,
|
|
3507
|
+
collectReply: claudeCodeReply,
|
|
3508
|
+
requiredEnv: ["ANTHROPIC_API_KEY"],
|
|
3509
|
+
});
|
|
3510
|
+
`;
|
|
3511
|
+
var copilot = `import { declareHarness } from "thunderjar";
|
|
3512
|
+
|
|
3513
|
+
// GitHub Copilot CLI. COPILOT_GITHUB_TOKEN is a GitHub token with Copilot access: a fine-grained
|
|
3514
|
+
// personal access token with the "Copilot Requests" permission.
|
|
3515
|
+
export default declareHarness({
|
|
3516
|
+
applyParameter: (_config, ctx) => ${installAsRoot("npm install -g @github/copilot@${ctx.version}")},
|
|
3517
|
+
// The prompt is read from its file, so quotes and newlines reach the agent intact. Any turn after
|
|
3518
|
+
// the first continues the session.
|
|
3519
|
+
cli: (ctx) =>
|
|
3520
|
+
\`copilot -p "$(cat \${ctx.promptPath})" --model \${ctx.modelId} --allow-all-tools --no-color\` +
|
|
3521
|
+
\`\${ctx.turn === 0 ? "" : " --continue"} > \${ctx.resultPath}\`,
|
|
3522
|
+
${stubReply}
|
|
3523
|
+
requiredEnv: ["COPILOT_GITHUB_TOKEN"],
|
|
3524
|
+
});
|
|
3525
|
+
`;
|
|
3526
|
+
var codex = `import { declareHarness } from "thunderjar";
|
|
3527
|
+
|
|
3528
|
+
// OpenAI's Codex CLI, without its sandbox or approvals: the container is the sandbox.
|
|
3529
|
+
export default declareHarness({
|
|
3530
|
+
applyParameter: (_config, ctx) => ${installAsRoot("npm install -g @openai/codex@${ctx.version}")},
|
|
3531
|
+
// The prompt is read from its file, so quotes and newlines reach the agent intact. Any turn after
|
|
3532
|
+
// the first resumes the session.
|
|
3533
|
+
cli: (ctx) =>
|
|
3534
|
+
\`codex exec\${ctx.turn === 0 ? "" : " resume --last"} --model \${ctx.modelId} --json --dangerously-bypass-approvals-and-sandbox\` +
|
|
3535
|
+
\` "$(cat \${ctx.promptPath})" > \${ctx.resultPath}\`,
|
|
3536
|
+
${stubReply}
|
|
3537
|
+
requiredEnv: ["OPENAI_API_KEY"],
|
|
3538
|
+
});
|
|
3539
|
+
`;
|
|
3540
|
+
var opencode = `import { declareHarness } from "thunderjar";
|
|
3541
|
+
|
|
3542
|
+
// OpenCode, on OpenCode Zen. OPENCODE_API_KEY is an OpenCode Zen key.
|
|
3543
|
+
export default declareHarness({
|
|
3544
|
+
applyParameter: (_config, ctx) => ${installAsRoot("npm install -g opencode-ai@${ctx.version}")},
|
|
3545
|
+
// The prompt is read from its file, so quotes and newlines reach the agent intact. Any turn after
|
|
3546
|
+
// the first continues the session.
|
|
3547
|
+
cli: (ctx) =>
|
|
3548
|
+
\`opencode run --model \${ctx.modelId} --format json\${ctx.turn === 0 ? "" : " --continue"}\` +
|
|
3549
|
+
\` "$(cat \${ctx.promptPath})" > \${ctx.resultPath}\`,
|
|
3550
|
+
${stubReply}
|
|
3551
|
+
requiredEnv: ["OPENCODE_API_KEY"],
|
|
3552
|
+
});
|
|
3553
|
+
`;
|
|
3554
|
+
var CHECK = (where) => `// Models come and go: check ${where} for the current IDs.`;
|
|
3555
|
+
var STARTER_HARNESSES = {
|
|
3556
|
+
"claude-code": {
|
|
3557
|
+
"index.ts": claudeCode,
|
|
3558
|
+
"models.ts": models(CHECK("https://docs.claude.com/en/docs/about-claude/models"), [
|
|
3559
|
+
["haiku-4-5", "claude-haiku-4-5-20251001"],
|
|
3560
|
+
["haiku-5-5", "claude-haiku-5-5"],
|
|
3561
|
+
["sonnet-5-5", "claude-sonnet-5-5"],
|
|
3562
|
+
["opus-5-5", "claude-opus-5-5"]
|
|
3563
|
+
]),
|
|
3564
|
+
"versions.ts": versions("2.1.283"),
|
|
3565
|
+
"collectTokenCosts.ts": claudeCodeTokenCosts
|
|
3566
|
+
},
|
|
3567
|
+
copilot: {
|
|
3568
|
+
"index.ts": copilot,
|
|
3569
|
+
"models.ts": models(`${CHECK("`copilot --help`, or GitHub's supported models page,")}
|
|
3570
|
+
// Which you can use depends on your Copilot plan and your organization's policy.`, [
|
|
3571
|
+
["gpt-6-1-sol", "gpt-6.1-sol"],
|
|
3572
|
+
["gpt-5-6-luna", "gpt-5.6-luna"],
|
|
3573
|
+
["gpt-5-5", "gpt-5.5"],
|
|
3574
|
+
["haiku-5-5", "claude-haiku-5.5"],
|
|
3575
|
+
["sonnet-5-5", "claude-sonnet-5.5"],
|
|
3576
|
+
["opus-5-5", "claude-opus-5.5"]
|
|
3577
|
+
]),
|
|
3578
|
+
"versions.ts": versions("1.0.105"),
|
|
3579
|
+
"collectTokenCosts.ts": stubTokenCosts("Copilot CLI")
|
|
3580
|
+
},
|
|
3581
|
+
codex: {
|
|
3582
|
+
"index.ts": codex,
|
|
3583
|
+
"models.ts": models(CHECK("https://developers.openai.com/codex/models"), [
|
|
3584
|
+
["gpt-6-1-sol", "gpt-6.1-sol"],
|
|
3585
|
+
["gpt-6-astra", "gpt-6-astra"],
|
|
3586
|
+
["gpt-6-luna", "gpt-6-luna"],
|
|
3587
|
+
["gpt-5-6-luna", "gpt-5.6-luna"],
|
|
3588
|
+
["gpt-5-5", "gpt-5.5"]
|
|
3589
|
+
]),
|
|
3590
|
+
"versions.ts": versions("0.166.0"),
|
|
3591
|
+
"collectTokenCosts.ts": stubTokenCosts("Codex")
|
|
3592
|
+
},
|
|
3593
|
+
opencode: {
|
|
3594
|
+
"index.ts": opencode,
|
|
3595
|
+
"models.ts": models(CHECK("https://opencode.ai/zen/v1/models"), [
|
|
3596
|
+
["opus-5", "opencode/claude-opus-5"],
|
|
3597
|
+
["gpt-5-6-luna", "opencode/gpt-5.6-luna"],
|
|
3598
|
+
["gpt-5-5", "opencode/gpt-5.5"],
|
|
3599
|
+
["deepseek-v4-pro", "opencode/deepseek-v4-pro"]
|
|
3600
|
+
]),
|
|
3601
|
+
"versions.ts": versions("1.19.5"),
|
|
3602
|
+
"collectTokenCosts.ts": stubTokenCosts("OpenCode")
|
|
3603
|
+
}
|
|
3604
|
+
};
|
|
3605
|
+
|
|
3606
|
+
// src/cli/commands/init/scaffold.ts
|
|
3607
|
+
var declaration2 = (fn, body) => `import { ${fn} } from "thunderjar";
|
|
3608
|
+
|
|
3457
3609
|
export default ${fn}(${body});
|
|
3458
3610
|
`;
|
|
3459
3611
|
var json = (value) => JSON.stringify(value, null, 2) + `
|
|
@@ -3478,7 +3630,7 @@ var tsconfig = json({
|
|
|
3478
3630
|
include: ["**/*.ts"],
|
|
3479
3631
|
exclude: ["node_modules", ".data", "**/*.template.ts"]
|
|
3480
3632
|
});
|
|
3481
|
-
var testCase =
|
|
3633
|
+
var testCase = declaration2("declareTestCase", `{
|
|
3482
3634
|
baseImage: ["node20"],
|
|
3483
3635
|
codeState: ["baseline"],
|
|
3484
3636
|
promptSet: ["snerk"],
|
|
@@ -3487,47 +3639,93 @@ var testCase = declaration("declareTestCase", `{
|
|
|
3487
3639
|
initialPrompt: ["plain"],
|
|
3488
3640
|
iterations: 1,
|
|
3489
3641
|
}`);
|
|
3490
|
-
var baseImage =
|
|
3491
|
-
applyParameter: () => \`FROM node:20-bookworm\`,
|
|
3492
|
-
}`);
|
|
3493
|
-
var harness = `import { claudeCodeReply, declareHarness } from "thunderjar";
|
|
3642
|
+
var baseImage = `import { declareBaseImage } from "thunderjar";
|
|
3494
3643
|
|
|
3495
|
-
//
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
RUN mkdir -p /thunderjar && chown -R node:node /workspace /thunderjar
|
|
3500
|
-
USER node\`,
|
|
3501
|
-
// The prompt is read from its file, so quotes and newlines reach the agent intact. Turn 0 starts
|
|
3502
|
-
// the session, and any later turn resumes it.
|
|
3503
|
-
cli: (ctx) =>
|
|
3504
|
-
\`claude -p "$(cat \${ctx.promptPath})" --model \${ctx.modelId} --output-format json --dangerously-skip-permissions\` +
|
|
3505
|
-
\` \${ctx.turn === 0 ? "--session-id" : "--resume"} \${ctx.sessionId} > \${ctx.resultPath}\`,
|
|
3506
|
-
collectReply: claudeCodeReply,
|
|
3507
|
-
requiredEnv: ["ANTHROPIC_API_KEY"],
|
|
3644
|
+
// With bun, which the templateTest measurement runs its tests with.
|
|
3645
|
+
export default declareBaseImage({
|
|
3646
|
+
applyParameter: () => \`FROM node:20-bookworm
|
|
3647
|
+
RUN npm install --global bun@1.3.14\`,
|
|
3508
3648
|
});
|
|
3509
3649
|
`;
|
|
3510
|
-
var
|
|
3511
|
-
"haiku-4-5": "claude-haiku-4-5-20251001",
|
|
3512
|
-
}`);
|
|
3513
|
-
var harnessVersions = declaration("declareHarnessVersions", `{
|
|
3514
|
-
"2.1.283": { version: "2.1.283" },
|
|
3515
|
-
}`);
|
|
3516
|
-
var collectTokenCosts = `import { claudeCodeTokenCosts, declareTokenCostCollector } from "thunderjar";
|
|
3517
|
-
|
|
3518
|
-
export default declareTokenCostCollector((raw) => claudeCodeTokenCosts(raw));
|
|
3519
|
-
`;
|
|
3520
|
-
var initialPrompt = declaration("declareInitialPrompt", `{
|
|
3650
|
+
var initialPrompt = declaration2("declareInitialPrompt", `{
|
|
3521
3651
|
prompt: "Create a file isPrime.ts exporting a function that tests for primality.",
|
|
3522
3652
|
timeoutSeconds: 600,
|
|
3523
3653
|
}`);
|
|
3524
|
-
var measurement =
|
|
3654
|
+
var measurement = declaration2("declareMeasurement", `{
|
|
3525
3655
|
instrument: "fileCreated",
|
|
3526
3656
|
config: { glob: "**/isPrime.ts" },
|
|
3527
3657
|
aggregate: ["rate"],
|
|
3528
3658
|
}`);
|
|
3659
|
+
var templateTestMeasurement = declaration2("declareMeasurement", `{
|
|
3660
|
+
instrument: "templateTest",
|
|
3661
|
+
config: { subject: "**/isPrime.ts", template: "./isPrime.test.template.ts" },
|
|
3662
|
+
aggregate: ["sum", "mean"],
|
|
3663
|
+
}`);
|
|
3664
|
+
var isPrimeTestTemplate = `import { {{defaultExport}} } from "{{subjectPath}}";
|
|
3665
|
+
|
|
3666
|
+
test("2 is prime", () => expect({{defaultExport}}(2)).toBe(true));
|
|
3667
|
+
test("17 is prime", () => expect({{defaultExport}}(17)).toBe(true));
|
|
3668
|
+
test("15 is not prime", () => expect({{defaultExport}}(15)).toBe(false));
|
|
3669
|
+
test("0 is not prime", () => expect({{defaultExport}}(0)).toBe(false));
|
|
3670
|
+
test("1 is not prime", () => expect({{defaultExport}}(1)).toBe(false));
|
|
3671
|
+
`;
|
|
3672
|
+
var tsIgnoreCountMeasurement = declaration2("declareMeasurement", `{
|
|
3673
|
+
instrument: "grep",
|
|
3674
|
+
config: { glob: "**/*.ts", pattern: "@ts-ignore" },
|
|
3675
|
+
aggregate: ["mean", "max"],
|
|
3676
|
+
}`);
|
|
3677
|
+
var codeState = (head) => head ? `import { checkoutCommit, declareCodeState } from "thunderjar";
|
|
3678
|
+
|
|
3679
|
+
// The project's repository at a commit, checked out into /workspace. The commit must be in the
|
|
3680
|
+
// repository: commit it, or Thunderjar can't build it.
|
|
3681
|
+
export default declareCodeState({
|
|
3682
|
+
config: { commit: "${head}" },
|
|
3683
|
+
applyParameter: (config, ctx) => checkoutCommit(ctx, config.commit),
|
|
3684
|
+
});
|
|
3685
|
+
` : `import { declareCodeState } from "thunderjar";
|
|
3686
|
+
|
|
3687
|
+
// An empty /workspace. The project wasn't a git repository when \`thunderjar init\` ran. Once it is,
|
|
3688
|
+
// check a commit of it out into /workspace:
|
|
3689
|
+
//
|
|
3690
|
+
// import { checkoutCommit, declareCodeState } from "thunderjar";
|
|
3691
|
+
//
|
|
3692
|
+
// export default declareCodeState({
|
|
3693
|
+
// config: { commit: "<a commit hash>" },
|
|
3694
|
+
// applyParameter: (config, ctx) => checkoutCommit(ctx, config.commit),
|
|
3695
|
+
// });
|
|
3696
|
+
export default declareCodeState({});
|
|
3697
|
+
`;
|
|
3698
|
+
var snerk = (head) => `import { declarePromptSet } from "thunderjar";
|
|
3699
|
+
|
|
3700
|
+
// Files from a commit of the project's repository, such as its AGENTS.md or skills, copied into
|
|
3701
|
+
// /workspace at the same paths, like \`git checkout <commit> -- <paths>\`. They're left uncommitted.
|
|
3702
|
+
// Each path is from the repository's root, e.g. "AGENTS.md" or ".agents/skills/review/SKILL.md".
|
|
3703
|
+
export default declarePromptSet<{ commit: string; paths: string[] }>({
|
|
3704
|
+
config: {
|
|
3705
|
+
commit: "${head ?? "<a commit hash>"}",
|
|
3706
|
+
paths: [],
|
|
3707
|
+
},
|
|
3708
|
+
applyParameter: (config, ctx) =>
|
|
3709
|
+
config.paths.map((path) => \`COPY \${ctx.git.show(config.commit, path)} /workspace/\${path}\`).join("\\n"),
|
|
3710
|
+
});
|
|
3711
|
+
`;
|
|
3712
|
+
var houseStyle = `import { declarePromptSet } from "thunderjar";
|
|
3713
|
+
|
|
3714
|
+
// The files in this folder's files/, copied over /workspace and committed, so the agent's changes
|
|
3715
|
+
// can be told from them. \`git add -A\`, not \`git commit -a\`, which skips new files such as a new
|
|
3716
|
+
// skill. Use it with a code state that checks out a commit, so /workspace is a git repository.
|
|
3717
|
+
export default declarePromptSet({
|
|
3718
|
+
applyParameter: (_config, ctx) => \`COPY \${ctx.dir}/files/ /workspace/
|
|
3719
|
+
RUN cd /workspace && git add -A && git commit --quiet -m "Thunderjar: apply promptSet"\`,
|
|
3720
|
+
});
|
|
3721
|
+
`;
|
|
3722
|
+
var houseStyleAgentsMd = `# House style
|
|
3723
|
+
|
|
3724
|
+
- Write TypeScript, with strict types.
|
|
3725
|
+
- Add a test beside each new module.
|
|
3726
|
+
`;
|
|
3529
3727
|
var P = "test-case-parameters";
|
|
3530
|
-
function scaffold({ nestedPackage }) {
|
|
3728
|
+
function scaffold({ nestedPackage, head }) {
|
|
3531
3729
|
return {
|
|
3532
3730
|
...nestedPackage && {
|
|
3533
3731
|
"package.json": json({ name: "thunderjar-config", private: true, type: "module", dependencies: { thunderjar: "latest" } })
|
|
@@ -3539,14 +3737,16 @@ function scaffold({ nestedPackage }) {
|
|
|
3539
3737
|
"thunderjar.config.ts": config,
|
|
3540
3738
|
"test-cases/example.ts": testCase,
|
|
3541
3739
|
[`${P}/baseImages/node20/index.ts`]: baseImage,
|
|
3542
|
-
[`${P}/codeStates/baseline/index.ts`]:
|
|
3543
|
-
[`${P}/promptSets/snerk/index.ts`]:
|
|
3544
|
-
[`${P}/
|
|
3545
|
-
[`${P}/
|
|
3546
|
-
[`${P}/harnessModels
|
|
3547
|
-
[`${P}/harnessModels/claude-code/collectTokenCosts.ts`]: collectTokenCosts,
|
|
3740
|
+
[`${P}/codeStates/baseline/index.ts`]: codeState(head),
|
|
3741
|
+
[`${P}/promptSets/snerk/index.ts`]: snerk(head),
|
|
3742
|
+
[`${P}/promptSets/house-style/index.ts`]: houseStyle,
|
|
3743
|
+
[`${P}/promptSets/house-style/files/AGENTS.md`]: houseStyleAgentsMd,
|
|
3744
|
+
...Object.fromEntries(Object.entries(STARTER_HARNESSES).flatMap(([harness, files]) => Object.entries(files).map(([file, content]) => [`${P}/harnessModels/${harness}/${file}`, content]))),
|
|
3548
3745
|
[`${P}/tasks/is-prime/initial-prompts/plain.ts`]: initialPrompt,
|
|
3549
|
-
[`${P}/tasks/is-prime/measurements/isPrimeTsExists.ts`]: measurement
|
|
3746
|
+
[`${P}/tasks/is-prime/measurements/isPrimeTsExists.ts`]: measurement,
|
|
3747
|
+
[`${P}/tasks/is-prime/measurements/isPrimeTemplateTest.ts`]: templateTestMeasurement,
|
|
3748
|
+
[`${P}/tasks/is-prime/measurements/isPrime.test.template.ts`]: isPrimeTestTemplate,
|
|
3749
|
+
[`${P}/tasks/is-prime/measurements/tsIgnoreCount.ts`]: tsIgnoreCountMeasurement
|
|
3550
3750
|
};
|
|
3551
3751
|
}
|
|
3552
3752
|
|
|
@@ -3559,12 +3759,17 @@ async function isDirectory2(path2) {
|
|
|
3559
3759
|
return false;
|
|
3560
3760
|
}
|
|
3561
3761
|
}
|
|
3762
|
+
async function headCommit(dir) {
|
|
3763
|
+
const result = await $5`git rev-parse --verify --quiet HEAD`.cwd(dir).quiet().nothrow();
|
|
3764
|
+
return result.exitCode === 0 ? result.stdout.toString().trim() : undefined;
|
|
3765
|
+
}
|
|
3562
3766
|
async function init(projectDir) {
|
|
3563
3767
|
const root = `${projectDir}/${FOLDER2}`;
|
|
3564
3768
|
const createdFolder = !await isDirectory2(root);
|
|
3565
3769
|
const nestedPackage = !await Bun.file(`${projectDir}/package.json`).exists();
|
|
3770
|
+
const head = await headCommit(projectDir);
|
|
3566
3771
|
const result = { createdFolder, created: [], existing: [] };
|
|
3567
|
-
for (const [path2, content] of Object.entries(scaffold({ nestedPackage }))) {
|
|
3772
|
+
for (const [path2, content] of Object.entries(scaffold({ nestedPackage, head }))) {
|
|
3568
3773
|
const file = Bun.file(`${root}/${path2}`);
|
|
3569
3774
|
if (await file.exists()) {
|
|
3570
3775
|
result.existing.push(path2);
|
|
@@ -3576,7 +3781,7 @@ async function init(projectDir) {
|
|
|
3576
3781
|
return result;
|
|
3577
3782
|
}
|
|
3578
3783
|
function createInitCommand() {
|
|
3579
|
-
return new Command("init").description("Scaffold the .thunderjar/ configuration folder in the current directory, with an example test case. Existing files are left as they are.").action(async () => {
|
|
3784
|
+
return new Command("init").description("Scaffold the .thunderjar/ configuration folder in the current directory, with an example test case, and starter prompt sets and harnesses to copy. Existing files are left as they are.").action(async () => {
|
|
3580
3785
|
const { createdFolder, created, existing } = await init(process.cwd());
|
|
3581
3786
|
if (createdFolder)
|
|
3582
3787
|
console.log(`Created ${FOLDER2}/`);
|
|
@@ -3657,7 +3862,7 @@ var promptsOf = (initialPrompt2) => initialPrompt2.turns ?? [initialPrompt2.prom
|
|
|
3657
3862
|
async function loadHarness(root, task, permutation, problems) {
|
|
3658
3863
|
const { harness: name, version, model } = parsePair(permutation.harnessModel);
|
|
3659
3864
|
const dir = `${PARAMETERS}/harnessModels/${name}`;
|
|
3660
|
-
const [baseImage2,
|
|
3865
|
+
const [baseImage2, harness, collectTokenCosts, versions2, models2, initialPrompt2] = await Promise.all([
|
|
3661
3866
|
loadDeclaration(root, `${PARAMETERS}/baseImages/${permutation.baseImage}/index.ts`, "declareBaseImage", problems),
|
|
3662
3867
|
loadDeclaration(root, `${dir}/index.ts`, "declareHarness", problems),
|
|
3663
3868
|
loadDeclaration(root, `${dir}/collectTokenCosts.ts`, "declareTokenCostCollector", problems),
|
|
@@ -3665,16 +3870,16 @@ async function loadHarness(root, task, permutation, problems) {
|
|
|
3665
3870
|
loadDeclaration(root, `${dir}/models.ts`, "declareHarnessModels", problems),
|
|
3666
3871
|
loadDeclaration(root, `${PARAMETERS}/tasks/${task}/initial-prompts/${permutation.initialPrompt}.ts`, "declareInitialPrompt", problems)
|
|
3667
3872
|
]);
|
|
3668
|
-
if (!baseImage2 || !
|
|
3873
|
+
if (!baseImage2 || !harness || !collectTokenCosts || !versions2 || !models2 || !initialPrompt2)
|
|
3669
3874
|
return;
|
|
3670
|
-
const pinned =
|
|
3671
|
-
return { baseImage: baseImage2, harness
|
|
3875
|
+
const pinned = versions2[version].version;
|
|
3876
|
+
return { baseImage: baseImage2, harness, collectTokenCosts, version: pinned, modelId: models2[model], initialPrompt: initialPrompt2 };
|
|
3672
3877
|
}
|
|
3673
|
-
function planTurns({ harness
|
|
3878
|
+
function planTurns({ harness, version, modelId, initialPrompt: initialPrompt2 }, sessionId) {
|
|
3674
3879
|
const prompts = promptsOf(initialPrompt2);
|
|
3675
3880
|
return prompts.map((prompt, turn) => {
|
|
3676
3881
|
const paths = turnPaths(turn, prompts.length);
|
|
3677
|
-
const command =
|
|
3882
|
+
const command = harness.cli({ version, modelId, initialPrompt: prompt, ...paths, turn, sessionId });
|
|
3678
3883
|
return { prompt, ...paths, command };
|
|
3679
3884
|
});
|
|
3680
3885
|
}
|
|
@@ -3799,8 +4004,8 @@ var message = (error) => error instanceof Error ? error.message : String(error);
|
|
|
3799
4004
|
var recordable = (result) => result.outcome === "erroredWhileMeasuring" ? { outcome: "erroredWhileMeasuring", error: { message: message(result.error) } } : result;
|
|
3800
4005
|
async function measureContainerRun(measurements, open, now = () => new Date) {
|
|
3801
4006
|
const result = {};
|
|
3802
|
-
for (const { name, declaration:
|
|
3803
|
-
const instrument = instruments[
|
|
4007
|
+
for (const { name, declaration: declaration3, config: config2, contentHash } of measurements) {
|
|
4008
|
+
const instrument = instruments[declaration3.instrument];
|
|
3804
4009
|
const ctx = await open(instrument.usesContainer !== false);
|
|
3805
4010
|
let outcome;
|
|
3806
4011
|
try {
|
|
@@ -3810,7 +4015,7 @@ async function measureContainerRun(measurements, open, now = () => new Date) {
|
|
|
3810
4015
|
} finally {
|
|
3811
4016
|
await ctx.close();
|
|
3812
4017
|
}
|
|
3813
|
-
result[name] = { instrument:
|
|
4018
|
+
result[name] = { instrument: declaration3.instrument, contentHash, measuredAt: now().toISOString(), outcome };
|
|
3814
4019
|
}
|
|
3815
4020
|
return result;
|
|
3816
4021
|
}
|
|
@@ -3925,7 +4130,7 @@ function add(a, b) {
|
|
|
3925
4130
|
...costBasis !== undefined && { costBasis }
|
|
3926
4131
|
};
|
|
3927
4132
|
}
|
|
3928
|
-
function
|
|
4133
|
+
function collectTokenCosts(collect, harnessResults, ctx) {
|
|
3929
4134
|
const all = harnessResults.map((raw) => collectOne(collect, raw, ctx));
|
|
3930
4135
|
const collected = all.filter((costs) => costs.outcome === "collected");
|
|
3931
4136
|
if (collected.length === 0)
|
|
@@ -3943,7 +4148,7 @@ function collectTokenCosts2(collect, harnessResults, ctx) {
|
|
|
3943
4148
|
// src/execution/runExecution.ts
|
|
3944
4149
|
function containerRunRecord({ plan: plan2, name, testCase: testCase2 }, { number: permutationNumber, permutation }, iteration, execution, turns, tokenCosts) {
|
|
3945
4150
|
const last = turns.at(-1);
|
|
3946
|
-
const { harness
|
|
4151
|
+
const { harness, version, model } = parsePair(permutation.harnessModel);
|
|
3947
4152
|
return {
|
|
3948
4153
|
executionId: plan2.executionId,
|
|
3949
4154
|
testCase: name,
|
|
@@ -3954,7 +4159,7 @@ function containerRunRecord({ plan: plan2, name, testCase: testCase2 }, { number
|
|
|
3954
4159
|
postrunImage: iteration.postrunImage,
|
|
3955
4160
|
ranAt: new Date(execution.startedAt).toISOString(),
|
|
3956
4161
|
parameters: Object.fromEntries(AXES.map((axis) => [axis, { name: permutation[axis], parameterHash: permutation.parameterHashes[axis] }])),
|
|
3957
|
-
harnessModel: { harness
|
|
4162
|
+
harnessModel: { harness, version, model, modelId: permutation.modelId },
|
|
3958
4163
|
execution,
|
|
3959
4164
|
harnessResult: last.harnessResult,
|
|
3960
4165
|
reply: last.reply,
|
|
@@ -3996,7 +4201,7 @@ async function runExecution(run, store, hooks = {}) {
|
|
|
3996
4201
|
container
|
|
3997
4202
|
});
|
|
3998
4203
|
const turns = harnessResults.map((harnessResult2) => ({ harnessResult: harnessResult2, reply: collectReply(permutation.harness.collectReply, harnessResult2) }));
|
|
3999
|
-
const tokenCosts =
|
|
4204
|
+
const tokenCosts = collectTokenCosts(permutation.collectTokenCosts, harnessResults, {
|
|
4000
4205
|
version: permutation.version,
|
|
4001
4206
|
modelId: permutation.modelId
|
|
4002
4207
|
});
|
|
@@ -4084,8 +4289,8 @@ var showMeasures = (result) => Object.entries(result).map(([name, { outcome }])
|
|
|
4084
4289
|
function label(testCase2, { permutation }) {
|
|
4085
4290
|
const varying = AXES.filter((axis) => axis !== "harnessModel" && axis !== "initialPrompt" && testCase2[axis].length > 1);
|
|
4086
4291
|
const harnesses = new Set(testCase2.harnessModel.map((pair) => parsePair(pair).harness));
|
|
4087
|
-
const
|
|
4088
|
-
return [...varying.map((axis) => permutation[axis]),
|
|
4292
|
+
const harness = harnesses.size === testCase2.harnessModel.length ? parsePair(permutation.harnessModel).harness : permutation.harnessModel;
|
|
4293
|
+
return [...varying.map((axis) => permutation[axis]), harness, permutation.initialPrompt].join(" | ");
|
|
4089
4294
|
}
|
|
4090
4295
|
async function run(cwd, name, imagePrefix, dataDir) {
|
|
4091
4296
|
const root = await findThunderjarRoot(cwd);
|
|
@@ -4202,12 +4407,12 @@ function aggregateValues(values, methods) {
|
|
|
4202
4407
|
};
|
|
4203
4408
|
}
|
|
4204
4409
|
function aggregatePermutation(runs, measurements) {
|
|
4205
|
-
return Object.fromEntries(measurements.map(({ name, declaration:
|
|
4410
|
+
return Object.fromEntries(measurements.map(({ name, declaration: declaration3 }) => {
|
|
4206
4411
|
const values = runs.flatMap(({ result }) => {
|
|
4207
4412
|
const outcome = result[name]?.outcome;
|
|
4208
4413
|
return outcome?.outcome === "measured" ? [outcome.data] : [];
|
|
4209
4414
|
});
|
|
4210
|
-
const combined = aggregateValues(values,
|
|
4415
|
+
const combined = aggregateValues(values, declaration3.aggregate);
|
|
4211
4416
|
const measure = "incompatible" in combined ? { measured: values.length, values: {}, incompatible: combined.incompatible } : { measured: values.length, values: combined.values };
|
|
4212
4417
|
return [name, measure];
|
|
4213
4418
|
}));
|
|
@@ -4285,7 +4490,7 @@ function varyingAxes(runs) {
|
|
|
4285
4490
|
}
|
|
4286
4491
|
function permutationTable(execution, runs, measurements) {
|
|
4287
4492
|
const axes = varyingAxes(runs);
|
|
4288
|
-
const methodColumns = measurements.flatMap(({ name, declaration:
|
|
4493
|
+
const methodColumns = measurements.flatMap(({ name, declaration: declaration3 }) => declaration3.aggregate.map((method, i) => ({ name, method, first: i === 0 })));
|
|
4289
4494
|
const numberWidth = String(execution.matrix.permutations).length;
|
|
4290
4495
|
const withCosts = runs.some(({ tokenCosts }) => tokenCosts !== undefined);
|
|
4291
4496
|
const rows = [];
|
|
@@ -4336,9 +4541,9 @@ function permutationTable(execution, runs, measurements) {
|
|
|
4336
4541
|
}
|
|
4337
4542
|
const header = ["", axes.join(" | "), "runs", ...methodColumns.map(({ method }) => method), ...withCosts ? ["tokens (mean)", "cost (mean)"] : []];
|
|
4338
4543
|
const widths = header.map((cell, i) => Math.max(cell.length, ...rows.map((row) => row[i]?.length ?? 0)));
|
|
4339
|
-
for (const { name, declaration:
|
|
4544
|
+
for (const { name, declaration: declaration3 } of measurements) {
|
|
4340
4545
|
const start = 3 + methodColumns.findIndex((column2) => column2.name === name);
|
|
4341
|
-
const end = start +
|
|
4546
|
+
const end = start + declaration3.aggregate.length - 1;
|
|
4342
4547
|
const span = widths.slice(start, end + 1).reduce((a, b) => a + b, 0) + GAP.length * (end - start);
|
|
4343
4548
|
if (span < name.length)
|
|
4344
4549
|
widths[end] += name.length - span;
|
package/package.json
CHANGED
package/src/api/declarations.ts
CHANGED
|
@@ -79,6 +79,9 @@ export type BaseImageDeclaration<C = unknown> = Applies<C, ApplyParameterCtx> &
|
|
|
79
79
|
* namespaces: `["--security-opt", "seccomp=unconfined", "--security-opt", "apparmor=unconfined"]`.
|
|
80
80
|
* Each option and value is its own string. Ask for the least you need: `--privileged` works, but
|
|
81
81
|
* gives the agent the whole machine.
|
|
82
|
+
*
|
|
83
|
+
* Some hosts forbid what no option can allow. Ubuntu 23.10 and later stop unconfined processes
|
|
84
|
+
* making user namespaces until `sysctl kernel.apparmor_restrict_unprivileged_userns=0`.
|
|
82
85
|
*/
|
|
83
86
|
runOptions?: string[];
|
|
84
87
|
};
|