@workos/quickstudy 0.6.0 → 0.7.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workos/quickstudy",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "An open-source, company-agnostic eval harness for measuring whether coding agents can complete real integration tasks, across an agent x feature x framework x surface matrix.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -9,7 +9,7 @@ export const EMBEDDED_BUILD_INFO: {
9
9
  // (the `x-release-please-version` marker anchors the replacement). It is
10
10
  // restored as-is after a binary build (scripts/build-binary.ts), so the
11
11
  // committed source `--version` stays in lockstep with package.json.
12
- version: "0.6.0", // x-release-please-version
12
+ version: "0.7.0", // x-release-please-version
13
13
  revision: null,
14
14
  dirty: null,
15
15
  sourceHash: null,
@@ -17,7 +17,7 @@ export function renderDiagnosisText(diagnosis: DiagnosisJson, resultsDir: string
17
17
  lines.push(`diagnosis: run ${diagnosis.run_id}`);
18
18
  lines.push(` model: ${diagnosis.model.requested}${diagnosis.model.resolved !== null ? ` (resolved: ${diagnosis.model.resolved})` : ""}`);
19
19
  lines.push(
20
- ` pairs: ${diagnosis.pairs_diagnosed} diagnosed, ${diagnosis.pairs_skipped} skipped (all passing)` +
20
+ ` pairs: ${diagnosis.pairs_diagnosed} diagnosed, ${diagnosis.pairs_skipped} skipped (passing or control)` +
21
21
  (diagnosis.errors.length > 0 ? `, ${diagnosis.errors.length} errored` : ""),
22
22
  );
23
23
 
@@ -30,6 +30,7 @@ import {
30
30
  type PairBundle,
31
31
  } from "./prompt.ts";
32
32
  import { selectDiagnosablePairs, type DiagnosablePair } from "./select.ts";
33
+ import { controlExperiments } from "../report/report.ts";
33
34
  import {
34
35
  DIAGNOSIS_DISCLAIMER,
35
36
  DIAGNOSIS_PROMPT_VERSION,
@@ -218,10 +219,13 @@ export async function runDiagnose(options: RunDiagnoseOptions): Promise<Diagnosi
218
219
 
219
220
  const run = mustResolveRun(store, options.runId);
220
221
  const attempts = store.listAttempts(run.id);
221
- const selection = selectDiagnosablePairs(attempts);
222
+ // Controls check the instrument, not an agent: the oracle passes and the
223
+ // no-op fails by construction, so their pairs carry no diagnosable cause.
224
+ const controls = controlExperiments(run);
225
+ const selection = selectDiagnosablePairs(attempts, { controls });
222
226
  log(
223
227
  `diagnose: run ${run.id} — ${selection.pairs.length} diagnosable pair(s), ` +
224
- `${selection.skipped} passing pair(s) skipped`,
228
+ `${selection.skipped} passing pair(s) skipped, ${selection.controls} control pair(s) excluded`,
225
229
  );
226
230
 
227
231
  const findings: DiagnosisFinding[] = [];
@@ -281,7 +285,7 @@ export async function runDiagnose(options: RunDiagnoseOptions): Promise<Diagnosi
281
285
  prompt_version: DIAGNOSIS_PROMPT_VERSION,
282
286
  disclaimer: DIAGNOSIS_DISCLAIMER,
283
287
  pairs_diagnosed: selection.pairs.length - errors.length,
284
- pairs_skipped: selection.skipped,
288
+ pairs_skipped: selection.skipped + selection.controls,
285
289
  findings,
286
290
  errors,
287
291
  };
@@ -47,6 +47,19 @@ export interface PairSelection {
47
47
  pairs: DiagnosablePair[];
48
48
  /** Passing-only pairs — skipped, never sent to the model. */
49
49
  skipped: number;
50
+ /**
51
+ * Control pairs (the positive-control oracle, the negative-control no-op) —
52
+ * excluded before classification, never sent to the model. A control's
53
+ * outcome is fixed by construction: asked why the no-op "failed", a model
54
+ * confidently reports a broken harness. The report's scorer health watches
55
+ * the oracle instead.
56
+ */
57
+ controls: number;
58
+ }
59
+
60
+ export interface SelectOptions {
61
+ /** Experiment ids the run recorded as controls (RunConfig.roles other than "agent"). */
62
+ controls?: ReadonlySet<string>;
50
63
  }
51
64
 
52
65
  /** Bundle ordering: failed first, then errors, then incomplete. */
@@ -58,10 +71,11 @@ const CLASS_ORDER: Record<Exclude<AttemptClass, "passed">, number> = {
58
71
 
59
72
  /**
60
73
  * Group attempts into pairs, classify, and select each diagnosable pair's
61
- * bundle. Deterministic: pairs sort by eval id then experiment id (the
62
- * report's order); within a class, attempts sort by ULID (id) order.
74
+ * bundle. Control experiments are excluded first (see PairSelection.controls).
75
+ * Deterministic: pairs sort by eval id then experiment id (the report's
76
+ * order); within a class, attempts sort by ULID (id) order.
63
77
  */
64
- export function selectDiagnosablePairs(attempts: AttemptRecord[]): PairSelection {
78
+ export function selectDiagnosablePairs(attempts: AttemptRecord[], options: SelectOptions = {}): PairSelection {
65
79
  const byPair = new Map<string, AttemptRecord[]>();
66
80
  for (const attempt of attempts) {
67
81
  const key = `${attempt.evalId}|${attempt.experimentId}`;
@@ -72,8 +86,13 @@ export function selectDiagnosablePairs(attempts: AttemptRecord[]): PairSelection
72
86
 
73
87
  const pairs: DiagnosablePair[] = [];
74
88
  let skipped = 0;
89
+ let controls = 0;
75
90
 
76
91
  for (const group of byPair.values()) {
92
+ if (options.controls?.has(group[0]!.experimentId)) {
93
+ controls += 1;
94
+ continue;
95
+ }
77
96
  const report = collectPairs(group)[0]!;
78
97
  if (report.failed + report.errors + report.incomplete === 0) {
79
98
  skipped += 1;
@@ -106,5 +125,5 @@ export function selectDiagnosablePairs(attempts: AttemptRecord[]): PairSelection
106
125
  }
107
126
 
108
127
  pairs.sort((a, b) => a.evalId.localeCompare(b.evalId) || a.experimentId.localeCompare(b.experimentId));
109
- return { pairs, skipped };
128
+ return { pairs, skipped, controls };
110
129
  }
@@ -85,6 +85,7 @@ export interface DiagnosisJson {
85
85
  /** Fixed self-labeling: hypotheses with evidence links, not verdicts; single-sweep scope. */
86
86
  disclaimer: string;
87
87
  pairs_diagnosed: number;
88
+ /** Pairs never sent to the model: passing-only pairs and control (oracle / no-op) pairs. */
88
89
  pairs_skipped: number;
89
90
  findings: DiagnosisFinding[];
90
91
  errors: PairDiagnosisError[];
@@ -534,13 +534,28 @@ function dialUpstream(client: Socket, host: string, port: number, ready: (upstre
534
534
  });
535
535
  }
536
536
 
537
- /** Bidirectional tunnel wiring shared by CONNECT and plain forwards. */
537
+ /**
538
+ * Bidirectional tunnel wiring shared by CONNECT and plain forwards.
539
+ *
540
+ * \`pipe()\` already ends the destination once the source ends — after the
541
+ * buffered bytes have flushed. Destroying the client on the upstream's
542
+ * \`close\` threw that buffer away: an upstream that closes right after a
543
+ * large response (a registry tarball) left the client with a truncated body
544
+ * and an ECONNRESET, so \`npm install\` through the proxy failed at random.
545
+ * So the upstream's close only ENDS the client (flush, then FIN). The other
546
+ * direction stays a destroy: a client that is gone will never read what the
547
+ * upstream still has to say, and a merely ended upstream would sit paused
548
+ * and half-open until the origin gave up.
549
+ */
538
550
  function pipeBoth(client: Socket, upstream: Socket): void {
539
551
  client.pipe(upstream);
540
552
  upstream.pipe(client);
541
- client.on("close", () => upstream.destroy());
542
- upstream.on("close", () => client.destroy());
543
553
  client.on("error", () => upstream.destroy());
554
+ upstream.on("error", () => client.destroy());
555
+ client.on("close", () => upstream.destroy());
556
+ upstream.on("close", () => {
557
+ if (!client.writableEnded) client.end();
558
+ });
544
559
  }
545
560
 
546
561
  /** CONNECT host:port — the HTTPS path. The tunnel is opaque to the proxy. */
@@ -252,13 +252,28 @@ function dialUpstream(client: Socket, host: string, port: number, ready: (upstre
252
252
  });
253
253
  }
254
254
 
255
- /** Bidirectional tunnel wiring shared by CONNECT and plain forwards. */
255
+ /**
256
+ * Bidirectional tunnel wiring shared by CONNECT and plain forwards.
257
+ *
258
+ * `pipe()` already ends the destination once the source ends — after the
259
+ * buffered bytes have flushed. Destroying the client on the upstream's
260
+ * `close` threw that buffer away: an upstream that closes right after a
261
+ * large response (a registry tarball) left the client with a truncated body
262
+ * and an ECONNRESET, so `npm install` through the proxy failed at random.
263
+ * So the upstream's close only ENDS the client (flush, then FIN). The other
264
+ * direction stays a destroy: a client that is gone will never read what the
265
+ * upstream still has to say, and a merely ended upstream would sit paused
266
+ * and half-open until the origin gave up.
267
+ */
256
268
  function pipeBoth(client: Socket, upstream: Socket): void {
257
269
  client.pipe(upstream);
258
270
  upstream.pipe(client);
259
- client.on("close", () => upstream.destroy());
260
- upstream.on("close", () => client.destroy());
261
271
  client.on("error", () => upstream.destroy());
272
+ upstream.on("error", () => client.destroy());
273
+ client.on("close", () => upstream.destroy());
274
+ upstream.on("close", () => {
275
+ if (!client.writableEnded) client.end();
276
+ });
262
277
  }
263
278
 
264
279
  /** CONNECT host:port — the HTTPS path. The tunnel is opaque to the proxy. */
@@ -454,7 +454,7 @@ function roleOf(run: RunRecord, experimentId: string): ExperimentRole {
454
454
  }
455
455
 
456
456
  /** The run's control experiments, from the roles it recorded. */
457
- function controlExperiments(run: RunRecord): Set<string> {
457
+ export function controlExperiments(run: RunRecord): Set<string> {
458
458
  return new Set(run.config.experimentIds.filter((id) => roleOf(run, id) !== "agent"));
459
459
  }
460
460