@workos/quickstudy 0.3.0 → 0.5.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/src/store/db.ts CHANGED
@@ -18,8 +18,29 @@ import { SCHEMA_SQL } from "./schema.ts";
18
18
 
19
19
  export type AttemptStatus = "completed" | "incomplete" | "error";
20
20
 
21
+ /** Which stage failed when an attempt's status is "error". */
22
+ export const ERROR_KINDS = ["environment", "agent", "scorer", "harness"] as const;
23
+ export type ErrorKind = (typeof ERROR_KINDS)[number];
24
+ /** Which limit an attempt hit; the tree it left behind was still graded. */
25
+ export type LimitKind = "timeout";
26
+
27
+ function toErrorKind(value: string | null): ErrorKind | null {
28
+ return value !== null && (ERROR_KINDS as readonly string[]).includes(value) ? (value as ErrorKind) : null;
29
+ }
30
+
31
+ function toLimitKind(value: string | null): LimitKind | null {
32
+ return value === "timeout" ? value : null;
33
+ }
34
+
21
35
  /** The run-level configuration recorded in `runs.config_json`. */
22
36
  export interface RunConfig {
37
+ /**
38
+ * Each experiment's role, from the adapter that actually ran it (a scripted
39
+ * container command is always "agent"). Recorded here — NOT in the manifest —
40
+ * like `groups`, so it never perturbs run identity. Absent on older runs: every
41
+ * experiment then reads as "agent".
42
+ */
43
+ roles?: Record<string, import("../adapters/types.ts").AdapterRole>;
23
44
  scheduler?: { concurrency: number; seed: string; policy: "seeded-hash-v1"; order: import("../plan.ts").AttemptPlan[] };
24
45
  trials: number;
25
46
  evalIds: string[];
@@ -82,6 +103,13 @@ export interface NewAttempt {
82
103
  startedAt?: number | null;
83
104
  finishedAt?: number | null;
84
105
  error?: string | null;
106
+ /** "timeout" = the agent hit its wall-clock limit and the tree it left was graded (column limit_hit). */
107
+ limitHit?: LimitKind | null;
108
+ /** Which stage failed; set only when status = "error". */
109
+ errorKind?: ErrorKind | null;
110
+ /** Agent phase only, epoch ms; null = not recorded. */
111
+ agentStartedAt?: number | null;
112
+ agentFinishedAt?: number | null;
85
113
  }
86
114
 
87
115
  export interface AttemptRecord {
@@ -106,6 +134,13 @@ export interface AttemptRecord {
106
134
  startedAt: number | null;
107
135
  finishedAt: number | null;
108
136
  error: string | null;
137
+ /** "timeout" = the agent hit its wall-clock limit and the tree it left was graded (column limit_hit). */
138
+ limitHit?: LimitKind | null;
139
+ /** Which stage failed; set only when status = "error". */
140
+ errorKind?: ErrorKind | null;
141
+ /** Agent phase only, epoch ms; null = not recorded. */
142
+ agentStartedAt?: number | null;
143
+ agentFinishedAt?: number | null;
109
144
  }
110
145
 
111
146
  interface RunRow {
@@ -139,6 +174,10 @@ interface AttemptRow {
139
174
  started_at: number | null;
140
175
  finished_at: number | null;
141
176
  error: string | null;
177
+ limit_hit: string | null;
178
+ error_kind: string | null;
179
+ agent_started_at: number | null;
180
+ agent_finished_at: number | null;
142
181
  }
143
182
 
144
183
  function toRunRecord(row: RunRow): RunRecord {
@@ -175,6 +214,10 @@ function toAttemptRecord(row: AttemptRow): AttemptRecord {
175
214
  startedAt: row.started_at,
176
215
  finishedAt: row.finished_at,
177
216
  error: row.error,
217
+ limitHit: toLimitKind(row.limit_hit ?? null),
218
+ errorKind: toErrorKind(row.error_kind ?? null),
219
+ agentStartedAt: row.agent_started_at ?? null,
220
+ agentFinishedAt: row.agent_finished_at ?? null,
178
221
  };
179
222
  }
180
223
 
@@ -226,6 +269,21 @@ export class ResultsStore {
226
269
  }
227
270
  if (!attemptColumns.includes("judge_usage_json")) this.db.exec("ALTER TABLE attempts ADD COLUMN judge_usage_json TEXT;");
228
271
  if (!attemptColumns.includes("retry_of")) this.db.exec("ALTER TABLE attempts ADD COLUMN retry_of TEXT;");
272
+ // Outcome taxonomy + agent-phase timing (nullable; NULL = unavailable).
273
+ for (const [name, type] of [
274
+ ["limit_hit", "TEXT"],
275
+ ["error_kind", "TEXT"],
276
+ ["agent_started_at", "INTEGER"],
277
+ ["agent_finished_at", "INTEGER"],
278
+ ] as const) {
279
+ if (attemptColumns.includes(name)) continue;
280
+ try {
281
+ this.db.exec(`ALTER TABLE attempts ADD COLUMN ${name} ${type};`);
282
+ } catch (err) {
283
+ // A concurrent opener may have added it between the read and the ALTER.
284
+ if (!/duplicate column/i.test(err instanceof Error ? err.message : String(err))) throw err;
285
+ }
286
+ }
229
287
  this.db.exec("PRAGMA user_version = 3;");
230
288
  }
231
289
 
@@ -264,8 +322,9 @@ export class ResultsStore {
264
322
  `INSERT INTO attempts (
265
323
  id, run_id, eval_id, experiment_id, suite, trial_index, attempt_identity_hash, status,
266
324
  result_json, surface_usage_json, transcript_ref, diff_ref, cost_usd, tokens_in, tokens_out,
267
- turns, started_at, finished_at, error, retry_of, judge_usage_json
268
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
325
+ turns, started_at, finished_at, error, retry_of, judge_usage_json,
326
+ limit_hit, error_kind, agent_started_at, agent_finished_at
327
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
269
328
  )
270
329
  .run(
271
330
  attempt.id,
@@ -289,6 +348,10 @@ export class ResultsStore {
289
348
  attempt.error ?? null,
290
349
  attempt.retryOf ?? null,
291
350
  attempt.judgeRecords ? JSON.stringify(attempt.judgeRecords) : null,
351
+ attempt.limitHit ?? null,
352
+ attempt.errorKind ?? null,
353
+ attempt.agentStartedAt ?? null,
354
+ attempt.agentFinishedAt ?? null,
292
355
  );
293
356
  }
294
357
 
@@ -32,7 +32,13 @@ CREATE TABLE IF NOT EXISTS attempts (
32
32
  tokens_in INTEGER, tokens_out INTEGER,
33
33
  turns INTEGER, -- agent steps; NULL = no adapter stream (scripted commands)
34
34
  started_at INTEGER, finished_at INTEGER,
35
- error TEXT -- only when status = 'error'
35
+ error TEXT, -- only when status = 'error'
36
+ -- Outcome taxonomy + agent-phase timing. NULL = unavailable / not applicable
37
+ -- (rows written before these columns existed read NULL; never backfilled).
38
+ limit_hit TEXT, -- 'timeout' when the agent hit its wall-clock limit and the tree it left was graded
39
+ error_kind TEXT, -- 'environment' | 'agent' | 'scorer' | 'harness' (status = 'error' only)
40
+ agent_started_at INTEGER, -- epoch ms; agent phase only
41
+ agent_finished_at INTEGER -- epoch ms; agent phase only
36
42
  );
37
43
 
38
44
  CREATE INDEX IF NOT EXISTS idx_attempts_run_pair ON attempts(run_id, eval_id, experiment_id);