@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.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +270 -0
  3. package/examples/harbor-notes/README.md +40 -0
  4. package/examples/harbor-notes/evals/create-note/EVAL.ts +14 -0
  5. package/examples/harbor-notes/evals/create-note/PROMPT.md +9 -0
  6. package/examples/harbor-notes/evals/create-note/local/README.txt +1 -0
  7. package/examples/harbor-notes/experiments/scripted.ts +6 -0
  8. package/examples/harbor-notes/package.json +6 -0
  9. package/examples/harbor-notes/quickstudy.identity.json +1 -0
  10. package/examples/harbor-notes/runtime.ts +48 -0
  11. package/examples/harbor-notes/semantic-example.ts +21 -0
  12. package/images/agent-runtime/Dockerfile +58 -0
  13. package/images/egress-proxy/Dockerfile +28 -0
  14. package/images/mcp-proxy/Dockerfile +30 -0
  15. package/package.json +53 -0
  16. package/src/adapters/claude.ts +107 -0
  17. package/src/adapters/codex.ts +107 -0
  18. package/src/adapters/echo.ts +57 -0
  19. package/src/adapters/parse.ts +117 -0
  20. package/src/adapters/types.ts +152 -0
  21. package/src/build-info.generated.ts +12 -0
  22. package/src/cli.ts +787 -0
  23. package/src/completeness.ts +104 -0
  24. package/src/diagnose/excerpt.ts +106 -0
  25. package/src/diagnose/prompt.ts +175 -0
  26. package/src/diagnose/render.ts +55 -0
  27. package/src/diagnose/run.ts +290 -0
  28. package/src/diagnose/select.ts +110 -0
  29. package/src/diagnose/types.ts +88 -0
  30. package/src/evals/discovery.ts +173 -0
  31. package/src/evals/prompt.ts +190 -0
  32. package/src/evals/result.ts +10 -0
  33. package/src/evals/types.ts +115 -0
  34. package/src/execution-policy.ts +71 -0
  35. package/src/experiments/discovery.ts +76 -0
  36. package/src/experiments/groups.ts +119 -0
  37. package/src/experiments/types.ts +116 -0
  38. package/src/export-types.ts +127 -0
  39. package/src/export.ts +381 -0
  40. package/src/hash.ts +74 -0
  41. package/src/identity-diff.ts +30 -0
  42. package/src/ids.ts +30 -0
  43. package/src/index.ts +58 -0
  44. package/src/isolation/docker.ts +639 -0
  45. package/src/isolation/image-contexts.generated.ts +927 -0
  46. package/src/isolation/images.ts +138 -0
  47. package/src/isolation/mcp-proxy/server.ts +260 -0
  48. package/src/isolation/mcp.ts +144 -0
  49. package/src/isolation/proxy/allowlist.ts +148 -0
  50. package/src/isolation/proxy/server.ts +382 -0
  51. package/src/llm.ts +132 -0
  52. package/src/manifest.ts +228 -0
  53. package/src/model-identity.ts +12 -0
  54. package/src/plan.ts +55 -0
  55. package/src/probe.ts +426 -0
  56. package/src/report/pass-at-k.ts +76 -0
  57. package/src/report/report.ts +731 -0
  58. package/src/runner/context.ts +96 -0
  59. package/src/runner/deadline.ts +37 -0
  60. package/src/runner/execute.ts +992 -0
  61. package/src/runner/run-lock.ts +32 -0
  62. package/src/runner/scheduler.ts +62 -0
  63. package/src/runner/score-worker.ts +107 -0
  64. package/src/runner/scorer-worker.ts +61 -0
  65. package/src/runtime/types.ts +89 -0
  66. package/src/secrets.ts +151 -0
  67. package/src/semantic.ts +185 -0
  68. package/src/serve.ts +52 -0
  69. package/src/source-identity.ts +76 -0
  70. package/src/store/artifacts.ts +146 -0
  71. package/src/store/db.ts +318 -0
  72. package/src/store/schema.ts +39 -0
  73. package/src/surface-usage.ts +297 -0
  74. package/src/ui-bundle.generated.ts +12 -0
  75. package/ui/dist/index.html +32 -0
