thunderjar 0.1.3-preview-eee7160 → 0.1.3-preview-e09ffa1
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 +1934 -1721
- package/package.json +14 -8
- package/src/api/checkoutCommit.ts +35 -0
- package/src/api/claudeCode.ts +74 -0
- package/src/api/declarations.ts +176 -133
- package/src/api/index.ts +44 -0
- package/src/api/parameterNames.ts +19 -0
- package/src/AGENTS.md +0 -156
- package/src/adapters/docker/docker.ts +0 -117
- package/src/api/fragments.ts +0 -30
- package/src/api/parameter-names.d.ts +0 -16
- package/src/cli/commands/build/build.docker.test.ts +0 -173
- package/src/cli/commands/build/build.test.ts +0 -234
- package/src/cli/commands/build/build.ts +0 -115
- package/src/cli/commands/declarations.typecheck.test.ts +0 -173
- package/src/cli/commands/executions/executions.test.ts +0 -84
- package/src/cli/commands/executions/executions.ts +0 -31
- package/src/cli/commands/format.ts +0 -33
- package/src/cli/commands/generate/generate.test.ts +0 -101
- package/src/cli/commands/generate/generate.ts +0 -18
- package/src/cli/commands/generate/parameter-names.ts +0 -131
- package/src/cli/commands/init/init.test.ts +0 -123
- package/src/cli/commands/init/init.ts +0 -189
- package/src/cli/commands/options.ts +0 -17
- package/src/cli/commands/plan/plan.test.ts +0 -168
- package/src/cli/commands/plan/plan.ts +0 -72
- package/src/cli/commands/remeasure/remeasure.test.ts +0 -536
- package/src/cli/commands/remeasure/remeasure.ts +0 -194
- package/src/cli/commands/run/run.test.ts +0 -622
- package/src/cli/commands/run/run.ts +0 -203
- package/src/cli/commands/show/show.test.ts +0 -288
- package/src/cli/commands/show/show.ts +0 -252
- package/src/cli/commands/validation.test.ts +0 -200
- package/src/cli/index.test.ts +0 -47
- package/src/cli/index.ts +0 -9
- package/src/cli/program.ts +0 -29
- package/src/images/prerun-builds.ts +0 -108
- package/src/images/prerun.test.ts +0 -243
- package/src/images/prerun.ts +0 -272
- package/src/images/repository.ts +0 -72
- package/src/index.ts +0 -24
- package/src/instruments/instruments.test.ts +0 -240
- package/src/instruments/instruments.ts +0 -205
- package/src/matrix/execution.ts +0 -122
- package/src/matrix/hashing.test.ts +0 -298
- package/src/matrix/hashing.ts +0 -131
- package/src/matrix/permutations.test.ts +0 -58
- package/src/matrix/permutations.ts +0 -50
- package/src/matrix/resolve.ts +0 -63
- package/src/matrix/tags.test.ts +0 -15
- package/src/matrix/tags.ts +0 -22
- package/src/matrix/ulid.test.ts +0 -28
- package/src/matrix/ulid.ts +0 -22
- package/src/measuring/aggregate.test.ts +0 -188
- package/src/measuring/aggregate.ts +0 -154
- package/src/measuring/measure.test.ts +0 -105
- package/src/measuring/measure.ts +0 -62
- package/src/measuring/remeasure-templates/v1/isPrime.test.template.txt +0 -4
- package/src/measuring/remeasure-templates/v2/isPrime.test.template.txt +0 -5
- package/src/measuring/remeasure.test.ts +0 -352
- package/src/measuring/remeasure.ts +0 -113
- package/src/platform/fs.test.ts +0 -89
- package/src/platform/fs.ts +0 -123
- package/src/platform/path.test.ts +0 -38
- package/src/platform/path.ts +0 -72
- package/src/platform/signals.test.ts +0 -25
- package/src/platform/signals.ts +0 -68
- package/src/project/config.test.ts +0 -68
- package/src/project/config.ts +0 -297
- package/src/running/runner.test.ts +0 -73
- package/src/running/runner.ts +0 -252
- package/src/store/run-data-store.test.ts +0 -232
- package/src/store/run-data-store.ts +0 -254
- package/src/testing/cli.ts +0 -65
- package/src/testing/fake-docker.test.ts +0 -197
- package/src/testing/fake-docker.ts +0 -104
- package/src/testing/fixture-project.ts +0 -177
- package/src/testing/measurement-context.ts +0 -5
- package/src/testing/seed-repo.test.ts +0 -10
- package/src/testing/seed-repo.ts +0 -83
- package/src/testing/test-setup.ts +0 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thunderjar",
|
|
3
|
-
"version": "0.1.3-preview-
|
|
3
|
+
"version": "0.1.3-preview-e09ffa1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Thunderjar",
|
|
6
6
|
"license": "MIT",
|
|
@@ -11,24 +11,30 @@
|
|
|
11
11
|
"bin": {
|
|
12
12
|
"thunderjar": "./dist/cli.js"
|
|
13
13
|
},
|
|
14
|
-
"main": "./src/index.ts",
|
|
15
|
-
"types": "./src/index.ts",
|
|
14
|
+
"main": "./src/api/index.ts",
|
|
15
|
+
"types": "./src/api/index.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": "./src/api/index.ts"
|
|
18
|
+
},
|
|
16
19
|
"files": [
|
|
17
20
|
"dist",
|
|
18
|
-
"src"
|
|
21
|
+
"src/api",
|
|
22
|
+
"!src/**/*.test.ts",
|
|
23
|
+
"!src/**/*.result.json"
|
|
19
24
|
],
|
|
20
25
|
"publishConfig": {
|
|
21
26
|
"access": "public"
|
|
22
27
|
},
|
|
23
28
|
"scripts": {
|
|
24
29
|
"build": "bun build src/cli/index.ts --target bun --outfile dist/cli.js",
|
|
25
|
-
"lint": "bun scripts/
|
|
30
|
+
"lint": "bun scripts/lint-layers.ts --check && oxlint",
|
|
31
|
+
"lint:generate": "bun scripts/lint-layers.ts",
|
|
26
32
|
"typecheck": "tsc --noEmit",
|
|
27
33
|
"test": "bun test",
|
|
28
|
-
"test:
|
|
29
|
-
"test:
|
|
34
|
+
"test:docker": "THUNDERJAR_DOCKER_TESTS=1 bun test --timeout 600000 .docker.test",
|
|
35
|
+
"test:agent": "THUNDERJAR_AGENT_TESTS=1 bun test --timeout 900000 .agent.test",
|
|
36
|
+
"test:package": "bun scripts/package-smoke-test.ts",
|
|
30
37
|
"fixture": "bun scripts/fixture.ts",
|
|
31
|
-
"dev:link": "bun scripts/dev-link.ts",
|
|
32
38
|
"docs:dev": "vitepress dev docs",
|
|
33
39
|
"docs:build": "vitepress build docs",
|
|
34
40
|
"docs:build:versions": "bun scripts/build-versioned-docs.ts",
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ApplyParameterCtx } from "./declarations.ts";
|
|
2
|
+
|
|
3
|
+
// How checkoutCommit tells Thunderjar which commit to check out: a code state's ctx carries a
|
|
4
|
+
// function under this key. Symbol.for, so it is found even when the user's project resolves a
|
|
5
|
+
// different copy of this module.
|
|
6
|
+
export const CHECKOUT_COMMIT = Symbol.for("thunderjar.checkoutCommit");
|
|
7
|
+
|
|
8
|
+
/** A code state's ctx, as Thunderjar makes it. */
|
|
9
|
+
export type CheckoutCtx = ApplyParameterCtx & { [CHECKOUT_COMMIT]?: (commit: string) => void };
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Checks `commit`, a hash, out into `/workspace` as a real repository. For a code state's
|
|
13
|
+
* `applyParameter` only.
|
|
14
|
+
*
|
|
15
|
+
* Thunderjar bundles the commit and its ancestors, and nothing after it, into the build context.
|
|
16
|
+
* Straight after the base image's fragment, the prerun build clones it to `/workspace`, checks
|
|
17
|
+
* the commit out detached, removes the remote, and sets a git identity so the agent can commit.
|
|
18
|
+
* Those lines are Thunderjar's, so this returns an empty fragment, for the code state's own
|
|
19
|
+
* lines to follow:
|
|
20
|
+
*
|
|
21
|
+
* ```ts
|
|
22
|
+
* export default declareCodeState({
|
|
23
|
+
* config: { commit: "a1b2c3" },
|
|
24
|
+
* applyParameter: (config, ctx) => `${checkoutCommit(ctx, config.commit)}\nRUN npm ci`,
|
|
25
|
+
* });
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* The commit must already be in the repository the project is in, or nothing is built.
|
|
29
|
+
*/
|
|
30
|
+
export function checkoutCommit(ctx: ApplyParameterCtx, commit: string): string {
|
|
31
|
+
const checkout = (ctx as CheckoutCtx)[CHECKOUT_COMMIT];
|
|
32
|
+
if (!checkout) throw new Error("checkoutCommit is only for a code state's applyParameter.");
|
|
33
|
+
checkout(commit);
|
|
34
|
+
return "";
|
|
35
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Reading what `claude -p --output-format json` writes: one JSON object, with the reply as
|
|
2
|
+
// `result`, and the tokens and cost of each model it used as `modelUsage`. For a harness's
|
|
3
|
+
// `collectReply` and `collectTokenCosts`, so a Claude Code harness needn't parse it itself.
|
|
4
|
+
//
|
|
5
|
+
// `modelUsage` is read, not the flat `usage`: `usage` counts only the top-level agent loop, so it
|
|
6
|
+
// leaves out what sub-agents spent. Its keys are camelCase, where `usage`'s are snake_case.
|
|
7
|
+
import type { ModelTokenCosts, TokenCosts } from "./declarations.ts";
|
|
8
|
+
|
|
9
|
+
type ModelUsage = {
|
|
10
|
+
inputTokens?: unknown;
|
|
11
|
+
outputTokens?: unknown;
|
|
12
|
+
cacheReadInputTokens?: unknown;
|
|
13
|
+
cacheCreationInputTokens?: unknown;
|
|
14
|
+
costUSD?: unknown;
|
|
15
|
+
costBasis?: unknown;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type ClaudeCodeResult = { type?: unknown; result?: unknown; modelUsage?: unknown; duration_api_ms?: unknown };
|
|
19
|
+
|
|
20
|
+
const COST_BASES = ["list", "managed", "unknown"] as const;
|
|
21
|
+
|
|
22
|
+
const count = (value: unknown) => (typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined);
|
|
23
|
+
|
|
24
|
+
/** `raw` parsed as Claude Code's result, or undefined if it isn't one. */
|
|
25
|
+
function parse(raw: string): ClaudeCodeResult | undefined {
|
|
26
|
+
let parsed: unknown;
|
|
27
|
+
try {
|
|
28
|
+
parsed = JSON.parse(raw);
|
|
29
|
+
} catch {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
return typeof parsed === "object" && parsed !== null && (parsed as ClaudeCodeResult).type === "result"
|
|
33
|
+
? (parsed as ClaudeCodeResult)
|
|
34
|
+
: undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The agent's final reply, from the result file Claude Code wrote. Throws if it isn't one. */
|
|
38
|
+
export function claudeCodeReply(raw: string): string {
|
|
39
|
+
const result = parse(raw)?.result;
|
|
40
|
+
if (typeof result !== "string") throw new Error("not a result of `claude -p --output-format json`");
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The tokens and cost of each model, from the result file Claude Code wrote. `unparseable` if it
|
|
46
|
+
* isn't one, and `notReported` if it counts no tokens at all, as a crashed session's does. Its
|
|
47
|
+
* cost is its own estimate, from a price table bundled with it: `costBasis` says whether that
|
|
48
|
+
* table knew the model.
|
|
49
|
+
*/
|
|
50
|
+
export function claudeCodeTokenCosts(raw: string): TokenCosts {
|
|
51
|
+
const result = parse(raw);
|
|
52
|
+
if (!result || typeof result.modelUsage !== "object" || result.modelUsage === null) return { outcome: "unavailable", reason: "unparseable" };
|
|
53
|
+
const models: ModelTokenCosts[] = [];
|
|
54
|
+
for (const [model, usage] of Object.entries(result.modelUsage as Record<string, ModelUsage>)) {
|
|
55
|
+
const tokens = [usage.inputTokens, usage.outputTokens, usage.cacheCreationInputTokens, usage.cacheReadInputTokens].map(count);
|
|
56
|
+
if (tokens.some((n) => n === undefined)) return { outcome: "unavailable", reason: "unparseable" };
|
|
57
|
+
const [inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens] = tokens as number[];
|
|
58
|
+
const costUsd = count(usage.costUSD);
|
|
59
|
+
const costBasis = COST_BASES.find((basis) => basis === usage.costBasis);
|
|
60
|
+
models.push({
|
|
61
|
+
model,
|
|
62
|
+
inputTokens: inputTokens as number,
|
|
63
|
+
outputTokens: outputTokens as number,
|
|
64
|
+
cacheCreationTokens: cacheCreationTokens as number,
|
|
65
|
+
cacheReadTokens: cacheReadTokens as number,
|
|
66
|
+
...(costUsd !== undefined && { costUsd }),
|
|
67
|
+
...(costBasis !== undefined && { costBasis }),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
const total = models.reduce((sum, m) => sum + m.inputTokens + m.outputTokens + m.cacheCreationTokens + m.cacheReadTokens, 0);
|
|
71
|
+
if (total === 0) return { outcome: "unavailable", reason: "notReported" };
|
|
72
|
+
const apiDurationMs = count(result.duration_api_ms);
|
|
73
|
+
return { outcome: "collected", models, ...(apiDurationMs !== undefined && { apiDurationMs }) };
|
|
74
|
+
}
|
package/src/api/declarations.ts
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
|
-
// The `declareX()` functions a
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
// The `declareX()` functions that a `.thunderjar/` file's default export is wrapped in. Each
|
|
2
|
+
// returns what it is given: the point is the type of its argument.
|
|
3
|
+
import type {
|
|
4
|
+
BaseImageNames,
|
|
5
|
+
CodeStateNames,
|
|
6
|
+
HarnessModelNames,
|
|
7
|
+
InitialPromptNamesByTask,
|
|
8
|
+
PromptSetNames,
|
|
9
|
+
TaskNames,
|
|
10
|
+
} from "./parameterNames.ts";
|
|
11
|
+
|
|
12
|
+
// Each declareX() marks what it returns with its own name, so the config loader can tell a wrapped
|
|
13
|
+
// default export from a bare one. Symbol.for, so the mark is recognised even when the user's
|
|
14
|
+
// project resolves a different copy of this module.
|
|
12
15
|
const DECLARED_BY = Symbol.for("thunderjar.declaredBy");
|
|
13
16
|
|
|
14
17
|
function declared<T extends object>(declareFn: string, value: T): T {
|
|
@@ -25,202 +28,244 @@ export function declaredBy(value: unknown): string | undefined {
|
|
|
25
28
|
return typeof mark === "string" ? mark : undefined;
|
|
26
29
|
}
|
|
27
30
|
|
|
28
|
-
// --- applyParameter
|
|
31
|
+
// --- applyParameter ---
|
|
29
32
|
//
|
|
30
|
-
// Each parameter's `applyParameter(config, ctx)` returns the Dockerfile fragment that applies
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
+
// Each parameter's `applyParameter(config, ctx)` returns the Dockerfile fragment that applies it
|
|
34
|
+
// to the prerun image; see src/execution/prerunDockerfile.ts for the order they are built in.
|
|
35
|
+
// `config` is the declaration's own `config`, as written (undefined if it has none).
|
|
33
36
|
|
|
34
37
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* before anything is built.
|
|
38
|
+
* Reads the repository the project is in, without fetching. Each commit is a hash, full or
|
|
39
|
+
* abbreviated, and must already be in the repository, or nothing is built.
|
|
38
40
|
*/
|
|
39
|
-
export
|
|
40
|
-
/**
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
export type GitCtx = {
|
|
42
|
+
/**
|
|
43
|
+
* Puts `path`'s content at `commit` into the build context, and returns where it is there, to
|
|
44
|
+
* COPY: `COPY ${ctx.git.show("e4f5a6", "prompts/snerk.md")} /workspace/CLAUDE.md`. `path` is
|
|
45
|
+
* relative to the repository's root.
|
|
46
|
+
*/
|
|
43
47
|
show(commit: string, path: string): string;
|
|
44
|
-
}
|
|
48
|
+
};
|
|
45
49
|
|
|
46
50
|
/** What every fragment gets. */
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
+
export type FragmentCtx = {
|
|
52
|
+
git: GitCtx;
|
|
53
|
+
};
|
|
51
54
|
|
|
52
55
|
/**
|
|
53
|
-
* What a fragment gets when its parameter is a folder
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* renaming the folder changes nothing.
|
|
56
|
+
* What a fragment gets when its parameter is a folder. `dir` is that folder's path in the build
|
|
57
|
+
* context, so files kept beside the declaration can be copied in: `COPY ${ctx.dir}/files/ /workspace/`.
|
|
58
|
+
* It names the parameter's role, not its folder, so renaming the folder changes nothing.
|
|
57
59
|
*/
|
|
58
|
-
export
|
|
60
|
+
export type ApplyParameterCtx = FragmentCtx & {
|
|
59
61
|
dir: string;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// spec/060-test-case-parameters.md#controlling-the-parameter-hash
|
|
63
|
-
interface HashOverrides {
|
|
64
|
-
additionalHash?: () => string;
|
|
65
|
-
determineHash?: () => string;
|
|
66
|
-
}
|
|
62
|
+
};
|
|
67
63
|
|
|
68
|
-
/**
|
|
69
|
-
|
|
64
|
+
/**
|
|
65
|
+
* A parameter declaration's `config`, and the `applyParameter` that receives it. A method, so a
|
|
66
|
+
* declaration with any `config` is still a `XDeclaration<unknown>` once loaded.
|
|
67
|
+
*/
|
|
68
|
+
type Applies<C, Ctx> = {
|
|
70
69
|
config?: C;
|
|
71
|
-
applyParameter
|
|
72
|
-
}
|
|
70
|
+
applyParameter?(config: C, ctx: Ctx): string;
|
|
71
|
+
};
|
|
73
72
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
73
|
+
/** A base image's fragment is required: it carries the prerun image's `FROM`. */
|
|
74
|
+
export type BaseImageDeclaration<C = unknown> = Applies<C, ApplyParameterCtx> & {
|
|
75
|
+
applyParameter(config: C, ctx: ApplyParameterCtx): string;
|
|
76
|
+
/**
|
|
77
|
+
* More `docker run` options for the agent's containers, for agents that need more of the
|
|
78
|
+
* kernel than Docker allows by default, e.g. Claude Code's bubblewrap sandbox, which needs user
|
|
79
|
+
* namespaces: `["--security-opt", "seccomp=unconfined", "--security-opt", "apparmor=unconfined"]`.
|
|
80
|
+
* Each option and value is its own string. Ask for the least you need: `--privileged` works, but
|
|
81
|
+
* gives the agent the whole machine.
|
|
82
|
+
*/
|
|
83
|
+
runOptions?: string[];
|
|
84
|
+
};
|
|
77
85
|
|
|
78
86
|
export function declareBaseImage<C = undefined>(declaration: BaseImageDeclaration<C>): BaseImageDeclaration<C> {
|
|
79
87
|
return declared("declareBaseImage", declaration);
|
|
80
88
|
}
|
|
81
89
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
interface CodeStateDeclaration<C> extends HashOverrides, Applies<C, ApplyParameterCtx> {}
|
|
90
|
+
export type CodeStateDeclaration<C = unknown> = Applies<C, ApplyParameterCtx>;
|
|
85
91
|
|
|
86
92
|
export function declareCodeState<C = undefined>(declaration: CodeStateDeclaration<C>): CodeStateDeclaration<C> {
|
|
87
93
|
return declared("declareCodeState", declaration);
|
|
88
94
|
}
|
|
89
95
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
interface PromptSetDeclaration<C> extends HashOverrides, Applies<C, ApplyParameterCtx> {}
|
|
96
|
+
export type PromptSetDeclaration<C = unknown> = Applies<C, ApplyParameterCtx>;
|
|
93
97
|
|
|
94
98
|
export function declarePromptSet<C = undefined>(declaration: PromptSetDeclaration<C>): PromptSetDeclaration<C> {
|
|
95
99
|
return declared("declarePromptSet", declaration);
|
|
96
100
|
}
|
|
97
101
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
/** `dir` is the family folder, less models.ts, versions.ts and collectTokenCosts.ts. */
|
|
101
|
-
export interface HarnessApplyCtx extends ApplyParameterCtx {
|
|
102
|
+
/** `dir` is the harness's folder; `version` is the pair's version, as its `versions.ts` entry gives it. */
|
|
103
|
+
export type HarnessApplyCtx = ApplyParameterCtx & {
|
|
102
104
|
version: string;
|
|
103
|
-
}
|
|
105
|
+
};
|
|
104
106
|
|
|
105
|
-
|
|
107
|
+
/**
|
|
108
|
+
* What a harness's `cli` gets, to build the command that runs the agent. An initial prompt of
|
|
109
|
+
* several turns runs `cli` once per turn, in the same container, one after another.
|
|
110
|
+
*/
|
|
111
|
+
export type HarnessCliCtx = {
|
|
106
112
|
version: string;
|
|
107
113
|
modelId: string;
|
|
114
|
+
/**
|
|
115
|
+
* The turn's prompt text. Pasted into a shell command it breaks on the first quote, `$` or
|
|
116
|
+
* backtick, so read `promptPath` instead.
|
|
117
|
+
*/
|
|
108
118
|
initialPrompt: string;
|
|
119
|
+
/**
|
|
120
|
+
* A file in the container holding the turn's prompt, exactly, with no newline added: pass it to
|
|
121
|
+
* the agent as `"$(cat ${ctx.promptPath})"`, or as a file if it takes one.
|
|
122
|
+
*/
|
|
123
|
+
promptPath: string;
|
|
124
|
+
/** Where the agent's own report of the turn goes. Each turn has its own. */
|
|
109
125
|
resultPath: string;
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
126
|
+
/** Which turn this is, counting from 0. A single prompt is turn 0. */
|
|
127
|
+
turn: number;
|
|
128
|
+
/**
|
|
129
|
+
* A fresh UUID for each container run, the same for each of its turns: start the agent's
|
|
130
|
+
* session with it on turn 0, and resume it on later turns, e.g. `--session-id`, then `--resume`.
|
|
131
|
+
*/
|
|
132
|
+
sessionId: string;
|
|
133
|
+
};
|
|
118
134
|
|
|
119
|
-
export
|
|
135
|
+
export type HarnessDeclaration<C = unknown> = Applies<C, HarnessApplyCtx> & {
|
|
120
136
|
cli: (ctx: HarnessCliCtx) => string;
|
|
137
|
+
/**
|
|
138
|
+
* The agent's final reply, read from the text of the result file its `cli` left, for
|
|
139
|
+
* instruments such as `replyMatches` to measure. Without it, a run has no reply.
|
|
140
|
+
*/
|
|
141
|
+
collectReply?: (raw: string) => string;
|
|
142
|
+
/**
|
|
143
|
+
* The names of the environment variables the agent needs, e.g. its API key. Each is passed into
|
|
144
|
+
* this harness's containers only, as `docker run -e NAME`, so its value comes from the shell that
|
|
145
|
+
* runs Thunderjar, and is never written down. `plan` and `run` stop if one is unset.
|
|
146
|
+
*/
|
|
121
147
|
requiredEnv?: string[];
|
|
122
|
-
}
|
|
148
|
+
};
|
|
123
149
|
|
|
124
150
|
export function declareHarness<C = undefined>(declaration: HarnessDeclaration<C>): HarnessDeclaration<C> {
|
|
125
151
|
return declared("declareHarness", declaration);
|
|
126
152
|
}
|
|
127
153
|
|
|
128
|
-
|
|
154
|
+
/** A harness's models: the name a test case uses, to the ID the agent is given. */
|
|
155
|
+
export type HarnessModelsDeclaration = Record<string, string>;
|
|
156
|
+
|
|
157
|
+
export function declareHarnessModels(models: HarnessModelsDeclaration): HarnessModelsDeclaration {
|
|
129
158
|
return declared("declareHarnessModels", models);
|
|
130
159
|
}
|
|
131
160
|
|
|
132
|
-
|
|
161
|
+
/** A harness's versions, by the name a test case uses. */
|
|
162
|
+
export type HarnessVersionsDeclaration = Record<string, { version: string }>;
|
|
133
163
|
|
|
134
|
-
export function declareHarnessVersions(
|
|
135
|
-
versions: Record<string, HarnessVersionEntry>,
|
|
136
|
-
): Record<string, HarnessVersionEntry> {
|
|
164
|
+
export function declareHarnessVersions(versions: HarnessVersionsDeclaration): HarnessVersionsDeclaration {
|
|
137
165
|
return declared("declareHarnessVersions", versions);
|
|
138
166
|
}
|
|
139
167
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
version: string;
|
|
143
|
-
modelId: string;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
interface ModelTokenCosts {
|
|
168
|
+
/** One model's tokens, in four buckets priced differently, and its cost if the harness reports one. */
|
|
169
|
+
export type ModelTokenCosts = {
|
|
147
170
|
model: string;
|
|
148
171
|
inputTokens: number;
|
|
149
172
|
outputTokens: number;
|
|
150
173
|
cacheCreationTokens: number;
|
|
151
174
|
cacheReadTokens: number;
|
|
175
|
+
/** The harness's own estimate, never computed by Thunderjar. Absent if it reports none. */
|
|
152
176
|
costUsd?: number;
|
|
177
|
+
/** How far to trust `costUsd`: `unknown` when the harness's price table didn't know the model. */
|
|
153
178
|
costBasis?: "list" | "managed" | "unknown";
|
|
154
|
-
}
|
|
179
|
+
};
|
|
155
180
|
|
|
156
|
-
|
|
157
|
-
|
|
181
|
+
/**
|
|
182
|
+
* A container run's token costs, per model, or why there are none: no result file (`absent`), one
|
|
183
|
+
* that couldn't be read (`unparseable`), or one that counted nothing (`notReported`).
|
|
184
|
+
*/
|
|
185
|
+
export type TokenCosts =
|
|
186
|
+
| { outcome: "collected"; models: ModelTokenCosts[]; /** Time spent waiting on the model, if the harness reports it. */ apiDurationMs?: number }
|
|
158
187
|
| { outcome: "unavailable"; reason: "absent" | "unparseable" | "notReported" };
|
|
159
188
|
|
|
160
|
-
|
|
189
|
+
/**
|
|
190
|
+
* Reads the token costs from what the harness's `cli` wrote to its result path. Thunderjar calls
|
|
191
|
+
* it only for a result file that exists, once per turn, and sums them. A throw is `unparseable`.
|
|
192
|
+
*/
|
|
193
|
+
export type TokenCostCollector = (raw: string, ctx: { version: string; modelId: string }) => TokenCosts;
|
|
161
194
|
|
|
162
195
|
export function declareTokenCostCollector(collector: TokenCostCollector): TokenCostCollector {
|
|
163
196
|
return declared("declareTokenCostCollector", collector);
|
|
164
197
|
}
|
|
165
198
|
|
|
166
|
-
// --- Initial prompt: spec/066-task.md#initial-prompts ---
|
|
167
|
-
|
|
168
199
|
/**
|
|
169
|
-
* An initial prompt is one file, not a folder, so its fragment has no `ctx.dir
|
|
170
|
-
*
|
|
200
|
+
* An initial prompt is one file, not a folder, so its fragment has no `ctx.dir`. It is used when
|
|
201
|
+
* the harness runs, so its fragment is usually left out.
|
|
202
|
+
*
|
|
203
|
+
* It's a single `prompt`, or a list of `turns`, each run by the harness's `cli` in turn, in the
|
|
204
|
+
* same container and agent session, so a later turn can ask about an earlier one. Its
|
|
205
|
+
* `timeoutSeconds` covers every turn together.
|
|
171
206
|
*/
|
|
172
|
-
export
|
|
173
|
-
prompt: string;
|
|
207
|
+
export type InitialPromptDeclaration<C = unknown> = Applies<C, FragmentCtx> & {
|
|
174
208
|
timeoutSeconds: number;
|
|
175
|
-
}
|
|
209
|
+
} & ({ prompt: string; turns?: never } | { turns: string[]; prompt?: never });
|
|
176
210
|
|
|
177
211
|
export function declareInitialPrompt<C = undefined>(declaration: InitialPromptDeclaration<C>): InitialPromptDeclaration<C> {
|
|
178
212
|
return declared("declareInitialPrompt", declaration);
|
|
179
213
|
}
|
|
180
214
|
|
|
181
|
-
// ---
|
|
215
|
+
// --- Measurements ---
|
|
182
216
|
|
|
183
|
-
/** What an instrument
|
|
217
|
+
/** What an instrument gives for one measurement of one container run. */
|
|
184
218
|
export type MeasurementResult<T> =
|
|
185
219
|
| { outcome: "measured"; data: T }
|
|
186
|
-
|
|
187
|
-
| { outcome: "
|
|
220
|
+
/** Nothing to measure, e.g. no file matched. */
|
|
221
|
+
| { outcome: "skipped"; reason?: string }
|
|
222
|
+
/** The instrument itself failed. */
|
|
223
|
+
| { outcome: "erroredWhileMeasuring"; error: Error };
|
|
188
224
|
|
|
189
225
|
/**
|
|
190
|
-
* What an instrument runs against:
|
|
191
|
-
*
|
|
192
|
-
* resolve in `workdir`.
|
|
226
|
+
* What an instrument runs against: a measurement container, started from a container run's
|
|
227
|
+
* postrun image, and discarded once the measure is taken. Relative paths, glob patterns and
|
|
228
|
+
* commands resolve in `workdir`.
|
|
193
229
|
*/
|
|
194
|
-
export
|
|
230
|
+
export type MeasurementContext = {
|
|
195
231
|
exec(command: string): Promise<{ stdout: string; stderr: string; exitCode: number }>;
|
|
196
232
|
readFile(path: string): Promise<string>;
|
|
197
233
|
writeFile(path: string, contents: string): Promise<void>;
|
|
198
|
-
/**
|
|
234
|
+
/** The files matching `pattern`, relative to `workdir`, sorted. */
|
|
199
235
|
glob(pattern: string): Promise<string[]>;
|
|
200
|
-
/**
|
|
236
|
+
/** The text of the result file the harness's `cli` left, or "" if it left none. */
|
|
201
237
|
harnessResult: string;
|
|
238
|
+
/** The agent's final reply, from its harness's `collectReply`, or why there's none. */
|
|
239
|
+
reply: Reply;
|
|
240
|
+
/** Its reply to each turn, in order: one, unless its initial prompt has several turns. */
|
|
241
|
+
replies: Reply[];
|
|
202
242
|
workdir: string;
|
|
203
|
-
}
|
|
243
|
+
};
|
|
204
244
|
|
|
205
|
-
/**
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
*/
|
|
209
|
-
export
|
|
245
|
+
/** The agent's final reply, as its harness's `collectReply` read it from the result file, or why there's none. */
|
|
246
|
+
export type Reply = { outcome: "collected"; text: string } | { outcome: "unavailable"; reason: string };
|
|
247
|
+
|
|
248
|
+
/** Each built-in instrument's config and value, by name. */
|
|
249
|
+
export type InstrumentTypes = {
|
|
210
250
|
/** Did the agent leave a file matching `glob`? */
|
|
211
251
|
fileCreated: { config: { glob: string }; value: boolean };
|
|
212
|
-
/** How many lines, across the files matching `glob`, match `pattern`, a JavaScript
|
|
252
|
+
/** How many lines, across the files matching `glob`, match `pattern`, a JavaScript regular expression? */
|
|
213
253
|
grep: { config: { glob: string; pattern: string }; value: number };
|
|
214
254
|
/**
|
|
215
|
-
* How many tests pass and fail when `template`, a path relative to the measurement file, is
|
|
255
|
+
* How many tests pass and fail when `template`, a path relative to the measurement's file, is
|
|
216
256
|
* rendered against each file matching `subject` and run with `bun test`?
|
|
217
257
|
*/
|
|
218
258
|
templateTest: { config: { subject: string; template: string }; value: { passed: number; failed: number } };
|
|
219
|
-
|
|
259
|
+
/**
|
|
260
|
+
* Does the agent's reply match `pattern`, a JavaScript regular expression? Its reply to `turn`,
|
|
261
|
+
* counting from 1, of an initial prompt of several turns, or by default its last.
|
|
262
|
+
*/
|
|
263
|
+
replyMatches: { config: { pattern: string; turn?: number }; value: boolean };
|
|
264
|
+
};
|
|
220
265
|
|
|
221
266
|
export type InstrumentName = keyof InstrumentTypes;
|
|
222
267
|
|
|
223
|
-
/** The ways
|
|
268
|
+
/** The ways values of type `T` can combine across iterations. */
|
|
224
269
|
export type AggregateFor<T> = [T] extends [boolean]
|
|
225
270
|
? "rate" | "count"
|
|
226
271
|
: [T] extends [number]
|
|
@@ -229,12 +274,12 @@ export type AggregateFor<T> = [T] extends [boolean]
|
|
|
229
274
|
? "sum" | "mean"
|
|
230
275
|
: never;
|
|
231
276
|
|
|
232
|
-
/** An instrument
|
|
277
|
+
/** An instrument and its config: a `config` that doesn't fit the instrument is a type error. */
|
|
233
278
|
export type MeasurementDeclaration = {
|
|
234
279
|
[I in InstrumentName]: {
|
|
235
280
|
instrument: I;
|
|
236
281
|
config: InstrumentTypes[I]["config"];
|
|
237
|
-
/** How
|
|
282
|
+
/** How its values combine across iterations, e.g. `rate`. */
|
|
238
283
|
aggregate: AggregateFor<InstrumentTypes[I]["value"]>[];
|
|
239
284
|
};
|
|
240
285
|
}[InstrumentName];
|
|
@@ -243,37 +288,35 @@ export function declareMeasurement(declaration: MeasurementDeclaration): Measure
|
|
|
243
288
|
return declared("declareMeasurement", declaration);
|
|
244
289
|
}
|
|
245
290
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
291
|
+
/**
|
|
292
|
+
* Every name must be declared under `test-case-parameters/`, and each initial prompt must be one of
|
|
293
|
+
* `task`'s, once `thunderjar generate` has run.
|
|
294
|
+
*/
|
|
295
|
+
export type TestCaseDeclaration<T extends TaskNames = TaskNames> = {
|
|
296
|
+
baseImage: BaseImageNames[];
|
|
297
|
+
codeState: CodeStateNames[];
|
|
298
|
+
promptSet: PromptSetNames[];
|
|
299
|
+
harnessModel: HarnessModelNames[];
|
|
253
300
|
task: T;
|
|
254
|
-
initialPrompt:
|
|
301
|
+
initialPrompt: (T extends keyof InitialPromptNamesByTask ? InitialPromptNamesByTask[T] : never)[];
|
|
255
302
|
iterations: number;
|
|
256
|
-
}
|
|
303
|
+
};
|
|
257
304
|
|
|
258
|
-
export function declareTestCase<T extends
|
|
305
|
+
export function declareTestCase<T extends TaskNames>(declaration: TestCaseDeclaration<T>): TestCaseDeclaration<T> {
|
|
259
306
|
return declared("declareTestCase", declaration);
|
|
260
307
|
}
|
|
261
308
|
|
|
262
|
-
|
|
309
|
+
export type SqliteStore = { kind: "sqlite"; path: string };
|
|
263
310
|
|
|
264
|
-
|
|
265
|
-
path: string;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
export function sqliteStore(options: SqliteStoreOptions): { kind: "sqlite" } & SqliteStoreOptions {
|
|
311
|
+
export function sqliteStore(options: { path: string }): SqliteStore {
|
|
269
312
|
return { kind: "sqlite", ...options };
|
|
270
313
|
}
|
|
271
314
|
|
|
272
|
-
|
|
315
|
+
export type ConfigDeclaration = {
|
|
273
316
|
imageStore: { repository: string };
|
|
274
|
-
runDataStore:
|
|
275
|
-
}
|
|
317
|
+
runDataStore: SqliteStore;
|
|
318
|
+
};
|
|
276
319
|
|
|
277
|
-
export function declareConfig(config:
|
|
320
|
+
export function declareConfig(config: ConfigDeclaration): ConfigDeclaration {
|
|
278
321
|
return declared("declareConfig", config);
|
|
279
322
|
}
|
package/src/api/index.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// The `thunderjar` library: what a project's `.thunderjar/` files import.
|
|
2
|
+
export { checkoutCommit } from "./checkoutCommit.ts";
|
|
3
|
+
export { claudeCodeReply, claudeCodeTokenCosts } from "./claudeCode.ts";
|
|
4
|
+
export {
|
|
5
|
+
declareBaseImage,
|
|
6
|
+
declareCodeState,
|
|
7
|
+
declareConfig,
|
|
8
|
+
declareHarness,
|
|
9
|
+
declareHarnessModels,
|
|
10
|
+
declareHarnessVersions,
|
|
11
|
+
declareInitialPrompt,
|
|
12
|
+
declareMeasurement,
|
|
13
|
+
declarePromptSet,
|
|
14
|
+
declareTestCase,
|
|
15
|
+
declareTokenCostCollector,
|
|
16
|
+
sqliteStore,
|
|
17
|
+
} from "./declarations.ts";
|
|
18
|
+
export type {
|
|
19
|
+
AggregateFor,
|
|
20
|
+
ApplyParameterCtx,
|
|
21
|
+
BaseImageDeclaration,
|
|
22
|
+
CodeStateDeclaration,
|
|
23
|
+
ConfigDeclaration,
|
|
24
|
+
FragmentCtx,
|
|
25
|
+
GitCtx,
|
|
26
|
+
HarnessApplyCtx,
|
|
27
|
+
HarnessCliCtx,
|
|
28
|
+
HarnessDeclaration,
|
|
29
|
+
HarnessModelsDeclaration,
|
|
30
|
+
HarnessVersionsDeclaration,
|
|
31
|
+
InitialPromptDeclaration,
|
|
32
|
+
InstrumentName,
|
|
33
|
+
InstrumentTypes,
|
|
34
|
+
MeasurementContext,
|
|
35
|
+
MeasurementDeclaration,
|
|
36
|
+
MeasurementResult,
|
|
37
|
+
ModelTokenCosts,
|
|
38
|
+
PromptSetDeclaration,
|
|
39
|
+
Reply,
|
|
40
|
+
SqliteStore,
|
|
41
|
+
TestCaseDeclaration,
|
|
42
|
+
TokenCostCollector,
|
|
43
|
+
TokenCosts,
|
|
44
|
+
} from "./declarations.ts";
|