@@ -0,0 +1,228 @@
1
+ /**
2
+ * The v3 run manifest: immutable, content-addressed identity for one run.
3
+ *
4
+ * Ports the legacy identity techniques (canonical-JSON ordering, secret
5
+ * exclusion, tree hashing, hash-of-components-then-hash-of-manifest) onto the
6
+ * eval × experiment component set. There are deliberately no pack,
7
+ * packRuntime, or reference components — those are legacy manifest fields.
8
+ *
9
+ * The component key sets are snapshot-asserted in tests/identity-v3.test.ts:
10
+ * adding or removing a component is a deliberate, test-visible act.
11
+ */
12
+
13
+ import { readFileSync } from "node:fs";
14
+ import { join, resolve } from "node:path";
15
+ import { EMBEDDED_BUILD_INFO } from "./build-info.generated.ts";
16
+ import { hashString, hashTree, type ContentIdentity } from "./hash.ts";
17
+ import { sourceIdentity } from "./source-identity.ts";
18
+ import { pairKey, resolvePairEnvironments, type PairEnvironment } from "./execution-policy.ts";
19
+ import { canonicalJson } from "./identity-diff.ts";
20
+ import type { EvalMetadata, LoadedEval } from "./evals/types.ts";
21
+ import type { LoadedExperiment } from "./experiments/types.ts";
22
+ import type { AttemptPlan } from "./plan.ts";
23
+
24
+ export const MANIFEST_VERSION = "v4";
25
+
26
+ export interface HarnessIdentity {
27
+ version: string;
28
+ revision: string;
29
+ dirty: boolean;
30
+ source: ContentIdentity;
31
+ }
32
+
33
+ /** A container image resolved to the content identity Docker will execute. */
34
+ export interface RuntimeImageIdentity {
35
+ tag: string;
36
+ id: string;
37
+ repoDigests: string[];
38
+ }
39
+
40
+ /** Per-eval identity components. Every value is a content hash (or null). */
41
+ export interface EvalIdentityComponents {
42
+ /** Hash of the entire PROMPT.md file, frontmatter included. */
43
+ promptSource: string;
44
+ /** Hash of the prompt the agent actually receives (the verbatim body). */
45
+ effectivePrompt: string;
46
+ /** Tree hash of `local/` (secret-pattern files excluded), or null when absent. */
47
+ local: string | null;
48
+ /** Hash of EVAL.ts. Statically-included shared scorer sources join in phase 3. */
49
+ scorer: string;
50
+ scorerConfiguration?: string | null;
51
+ /** Hash of the canonicalized frontmatter metadata. */
52
+ metadata: string;
53
+ }
54
+
55
+ export interface EvalIdentity {
56
+ id: string;
57
+ metadata: EvalMetadata;
58
+ components: EvalIdentityComponents;
59
+ hash: string;
60
+ }
61
+
62
+ /** Per-experiment identity components — phase 2's comparison-group diff reads these. */
63
+ export interface ExperimentIdentityComponents {
64
+ /** Hash of the experiment's defining module source. */
65
+ source: string;
66
+ /** Agent identity fields, recorded verbatim (never hashed away). */
67
+ agent: {
68
+ adapter: string;
69
+ provider: string;
70
+ model: string;
71
+ reasoning: string;
72
+ cliVersion: string;
73
+ };
74
+ /** Hash of the canonicalized runtime configuration. */
75
+ runtime: string;
76
+ /** Resolved image identity for container runtimes; null for host. */
77
+ image: RuntimeImageIdentity | null;
78
+ /** The declared treatment, verbatim; null when the experiment declares none. */
79
+ treatment: string | null;
80
+ /** Harness source hash — an experiment is only comparable across equal harnesses. */
81
+ harness: string;
82
+ }
83
+
84
+ export interface ExperimentIdentity {
85
+ id: string;
86
+ components: ExperimentIdentityComponents;
87
+ hash: string;
88
+ }
89
+
90
+ export interface RunManifest {
91
+ manifestVersion: "v3" | typeof MANIFEST_VERSION;
92
+ /** v4: exact per-pair environment consumed by the runner. */
93
+ pairs?: Record<string, PairEnvironment>;
94
+ harness: HarnessIdentity;
95
+ evals: Record<string, EvalIdentity>;
96
+ experiments: Record<string, ExperimentIdentity>;
97
+ /**
98
+ * Every container image the run resolved, keyed by tag — the exact bits
99
+ * Docker executed. Kept at the run level because a `containerImage(metadata)`
100
+ * resolver maps one experiment to a different image per eval, which a single
101
+ * per-experiment component cannot hold (that component still fills for
102
+ * static `runtime.image` tags). Empty for host-only runs.
103
+ */
104
+ images: Record<string, RuntimeImageIdentity>;
105
+ hash: string;
106
+ }
107
+
108
+ function contentHash(value: unknown): string {
109
+ return hashString(typeof value === "string" ? value : canonicalJson(value));
110
+ }
111
+
112
+ function git(args: string[], cwd: string): string | null {
113
+ const result = Bun.spawnSync(["git", ...args], { cwd, stdout: "pipe", stderr: "ignore" });
114
+ return result.exitCode === 0 ? result.stdout.toString().trim() : null;
115
+ }
116
+
117
+ /** Embedded build info when compiled; git revision + src tree hash from a checkout. */
118
+ export function resolveHarnessIdentityV3(): HarnessIdentity {
119
+ if (EMBEDDED_BUILD_INFO.revision && EMBEDDED_BUILD_INFO.sourceHash && EMBEDDED_BUILD_INFO.dirty !== null) {
120
+ return {
121
+ version: EMBEDDED_BUILD_INFO.version,
122
+ revision: EMBEDDED_BUILD_INFO.revision,
123
+ dirty: EMBEDDED_BUILD_INFO.dirty,
124
+ source: { sha256: EMBEDDED_BUILD_INFO.sourceHash },
125
+ };
126
+ }
127
+ const root = resolve(import.meta.dir, "..");
128
+ const revision = git(["rev-parse", "HEAD"], root) ?? "unversioned";
129
+ const dirty = (git(["status", "--porcelain", "--untracked-files=all", "--", "src", "package.json", "bun.lock"], root) ?? "unknown") !== "";
130
+ return { version: EMBEDDED_BUILD_INFO.version, revision, dirty, source: hashTree(join(root, "src")) };
131
+ }
132
+
133
+ export interface BuildRunManifestOptions {
134
+ evals: LoadedEval[];
135
+ experiments: LoadedExperiment[];
136
+ /** Resolved image identities keyed by image tag (container runtimes only). */
137
+ images?: Record<string, RuntimeImageIdentity>;
138
+ /**
139
+ * CLI versions probed from each experiment's resolved image via the
140
+ * adapter's `versionCommand`, keyed by experiment id. A probed version
141
+ * beats the experiment's declared string — the executable in the image is
142
+ * the ground truth.
143
+ */
144
+ cliVersions?: Record<string, string>;
145
+ /** Injectable for deterministic tests; production callers omit it. */
146
+ harness?: HarnessIdentity;
147
+ pairs?: Record<string, PairEnvironment>;
148
+ scorerIdentities?: Record<string, Record<string, unknown>>;
149
+ }
150
+
151
+ /** Build before the first paid attempt; all reads are deterministic and local. */
152
+ export function buildRunManifest(options: BuildRunManifestOptions): RunManifest {
153
+ const harness = options.harness ?? resolveHarnessIdentityV3();
154
+
155
+ const evals: Record<string, EvalIdentity> = {};
156
+ for (const loaded of options.evals) {
157
+ const components: EvalIdentityComponents = {
158
+ promptSource: contentHash(readFileSync(loaded.promptPath, "utf8")),
159
+ effectivePrompt: contentHash(loaded.promptBody),
160
+ local: loaded.localDir === null ? null : hashTree(loaded.localDir).sha256,
161
+ scorer: sourceIdentity(loaded.scorerPath),
162
+ scorerConfiguration: options.scorerIdentities?.[loaded.metadata.id] ? contentHash(options.scorerIdentities[loaded.metadata.id]) : null,
163
+ metadata: contentHash(loaded.metadata),
164
+ };
165
+ evals[loaded.metadata.id] = {
166
+ id: loaded.metadata.id,
167
+ metadata: loaded.metadata,
168
+ components,
169
+ hash: contentHash(components),
170
+ };
171
+ }
172
+
173
+ const experiments: Record<string, ExperimentIdentity> = {};
174
+ for (const { experiment, path } of options.experiments) {
175
+ const imageTag = experiment.runtime.image;
176
+ const components: ExperimentIdentityComponents = {
177
+ source: sourceIdentity(path),
178
+ agent: {
179
+ adapter: experiment.agent.adapter,
180
+ provider: experiment.agent.provider ?? "unspecified",
181
+ model: experiment.agent.model ?? "unspecified",
182
+ reasoning: experiment.agent.reasoning ?? "unspecified",
183
+ cliVersion: options.cliVersions?.[experiment.id] ?? experiment.agent.cliVersion ?? "unspecified",
184
+ },
185
+ runtime: contentHash({
186
+ kind: experiment.runtime.kind,
187
+ capacity: experiment.runtime.capacity ?? null,
188
+ observationTargets: experiment.runtime.observationTargets ?? null,
189
+ treatmentPolicyFields: [...(experiment.runtime.treatmentPolicyFields ?? [])].sort(),
190
+ image: experiment.runtime.image ?? null,
191
+ command: experiment.runtime.command ?? null,
192
+ config: experiment.runtime.config ?? {},
193
+ }),
194
+ image: (imageTag !== undefined ? options.images?.[imageTag] : undefined) ?? null,
195
+ treatment: experiment.treatment ?? null,
196
+ harness: harness.source.sha256,
197
+ };
198
+ experiments[experiment.id] = {
199
+ id: experiment.id,
200
+ components,
201
+ hash: contentHash(components),
202
+ };
203
+ }
204
+
205
+ const withoutHash: Omit<RunManifest, "hash"> = {
206
+ manifestVersion: MANIFEST_VERSION,
207
+ pairs: options.pairs ?? resolvePairEnvironments(options),
208
+ harness,
209
+ evals,
210
+ experiments,
211
+ images: options.images ?? {},
212
+ };
213
+ return { ...withoutHash, hash: contentHash(withoutHash) };
214
+ }
215
+
216
+ /** The identity persisted on every attempt row. Trials share it by design. */
217
+ export function attemptIdentityHash(plan: AttemptPlan, manifest: RunManifest): string {
218
+ const evalIdentity = manifest.evals[plan.evalId];
219
+ if (!evalIdentity) throw new Error(`attemptIdentityHash: eval "${plan.evalId}" is not in the manifest`);
220
+ const experimentIdentity = manifest.experiments[plan.experimentId];
221
+ if (!experimentIdentity) throw new Error(`attemptIdentityHash: experiment "${plan.experimentId}" is not in the manifest`);
222
+ return contentHash({
223
+ manifestVersion: manifest.manifestVersion,
224
+ ...(manifest.manifestVersion === "v4" ? { environment: manifest.pairs?.[pairKey(plan.evalId, plan.experimentId)] } : {}),
225
+ eval: evalIdentity.hash,
226
+ experiment: experimentIdentity.hash,
227
+ });
228
+ }
@@ -0,0 +1,12 @@
1
+ /** Pure helpers for refusing model names whose target can move over time. */
2
+
3
+ const GENERIC_MOVING_ALIAS = /^(?:auto|default|latest|sonnet|opus|haiku|fable)$/i;
4
+ const SUFFIXED_LATEST = /-latest$/i;
5
+ // Anthropic kept these convenient dateless names as aliases to dated
6
+ // snapshots. 4.6+ and 5.x canonical names are stable model identifiers, so
7
+ // only the documented pre-4.6 dateless family belongs here.
8
+ const ANTHROPIC_PRE_46_DATELESS = /^claude-(?:sonnet|opus|haiku)-4-(?:0|1|5)$/i;
9
+
10
+ export function isMovingModelAlias(model: string): boolean {
11
+ return GENERIC_MOVING_ALIAS.test(model) || SUFFIXED_LATEST.test(model) || ANTHROPIC_PRE_46_DATELESS.test(model);
12
+ }
package/src/plan.ts ADDED
@@ -0,0 +1,55 @@
1
+ /**
2
+ * The v3 planner: the eval × experiment × trials cross-product, and nothing
3
+ * else. Planning is pure and deterministic — evals sorted by id, experiments
4
+ * by id, trials innermost. Concurrency and interleaving are the runner's
5
+ * concern, never the plan's identity; metadata filters apply to the eval
6
+ * list BEFORE planning and never appear in the plan.
7
+ */
8
+
9
+ import type { LoadedEval } from "./evals/types.ts";
10
+ import type { Experiment } from "./experiments/types.ts";
11
+
12
+ /** One planned attempt. Exactly these keys — axis creep is a bug. */
13
+ export interface AttemptPlan {
14
+ evalId: string;
15
+ experimentId: string;
16
+ /** 0..trials-1 */
17
+ trialIndex: number;
18
+ }
19
+
20
+ /** Metadata filters applied to the eval list before planning. */
21
+ export interface EvalFilter {
22
+ suites?: string[];
23
+ frameworks?: string[];
24
+ products?: string[];
25
+ }
26
+
27
+ /** Keep only evals matching every provided filter axis (OR within an axis). */
28
+ export function filterEvals(evals: LoadedEval[], filter: EvalFilter): LoadedEval[] {
29
+ return evals.filter((entry) => {
30
+ const { suite, framework, product } = entry.metadata;
31
+ if (filter.suites && filter.suites.length > 0 && !filter.suites.includes(suite)) return false;
32
+ if (filter.frameworks && filter.frameworks.length > 0 && (framework === undefined || !filter.frameworks.includes(framework))) return false;
33
+ if (filter.products && filter.products.length > 0 && (product === undefined || !filter.products.includes(product))) return false;
34
+ return true;
35
+ });
36
+ }
37
+
38
+ /** Expand the cross-product. Zero evals or experiments is an empty plan, not an error. */
39
+ export function planRun(evals: LoadedEval[], experiments: Experiment[], trials: number): AttemptPlan[] {
40
+ if (!Number.isInteger(trials) || trials < 1) {
41
+ throw new Error(`planRun: trials must be an integer >= 1, got ${trials}`);
42
+ }
43
+ const evalIds = evals.map((entry) => entry.metadata.id).sort((a, b) => a.localeCompare(b));
44
+ const experimentIds = experiments.map((entry) => entry.id).sort((a, b) => a.localeCompare(b));
45
+
46
+ const plan: AttemptPlan[] = [];
47
+ for (const evalId of evalIds) {
48
+ for (const experimentId of experimentIds) {
49
+ for (let trialIndex = 0; trialIndex < trials; trialIndex += 1) {
50
+ plan.push({ evalId, experimentId, trialIndex });
51
+ }
52
+ }
53
+ }
54
+ return plan;
55
+ }