@stackbone/cli 0.2.8 → 0.3.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/CHANGELOG.md +71 -0
- package/README.md +1 -1
- package/main.js +26442 -9861
- package/migrations/0028_browser_provider_system_secrets.sql +39 -0
- package/migrations/0029_workspace_settings.sql +37 -0
- package/migrations/0030_run_steps_connection_hook.sql +27 -0
- package/migrations/0031_recurring_job_executions.sql +51 -0
- package/migrations/0032_run_steps_external_step_id.sql +48 -0
- package/migrations/0033_runtime_identity.sql +25 -0
- package/migrations/0034_guardrails.sql +92 -0
- package/migrations/0035_run_steps_guardrail.sql +41 -0
- package/migrations/0036_guardrail_attachment_units.sql +61 -0
- package/migrations/0037_runs_entity_kind.sql +45 -0
- package/migrations/0038_evals.sql +272 -0
- package/migrations/0039_eval_case_results_idempotency.sql +22 -0
- package/migrations/0040_eval_case_results_run_idx.sql +27 -0
- package/migrations/0041_eval_criterion_workflow_name.sql +68 -0
- package/migrations/0042_eval_case_results_ended.sql +64 -0
- package/migrations/0043_eval_suite_approval_policy.sql +50 -0
- package/migrations/0044_eval_persona_simulator.sql +88 -0
- package/migrations/0045_eval_case_description.sql +34 -0
- package/package.json +1 -1
- package/runtime-deps/@stackbone/workflow-world/index.js +71 -71
- package/stackbone-cli-0.3.0.tgz +0 -0
- package/stackbone-cli-0.2.8.tgz +0 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
-- 0038_evals.sql — EVALS in the box (STA-408, PRD STA-393): the six tables that
|
|
2
|
+
-- turn "is the agent any good?" from an opinion into a number, plus the `eval`
|
|
3
|
+
-- run trigger.
|
|
4
|
+
--
|
|
5
|
+
-- Widening a named CHECK REPLACES IT WHOLESALE. `runs_trigger_check` was
|
|
6
|
+
-- re-listed by hand in 0001 (manual/webhook/cron/event), 0016 (+job) and 0017
|
|
7
|
+
-- (+workflow/chat); a migration that only remembers its own value silently
|
|
8
|
+
-- revokes the rest, and nothing conflicts because they are different files. All
|
|
9
|
+
-- eight are re-listed below, and `evals-migration.spec.ts` now pins the complete
|
|
10
|
+
-- set so the next widening breaks loudly.
|
|
11
|
+
ALTER TABLE stackbone_platform.runs DROP CONSTRAINT IF EXISTS runs_trigger_check;
|
|
12
|
+
ALTER TABLE stackbone_platform.runs
|
|
13
|
+
ADD CONSTRAINT runs_trigger_check
|
|
14
|
+
CHECK (trigger IN (
|
|
15
|
+
'manual', 'webhook', 'cron', 'event', 'job', 'workflow', 'chat', 'eval'
|
|
16
|
+
));
|
|
17
|
+
|
|
18
|
+
-- --------------------------------------------------------------------------
|
|
19
|
+
-- The case list: a header row plus one immutable version per edit.
|
|
20
|
+
-- --------------------------------------------------------------------------
|
|
21
|
+
--
|
|
22
|
+
-- Same shape as prompts (0013), and for the same reason: a run has to keep
|
|
23
|
+
-- saying WHICH cases it measured. Editing the list copies its cases into a new
|
|
24
|
+
-- version, so a run from last month still reads its exact cases even after three
|
|
25
|
+
-- edits. The copy costs one INSERT per case; for the expected size — tens or
|
|
26
|
+
-- hundreds of cases per list — that is irrelevant next to losing the ability to
|
|
27
|
+
-- compare two runs at all.
|
|
28
|
+
--
|
|
29
|
+
-- Both header tables carry `deleted_at` and are SOFT-deleted by the store,
|
|
30
|
+
-- because a run points at a dataset version (and a suite) and a sealed run that
|
|
31
|
+
-- lost its subject is not evidence of anything. The uniqueness of the operator's
|
|
32
|
+
-- key therefore has to be PARTIAL — a full UNIQUE would let a deleted list squat
|
|
33
|
+
-- its name forever, so an operator who removes "smoke tests" could never make
|
|
34
|
+
-- another one.
|
|
35
|
+
|
|
36
|
+
CREATE TABLE IF NOT EXISTS stackbone_platform.eval_datasets (
|
|
37
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
38
|
+
-- Operator-facing handle, unique among the ACTIVE lists (index below).
|
|
39
|
+
key text NOT NULL,
|
|
40
|
+
name text NOT NULL,
|
|
41
|
+
description text,
|
|
42
|
+
-- Points at the live row in `eval_dataset_versions` for this list.
|
|
43
|
+
current_version integer NOT NULL DEFAULT 1 CHECK (current_version >= 1),
|
|
44
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
45
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
46
|
+
-- The acting user, as the verified JWT / signed actor header reported them.
|
|
47
|
+
-- NULL on the un-authenticated `stackbone dev` datapath.
|
|
48
|
+
created_by text,
|
|
49
|
+
updated_by text,
|
|
50
|
+
-- Soft delete. Reads filter on it; the row survives so the runs that
|
|
51
|
+
-- measured against it keep their subject.
|
|
52
|
+
deleted_at timestamptz
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
-- Unique among ACTIVE lists only: see the note above on squatting.
|
|
56
|
+
CREATE UNIQUE INDEX IF NOT EXISTS eval_datasets_active_key_idx
|
|
57
|
+
ON stackbone_platform.eval_datasets (key)
|
|
58
|
+
WHERE deleted_at IS NULL;
|
|
59
|
+
|
|
60
|
+
CREATE TABLE IF NOT EXISTS stackbone_platform.eval_dataset_versions (
|
|
61
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
62
|
+
dataset_id uuid NOT NULL
|
|
63
|
+
REFERENCES stackbone_platform.eval_datasets (id) ON DELETE CASCADE,
|
|
64
|
+
version integer NOT NULL CHECK (version >= 1),
|
|
65
|
+
-- Free text an operator can leave describing what this edit changed.
|
|
66
|
+
note text,
|
|
67
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
68
|
+
created_by text
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
-- The race backstop. The store assigns MAX(version) + 1 under a per-dataset
|
|
72
|
+
-- advisory lock inside the same transaction, exactly like prompts; this index is
|
|
73
|
+
-- what stops two writers both landing version 1 if that lock is ever lost.
|
|
74
|
+
CREATE UNIQUE INDEX IF NOT EXISTS eval_dataset_versions_version_idx
|
|
75
|
+
ON stackbone_platform.eval_dataset_versions (dataset_id, version);
|
|
76
|
+
|
|
77
|
+
-- Drives the version-history list (newest first).
|
|
78
|
+
CREATE INDEX IF NOT EXISTS eval_dataset_versions_version_desc_idx
|
|
79
|
+
ON stackbone_platform.eval_dataset_versions (dataset_id, version DESC);
|
|
80
|
+
|
|
81
|
+
-- A case belongs to ONE version. Editing the list copies the cases forward.
|
|
82
|
+
CREATE TABLE IF NOT EXISTS stackbone_platform.eval_cases (
|
|
83
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
84
|
+
dataset_version_id uuid NOT NULL
|
|
85
|
+
REFERENCES stackbone_platform.eval_dataset_versions (id) ON DELETE CASCADE,
|
|
86
|
+
name text NOT NULL,
|
|
87
|
+
input jsonb NOT NULL,
|
|
88
|
+
-- Optional on purpose: most cases are judged by a rubric, and demanding a
|
|
89
|
+
-- reference answer would force the operator to invent one.
|
|
90
|
+
expected jsonb,
|
|
91
|
+
tags text[] NOT NULL DEFAULT '{}',
|
|
92
|
+
-- Which run this case was captured from, if it was captured. Deliberately a
|
|
93
|
+
-- BARE uuid with NO foreign key: a captured case has to outlive run
|
|
94
|
+
-- retention, and a case that vanished when its source run was pruned would
|
|
95
|
+
-- silently shrink the list an operator built. Do not "fix" this into an FK.
|
|
96
|
+
source_run_id uuid,
|
|
97
|
+
-- Explicit order within the version, so the list reads the way it was built.
|
|
98
|
+
position integer NOT NULL
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
CREATE INDEX IF NOT EXISTS eval_cases_version_position_idx
|
|
102
|
+
ON stackbone_platform.eval_cases (dataset_version_id, position);
|
|
103
|
+
|
|
104
|
+
-- --------------------------------------------------------------------------
|
|
105
|
+
-- The suite, its runs, and the per-case verdicts.
|
|
106
|
+
-- --------------------------------------------------------------------------
|
|
107
|
+
--
|
|
108
|
+
-- Two conventions inherited from guardrails (0034), and they are the reason this
|
|
109
|
+
-- table looks under-constrained:
|
|
110
|
+
--
|
|
111
|
+
-- 1. The DISCRIMINATOR columns — `target_kind`, `status` — carry NO value
|
|
112
|
+
-- CHECK. A box running an older build has to be able to STORE a kind or a
|
|
113
|
+
-- state it cannot execute rather than reject the write and lose the
|
|
114
|
+
-- operator's configuration. An unknown value fails CLOSED at evaluation
|
|
115
|
+
-- time, which is the safe place for that decision.
|
|
116
|
+
-- 2. No PAYLOAD default lives here. The documented defaults live in
|
|
117
|
+
-- `@stackbone/validators` → `studio-evals.ts`, so the runtime, the facade
|
|
118
|
+
-- and the Studio form all read one source of truth. The defaults below are
|
|
119
|
+
-- structural only: identity, timestamps, the progress counters whose
|
|
120
|
+
-- absence would make a half-written run row unreadable, and `repeats`,
|
|
121
|
+
-- whose 1 is the neutral value (run each case once) rather than a
|
|
122
|
+
-- configuration choice.
|
|
123
|
+
--
|
|
124
|
+
-- `scorers` and `variants` are free-form jsonb validated against the CATALOG at
|
|
125
|
+
-- the handler, never by the DDL. A CHECK here would be a second description of
|
|
126
|
+
-- each scorer that drifts from the one the screen renders its form from. Neither
|
|
127
|
+
-- gets an empty-array default: a suite that measures nothing, and a run with no
|
|
128
|
+
-- variant, are not rows worth making writable.
|
|
129
|
+
|
|
130
|
+
CREATE TABLE IF NOT EXISTS stackbone_platform.eval_suites (
|
|
131
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
132
|
+
-- Operator-facing name, unique among the ACTIVE suites (index below), and
|
|
133
|
+
-- what `stackbone eval <suite>` is given on the command line.
|
|
134
|
+
name text NOT NULL,
|
|
135
|
+
description text,
|
|
136
|
+
-- `agent` | `workflow`. Deliberately unconstrained — see note 1 above.
|
|
137
|
+
target_kind text NOT NULL,
|
|
138
|
+
-- The agent or workflow name, resolved against the catalog at launch.
|
|
139
|
+
target_name text NOT NULL,
|
|
140
|
+
dataset_id uuid NOT NULL REFERENCES stackbone_platform.eval_datasets (id),
|
|
141
|
+
-- A pinned version number, or NULL for "the latest". NULL by default is what
|
|
142
|
+
-- the operator wants: the case captured yesterday joins today's run.
|
|
143
|
+
dataset_pin integer,
|
|
144
|
+
-- A list of { type, params }, validated against the scorer catalog.
|
|
145
|
+
scorers jsonb NOT NULL,
|
|
146
|
+
-- The pass mark. Left without a recommended value on purpose: inventing one
|
|
147
|
+
-- before there is data from a real suite would be a guess dressed as advice.
|
|
148
|
+
threshold numeric NOT NULL,
|
|
149
|
+
-- How many times each case runs. More than one is how a case that only
|
|
150
|
+
-- passes half the time stops sneaking through as a pass.
|
|
151
|
+
repeats integer NOT NULL DEFAULT 1 CHECK (repeats >= 1),
|
|
152
|
+
-- The spend brake. Every case costs real tokens out of the customer's own
|
|
153
|
+
-- budget, so a run that would exceed this truncates and SAYS SO.
|
|
154
|
+
max_cases integer NOT NULL CHECK (max_cases >= 1),
|
|
155
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
156
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
157
|
+
created_by text,
|
|
158
|
+
updated_by text,
|
|
159
|
+
deleted_at timestamptz
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
-- Unique among ACTIVE suites only, for the same anti-squatting reason as the
|
|
163
|
+
-- dataset key above.
|
|
164
|
+
CREATE UNIQUE INDEX IF NOT EXISTS eval_suites_active_name_idx
|
|
165
|
+
ON stackbone_platform.eval_suites (name)
|
|
166
|
+
WHERE deleted_at IS NULL;
|
|
167
|
+
|
|
168
|
+
CREATE INDEX IF NOT EXISTS eval_suites_active_dataset_idx
|
|
169
|
+
ON stackbone_platform.eval_suites (dataset_id)
|
|
170
|
+
WHERE deleted_at IS NULL;
|
|
171
|
+
|
|
172
|
+
CREATE TABLE IF NOT EXISTS stackbone_platform.eval_runs (
|
|
173
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
174
|
+
suite_id uuid NOT NULL
|
|
175
|
+
REFERENCES stackbone_platform.eval_suites (id) ON DELETE CASCADE,
|
|
176
|
+
-- The EXACT version measured, never "whatever is current now". Without this
|
|
177
|
+
-- pin, "this run scored 90 and last month's scored 70" is not a comparison.
|
|
178
|
+
dataset_version_id uuid NOT NULL
|
|
179
|
+
REFERENCES stackbone_platform.eval_dataset_versions (id),
|
|
180
|
+
-- Version seal: what was actually evaluated, so an improvement can be
|
|
181
|
+
-- attributed to a change instead of guessed at.
|
|
182
|
+
git_sha text,
|
|
183
|
+
agent_version text,
|
|
184
|
+
-- One or two variants, each with its configuration and its SEALED
|
|
185
|
+
-- aggregates. Sealed at close so opening the comparison screen does not
|
|
186
|
+
-- recompute — and so the numbers cannot drift under a reader.
|
|
187
|
+
variants jsonb NOT NULL,
|
|
188
|
+
-- `running` | `done` | `failed` | `cancelled`. Unconstrained — see note 1.
|
|
189
|
+
-- Also the CANCEL channel: the engine checks this row between cases and
|
|
190
|
+
-- aborts clean, sealing what it already measured. There is no live run
|
|
191
|
+
-- cancellation in the platform to lean on.
|
|
192
|
+
status text NOT NULL,
|
|
193
|
+
-- DISTINCT CASES this run meant to measure, after the spend brake. Not
|
|
194
|
+
-- evaluations: `truncation_notice` reads this number as cases ("stopped this
|
|
195
|
+
-- run at 5 cases"), and the plan LIMITs by it.
|
|
196
|
+
case_count integer NOT NULL,
|
|
197
|
+
-- A run that measured fewer cases than the suite asked for has to say so,
|
|
198
|
+
-- not present partial coverage as complete.
|
|
199
|
+
truncated boolean NOT NULL DEFAULT false,
|
|
200
|
+
-- How many times each case ran, SEALED from the suite at launch beside the
|
|
201
|
+
-- dataset version and the variants — and for the same reason: editing the
|
|
202
|
+
-- suite next week must not change what an old run says it measured. It is
|
|
203
|
+
-- also the missing factor in the progress denominator (cases × repeats ×
|
|
204
|
+
-- variants); without it on the row, a screen counting attempts against
|
|
205
|
+
-- cases reports a run of 30 evaluations as finished after 5.
|
|
206
|
+
repeats integer NOT NULL DEFAULT 1 CHECK (repeats >= 1),
|
|
207
|
+
-- Progress, counted in EVALUATIONS (one row of `eval_case_results` per case
|
|
208
|
+
-- × variant × repeat), polled by the screen while the run is in flight.
|
|
209
|
+
completed_count integer NOT NULL DEFAULT 0,
|
|
210
|
+
passed_count integer NOT NULL DEFAULT 0,
|
|
211
|
+
-- Sealed aggregates: NULL until the run closes.
|
|
212
|
+
score_avg numeric,
|
|
213
|
+
input_tokens integer,
|
|
214
|
+
output_tokens integer,
|
|
215
|
+
p95_duration_ms integer,
|
|
216
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
217
|
+
created_by text,
|
|
218
|
+
started_at timestamptz,
|
|
219
|
+
finished_at timestamptz
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
-- Drives both the suite's run history and the "diff against the previous run"
|
|
223
|
+
-- read. NULLS LAST keeps a queued run (no `started_at` yet) off the top.
|
|
224
|
+
CREATE INDEX IF NOT EXISTS eval_runs_suite_started_idx
|
|
225
|
+
ON stackbone_platform.eval_runs (suite_id, started_at DESC NULLS LAST);
|
|
226
|
+
|
|
227
|
+
CREATE TABLE IF NOT EXISTS stackbone_platform.eval_case_results (
|
|
228
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
229
|
+
eval_run_id uuid NOT NULL
|
|
230
|
+
REFERENCES stackbone_platform.eval_runs (id) ON DELETE CASCADE,
|
|
231
|
+
-- BARE uuid with NO foreign key, beside a denormalised `case_name`: the case
|
|
232
|
+
-- row dies with its dataset version, and the verdict has to stay readable
|
|
233
|
+
-- afterwards. Do not "fix" this into an FK.
|
|
234
|
+
case_id uuid,
|
|
235
|
+
case_name text NOT NULL,
|
|
236
|
+
-- Which of the (at most two) variants produced this verdict.
|
|
237
|
+
variant_label text NOT NULL,
|
|
238
|
+
attempt integer NOT NULL DEFAULT 1 CHECK (attempt >= 1),
|
|
239
|
+
-- The hinge: from a failed case, jump to its full trace. ON DELETE SET NULL
|
|
240
|
+
-- and not CASCADE — when run retention eventually prunes the run, the
|
|
241
|
+
-- VERDICT survives and only the explanation is lost. The score is the
|
|
242
|
+
-- evidence; the trace is the commentary.
|
|
243
|
+
run_id uuid REFERENCES stackbone_platform.runs (id) ON DELETE SET NULL,
|
|
244
|
+
passed boolean NOT NULL,
|
|
245
|
+
score numeric,
|
|
246
|
+
-- One entry per scorer, so a failure names WHICH criterion rejected it.
|
|
247
|
+
scores jsonb,
|
|
248
|
+
output jsonb,
|
|
249
|
+
duration_ms integer,
|
|
250
|
+
-- Structured failure: a case killed by a technical error counts as a failure
|
|
251
|
+
-- without taking the run down, and a case stopped by a guardrail or parked
|
|
252
|
+
-- on a human approval is distinguishable from one that simply answered badly.
|
|
253
|
+
error jsonb,
|
|
254
|
+
created_at timestamptz NOT NULL DEFAULT now()
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
CREATE INDEX IF NOT EXISTS eval_case_results_run_idx
|
|
258
|
+
ON stackbone_platform.eval_case_results (eval_run_id);
|
|
259
|
+
|
|
260
|
+
-- A note on visibility, since it is a consequence nobody chose explicitly:
|
|
261
|
+
-- 0005_stackbone_viewer.sql set DEFAULT PRIVILEGES granting SELECT on new tables
|
|
262
|
+
-- in this schema to `stackbone_viewer`, so the Studio DB Explorer can read these
|
|
263
|
+
-- rows — including `eval_cases.input`, which will hold real captured customer
|
|
264
|
+
-- data. That matches how `runs.input` already behaves. If a deployment ever
|
|
265
|
+
-- needs these hidden, 0011_secrets_revoke_viewer.sql is the precedent for
|
|
266
|
+
-- revoking; nothing is revoked here.
|
|
267
|
+
--
|
|
268
|
+
-- No pg_notify trigger is attached to any of these tables ON PURPOSE. Ten
|
|
269
|
+
-- earlier migrations do attach one, so copying it in is the obvious reflex — but
|
|
270
|
+
-- live progress for an eval run is done by POLLING the `eval_runs` row, which
|
|
271
|
+
-- carries its own counters. Emitting eval frames would mean touching the
|
|
272
|
+
-- trigger, the emulator bus and the client parser to paint a counter.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
-- 0039_eval_case_results_idempotency.sql — the constraint behind "a retry does
|
|
2
|
+
-- not re-invoke the model" (STA-415, PRD STA-393).
|
|
3
|
+
--
|
|
4
|
+
-- The engine already reads which cases an eval run has verdicts for and skips
|
|
5
|
+
-- them when it resumes. That read-then-write is a RACE on its own: two workers
|
|
6
|
+
-- picking up the same interrupted run would each read "case 3 has no verdict"
|
|
7
|
+
-- and each pay for case 3 in the customer's own token budget. Every eval case
|
|
8
|
+
-- costs real money, so the skip needs a constraint under it and not just a
|
|
9
|
+
-- convention.
|
|
10
|
+
--
|
|
11
|
+
-- A verdict is identified by WHICH case, under WHICH variant, on WHICH attempt.
|
|
12
|
+
-- All four columns are needed: repeats are the whole reason `attempt` exists (a
|
|
13
|
+
-- case that only passes half the time has to be run more than once), and the two
|
|
14
|
+
-- variants of a comparison measure the same case on purpose.
|
|
15
|
+
--
|
|
16
|
+
-- NULLS NOT DISTINCT because `case_id` is nullable — a verdict outlives the case
|
|
17
|
+
-- row it measured. Under the default (NULLS DISTINCT) two verdicts with a null
|
|
18
|
+
-- case would both be allowed through the index and the append would stop being
|
|
19
|
+
-- idempotent exactly when a list had been edited underneath a resumed run.
|
|
20
|
+
CREATE UNIQUE INDEX IF NOT EXISTS eval_case_results_attempt_idx
|
|
21
|
+
ON stackbone_platform.eval_case_results (eval_run_id, case_id, variant_label, attempt)
|
|
22
|
+
NULLS NOT DISTINCT;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
-- 0040_eval_case_results_run_idx.sql — the index behind the foreign key 0038 added
|
|
2
|
+
-- from `eval_case_results` to `runs` (STA-415, PRD STA-393).
|
|
3
|
+
--
|
|
4
|
+
-- `eval_case_results.run_id` REFERENCES `runs (id) ON DELETE SET NULL`, and
|
|
5
|
+
-- Postgres enforces that by running the equivalent of
|
|
6
|
+
-- UPDATE eval_case_results SET run_id = NULL WHERE run_id = $1
|
|
7
|
+
-- inside EVERY transaction that deletes a `runs` row. With no index on the
|
|
8
|
+
-- referencing column that is a full sequential scan of what becomes the largest
|
|
9
|
+
-- table this feature creates (cases x repeats x variants x runs, never pruned —
|
|
10
|
+
-- 0038 says out loud that a verdict must outlive its run).
|
|
11
|
+
--
|
|
12
|
+
-- The cost lands on code that has nothing to do with evals: the workflow
|
|
13
|
+
-- concurrency gate deletes a single `waiting` run row on every admission
|
|
14
|
+
-- (`clearWaitingRun` / `clearOldestWaitingRun`), so admission latency would grow
|
|
15
|
+
-- with the accumulated eval history of a box that merely ran nightly evals.
|
|
16
|
+
--
|
|
17
|
+
-- The repo already gets this right for the two other foreign keys of the same
|
|
18
|
+
-- shape — `approvals_run_idx` next to 0002's ON DELETE SET NULL, and
|
|
19
|
+
-- `run_steps_run_started_idx` next to 0018's cascade. This is the one that was
|
|
20
|
+
-- left bare. `eval_case_results_run_idx` (0038) reads like it covers this, but it
|
|
21
|
+
-- indexes `eval_run_id` — the EVAL run, not the platform run.
|
|
22
|
+
--
|
|
23
|
+
-- A separate migration rather than an edit to 0038: an applied migration's
|
|
24
|
+
-- checksum is verified on every boot, so rewriting it would brick a box that
|
|
25
|
+
-- already ran it.
|
|
26
|
+
CREATE INDEX IF NOT EXISTS eval_case_results_platform_run_idx
|
|
27
|
+
ON stackbone_platform.eval_case_results (run_id);
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
-- 0041_eval_criterion_workflow_name.sql — write the name that used to be implicit
|
|
2
|
+
-- onto every stored `custom_workflow` criterion (STA-521, PRD STA-393).
|
|
3
|
+
--
|
|
4
|
+
-- The criterion "judged by your own workflow" used to resolve ONE reserved workflow
|
|
5
|
+
-- name and store nothing about it: the role WAS the workflow name, so a suite only
|
|
6
|
+
-- had to say "custom_workflow" and the box knew where to go. STA-521 makes the
|
|
7
|
+
-- judging workflow a setting picked from a dropdown of what the box holds, which
|
|
8
|
+
-- means the name is now something a suite has to CARRY.
|
|
9
|
+
--
|
|
10
|
+
-- WHY THIS SHIPS IN THE SAME SLICE AS THE CATALOG CHANGE, not after it. The catalog
|
|
11
|
+
-- rejects settings it does not declare, so a backfill landing FIRST would break
|
|
12
|
+
-- every subsequent save; and once `workflowName` is required, an unmigrated suite
|
|
13
|
+
-- can neither be saved nor judged. Either order on its own opens a window in which
|
|
14
|
+
-- a stored suite is broken. Both halves land together, so there is no window.
|
|
15
|
+
--
|
|
16
|
+
-- THE LITERAL BELOW MUST MATCH `EVAL_CRITERION_ROLE` in
|
|
17
|
+
-- libs/shared/evals/src/lib/custom/custom-criterion.ts. SQL cannot import the
|
|
18
|
+
-- constant, and that constant now exists for exactly this reason: it is the value
|
|
19
|
+
-- every suite meant while the name was implicit, and both sides have to mean the
|
|
20
|
+
-- same thing by it. The TS side keeps a comment pointing back here.
|
|
21
|
+
--
|
|
22
|
+
-- ONLY WHERE THE SETTING IS ABSENT OR BLANK. A box that already had the picker has
|
|
23
|
+
-- an operator's own choice in that field; overwriting it would silently re-point
|
|
24
|
+
-- their judge at a workflow they never picked, and nothing on the screen would say
|
|
25
|
+
-- so. `COALESCE(... ->> 'workflowName', '') = ''` covers all three ways of having
|
|
26
|
+
-- said nothing — the key missing, a JSON null, and an empty string — and makes the
|
|
27
|
+
-- statement idempotent: run it twice and the second pass matches nothing.
|
|
28
|
+
--
|
|
29
|
+
-- `scorers` is a jsonb ARRAY of `{type, params}`, so the rewrite goes element by
|
|
30
|
+
-- element through `jsonb_array_elements` and reassembles the list with `jsonb_agg`.
|
|
31
|
+
-- Element ORDER is preserved through `WITH ORDINALITY` … `ORDER BY`, because a
|
|
32
|
+
-- criterion list is read positionally on the screen; the co-located spec pins that
|
|
33
|
+
-- against a suite whose criterion is neither first nor alone. The `COALESCE(…,
|
|
34
|
+
-- '[]')` around the aggregate is belt-and-braces only — `jsonb_agg` over no rows
|
|
35
|
+
-- returns NULL, and the `EXISTS` guard below already means no row without criteria
|
|
36
|
+
-- is ever updated. It stays because writing NULL into a NOT NULL column is the one
|
|
37
|
+
-- mistake here that would take a boot down.
|
|
38
|
+
--
|
|
39
|
+
-- A separate file rather than an edit to 0038: the migrator hashes each body and
|
|
40
|
+
-- verifies it on every boot, so rewriting a landed migration bricks a box that
|
|
41
|
+
-- already ran it. Comments count towards that hash too.
|
|
42
|
+
UPDATE stackbone_platform.eval_suites AS s
|
|
43
|
+
SET scorers = (
|
|
44
|
+
SELECT COALESCE(jsonb_agg(rewritten.scorer ORDER BY rewritten.position), '[]'::jsonb)
|
|
45
|
+
FROM (
|
|
46
|
+
SELECT element.position,
|
|
47
|
+
CASE
|
|
48
|
+
WHEN element.scorer->>'type' = 'custom_workflow'
|
|
49
|
+
AND COALESCE(element.scorer->'params'->>'workflowName', '') = ''
|
|
50
|
+
THEN jsonb_set(
|
|
51
|
+
element.scorer,
|
|
52
|
+
'{params}',
|
|
53
|
+
COALESCE(element.scorer->'params', '{}'::jsonb)
|
|
54
|
+
|| jsonb_build_object('workflowName', 'eval-criterion')
|
|
55
|
+
)
|
|
56
|
+
ELSE element.scorer
|
|
57
|
+
END AS scorer
|
|
58
|
+
FROM jsonb_array_elements(s.scorers)
|
|
59
|
+
WITH ORDINALITY AS element(scorer, position)
|
|
60
|
+
) AS rewritten
|
|
61
|
+
)
|
|
62
|
+
WHERE jsonb_typeof(s.scorers) = 'array'
|
|
63
|
+
AND EXISTS (
|
|
64
|
+
SELECT 1
|
|
65
|
+
FROM jsonb_array_elements(s.scorers) AS scorer
|
|
66
|
+
WHERE scorer->>'type' = 'custom_workflow'
|
|
67
|
+
AND COALESCE(scorer->'params'->>'workflowName', '') = ''
|
|
68
|
+
);
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
-- 0042_eval_case_results_ended.sql — record HOW a measured case ended, next to
|
|
2
|
+
-- the free-form answer the verdict already keeps (STA-526, PRD STA-393).
|
|
3
|
+
--
|
|
4
|
+
-- 0038's own comment already promised this: "a case stopped by a guardrail or
|
|
5
|
+
-- parked on a human approval is distinguishable from one that simply answered
|
|
6
|
+
-- badly". It was only ever true by convention — the ending was a TypeScript
|
|
7
|
+
-- union translated away before the insert, and the sole trace left behind was a
|
|
8
|
+
-- string inside the `error` blob. Studio then had to guess the verdict by
|
|
9
|
+
-- matching the front of that string, which is how a parked workflow ended up
|
|
10
|
+
-- painted as an error.
|
|
11
|
+
--
|
|
12
|
+
-- A COLUMN rather than a code inside `error`, for two reasons:
|
|
13
|
+
-- - an agent that correctly ESCALATED is not an error, and archiving it as one
|
|
14
|
+
-- makes a suite of held cases read as a broken box;
|
|
15
|
+
-- - "3 of 15 escalated" has to be one query, not a JSON read per row.
|
|
16
|
+
--
|
|
17
|
+
-- THE SIX VALUES, and why the two parks are separate:
|
|
18
|
+
-- completed reached a terminal state with an answer
|
|
19
|
+
-- escalated parked on an operator approval
|
|
20
|
+
-- asked_user parked on a resume hook, waiting on a person
|
|
21
|
+
-- blocked_by_guardrail refused at the border, never started
|
|
22
|
+
-- errored broke
|
|
23
|
+
-- cancelled stopped before it could finish
|
|
24
|
+
-- `escalated` and `asked_user` are NOT folded together: telling "escalated to a
|
|
25
|
+
-- human" from "asked the end user" is the whole distinction the pause criteria
|
|
26
|
+
-- are being built to measure.
|
|
27
|
+
--
|
|
28
|
+
-- NULLABLE, and deliberately never backfilled. This is an APPEND-ONLY results
|
|
29
|
+
-- table: every verdict written before today belongs to a sealed run, and there
|
|
30
|
+
-- is no honest value to invent for it — `completed` would put words in that
|
|
31
|
+
-- run's mouth. A NULL reads as "this run could not say", which is the truth.
|
|
32
|
+
-- It also keeps a rolling upgrade safe: a box that applies this migration a
|
|
33
|
+
-- moment before its runners learn to write the column still writes verdicts,
|
|
34
|
+
-- rather than failing every one of them on a NOT NULL.
|
|
35
|
+
--
|
|
36
|
+
-- A CHECK and not free text, unlike `runs.entity_kind` (0037) or the suite's
|
|
37
|
+
-- `target_kind`. Those two are discriminators a box on an older build must be
|
|
38
|
+
-- able to STORE without executing, so rejecting an unknown value would lose an
|
|
39
|
+
-- operator's configuration. This column is the opposite: the box WRITES it, one
|
|
40
|
+
-- of six values decided in this repository, and a screen that groups on it must
|
|
41
|
+
-- not have to defend against a seventh spelling invented by a typo.
|
|
42
|
+
--
|
|
43
|
+
-- A separate file rather than an edit to 0038: the migrator hashes each body and
|
|
44
|
+
-- verifies it on every boot, so rewriting a landed migration bricks a box that
|
|
45
|
+
-- already ran it. Comments count towards that hash too.
|
|
46
|
+
ALTER TABLE stackbone_platform.eval_case_results
|
|
47
|
+
ADD COLUMN IF NOT EXISTS ended text;
|
|
48
|
+
|
|
49
|
+
-- Added separately from the column so the statement above stays re-runnable:
|
|
50
|
+
-- `ADD COLUMN IF NOT EXISTS ... CHECK (...)` would skip the constraint entirely
|
|
51
|
+
-- on a box where the column somehow already exists.
|
|
52
|
+
ALTER TABLE stackbone_platform.eval_case_results
|
|
53
|
+
DROP CONSTRAINT IF EXISTS eval_case_results_ended_check;
|
|
54
|
+
|
|
55
|
+
ALTER TABLE stackbone_platform.eval_case_results
|
|
56
|
+
ADD CONSTRAINT eval_case_results_ended_check
|
|
57
|
+
CHECK (ended IS NULL OR ended IN (
|
|
58
|
+
'completed',
|
|
59
|
+
'escalated',
|
|
60
|
+
'asked_user',
|
|
61
|
+
'blocked_by_guardrail',
|
|
62
|
+
'errored',
|
|
63
|
+
'cancelled'
|
|
64
|
+
));
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
-- 0043_eval_suite_approval_policy.sql — what a suite does about an approval one
|
|
2
|
+
-- of its cases parks on (STA-530, PRD STA-393).
|
|
3
|
+
--
|
|
4
|
+
-- A measured case has NOBODY waiting on it. The thread belongs to one attempt of
|
|
5
|
+
-- one case and is never spoken to again, so an agent that correctly stops to ask
|
|
6
|
+
-- an operator is scored on the stop and everything it would have said afterwards
|
|
7
|
+
-- is never seen. Half of what an approval-bearing agent does is on the other side
|
|
8
|
+
-- of that gate, and today a suite cannot measure any of it.
|
|
9
|
+
--
|
|
10
|
+
-- THE FOUR VALUES:
|
|
11
|
+
-- leave_waiting the case closes on the park and the request it raised is
|
|
12
|
+
-- taken back out of the inbox. What the product did before this
|
|
13
|
+
-- column existed.
|
|
14
|
+
-- reject answer it no, then keep watching the turn.
|
|
15
|
+
-- approve answer it yes, then keep watching the turn.
|
|
16
|
+
-- model_decides ask the judging model which of the two, then answer it.
|
|
17
|
+
--
|
|
18
|
+
-- NOT NULL with a DEFAULT, unlike 0042's `ended`. The opposite situation: that
|
|
19
|
+
-- one is an append-only results column with no honest value to invent for a row
|
|
20
|
+
-- written before it existed, while this is CONFIGURATION and the honest value for
|
|
21
|
+
-- every suite already stored is precisely what they do today — leave the case
|
|
22
|
+
-- waiting. Backfilling it is therefore not putting words in anybody's mouth.
|
|
23
|
+
--
|
|
24
|
+
-- A CHECK and not free text, for the same reason 0042's ending carries one: the
|
|
25
|
+
-- BOX writes and reads this, one of four values decided in this repository, and
|
|
26
|
+
-- the runner that branches on it must not have to defend against a fifth spelling
|
|
27
|
+
-- invented by a typo. The suite's own `target_kind` is deliberately open text by
|
|
28
|
+
-- contrast — that one is a discriminator a box on an older build has to be able
|
|
29
|
+
-- to store without executing.
|
|
30
|
+
--
|
|
31
|
+
-- A separate file rather than an edit to 0038: the migrator hashes each body and
|
|
32
|
+
-- verifies it on every boot, so rewriting a landed migration bricks a box that
|
|
33
|
+
-- already ran it. Comments count towards that hash too.
|
|
34
|
+
ALTER TABLE stackbone_platform.eval_suites
|
|
35
|
+
ADD COLUMN IF NOT EXISTS approval_policy text NOT NULL DEFAULT 'leave_waiting';
|
|
36
|
+
|
|
37
|
+
-- Added separately from the column so the statement above stays re-runnable:
|
|
38
|
+
-- `ADD COLUMN IF NOT EXISTS ... CHECK (...)` would skip the constraint entirely
|
|
39
|
+
-- on a box where the column somehow already exists.
|
|
40
|
+
ALTER TABLE stackbone_platform.eval_suites
|
|
41
|
+
DROP CONSTRAINT IF EXISTS eval_suites_approval_policy_check;
|
|
42
|
+
|
|
43
|
+
ALTER TABLE stackbone_platform.eval_suites
|
|
44
|
+
ADD CONSTRAINT eval_suites_approval_policy_check
|
|
45
|
+
CHECK (approval_policy IN (
|
|
46
|
+
'leave_waiting',
|
|
47
|
+
'reject',
|
|
48
|
+
'approve',
|
|
49
|
+
'model_decides'
|
|
50
|
+
));
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
-- 0044_eval_persona_simulator.sql — a case carries a persona, and the suite
|
|
2
|
+
-- decides whether anybody plays it (STA-532, PRD STA-393).
|
|
3
|
+
--
|
|
4
|
+
-- The clarification loop is the part of the product no eval touched. An agent
|
|
5
|
+
-- that asks before it acts parks on the first question, so what it would have
|
|
6
|
+
-- done with the answer — the whole reason it asks at all — was never measured.
|
|
7
|
+
-- 0043 opened the half of that story that faces an OPERATOR; this one opens the
|
|
8
|
+
-- half that faces the END USER.
|
|
9
|
+
--
|
|
10
|
+
-- FOUR TABLES, and each column is on the table that owns the fact:
|
|
11
|
+
--
|
|
12
|
+
-- eval_cases.persona WHO answers, in what words, how many times.
|
|
13
|
+
-- Belongs to the case, so the same list can be
|
|
14
|
+
-- run by a suite that measures the first
|
|
15
|
+
-- question and one that measures the whole
|
|
16
|
+
-- conversation.
|
|
17
|
+
-- eval_suites.simulator WHETHER anybody plays it, and with which
|
|
18
|
+
-- model. Belongs to the suite, so a persona
|
|
19
|
+
-- with the simulator off is simply ignored.
|
|
20
|
+
-- eval_runs.simulator_model WHO ACTUALLY played it, sealed at launch.
|
|
21
|
+
-- eval_runs.reply_cap HOW MANY TIMES they were allowed to.
|
|
22
|
+
-- eval_runs.simulated_connectors WHICH connectors were stood in for.
|
|
23
|
+
-- eval_case_results.simulation WHAT was said, turn by turn, marking which
|
|
24
|
+
-- lines were scripted and which improvised.
|
|
25
|
+
--
|
|
26
|
+
-- WHY THE RUN SEALS THREE MORE THINGS. Two runs of the same suite a month apart
|
|
27
|
+
-- have to stay comparable, and every one of these moves a score without the agent
|
|
28
|
+
-- changing at all: a different model plays the user differently, a cap raised
|
|
29
|
+
-- from three to five gives the agent two more chances, and a connector that was
|
|
30
|
+
-- real last month and simulated today is a different world. A run that did not
|
|
31
|
+
-- record them makes the next comparison read a change of setting as a change in
|
|
32
|
+
-- the agent.
|
|
33
|
+
--
|
|
34
|
+
-- A NOTE ON GRANULARITY, so it is not rediscovered as a surprise. `git_sha` and
|
|
35
|
+
-- `agent_version` on this table are already taken from the FIRST variant only,
|
|
36
|
+
-- while a run may carry two. The three columns below are added at the SAME single
|
|
37
|
+
-- value per run, deliberately — a per-variant seal for these alone would leave
|
|
38
|
+
-- the row half one shape and half another. When the variant scoping is fixed it
|
|
39
|
+
-- has to be fixed for all five together.
|
|
40
|
+
--
|
|
41
|
+
-- JSONB rather than columns for `persona` and `simulator`, matching `scorers` and
|
|
42
|
+
-- `variants` beside them: they are configuration payloads whose documented
|
|
43
|
+
-- defaults live in `@stackbone/validators` → `studio-evals.ts`, and a CHECK here
|
|
44
|
+
-- would be a second description that drifts from the form an operator fills in.
|
|
45
|
+
--
|
|
46
|
+
-- `simulated_connectors` is `text[]` rather than jsonb, matching `eval_cases.tags`
|
|
47
|
+
-- — it is a flat list of identifiers, and the array type is what makes "which
|
|
48
|
+
-- runs simulated Outlook?" an ordinary query.
|
|
49
|
+
--
|
|
50
|
+
-- NULLABLE where the fact is HISTORICAL and defaulted where it is CONFIGURATION,
|
|
51
|
+
-- exactly as 0042 and 0043 split it. A verdict written before this column existed
|
|
52
|
+
-- has no honest simulation to invent, and a run sealed before it has no honest
|
|
53
|
+
-- model to name — those read NULL, which is the truth. A suite, by contrast, has
|
|
54
|
+
-- a perfectly honest current value: nobody answers, which is what every stored
|
|
55
|
+
-- suite does today.
|
|
56
|
+
--
|
|
57
|
+
-- A separate file rather than an edit to 0038: the migrator hashes each body and
|
|
58
|
+
-- verifies it on every boot, so rewriting a landed migration bricks a box that
|
|
59
|
+
-- already ran it. Comments count towards that hash too.
|
|
60
|
+
|
|
61
|
+
ALTER TABLE stackbone_platform.eval_cases
|
|
62
|
+
ADD COLUMN IF NOT EXISTS persona jsonb;
|
|
63
|
+
|
|
64
|
+
ALTER TABLE stackbone_platform.eval_suites
|
|
65
|
+
ADD COLUMN IF NOT EXISTS simulator jsonb NOT NULL
|
|
66
|
+
DEFAULT '{"enabled": false, "model": null}'::jsonb;
|
|
67
|
+
|
|
68
|
+
ALTER TABLE stackbone_platform.eval_runs
|
|
69
|
+
ADD COLUMN IF NOT EXISTS simulator_model text;
|
|
70
|
+
|
|
71
|
+
ALTER TABLE stackbone_platform.eval_runs
|
|
72
|
+
ADD COLUMN IF NOT EXISTS reply_cap integer;
|
|
73
|
+
|
|
74
|
+
ALTER TABLE stackbone_platform.eval_runs
|
|
75
|
+
ADD COLUMN IF NOT EXISTS simulated_connectors text[] NOT NULL DEFAULT '{}';
|
|
76
|
+
|
|
77
|
+
ALTER TABLE stackbone_platform.eval_case_results
|
|
78
|
+
ADD COLUMN IF NOT EXISTS simulation jsonb;
|
|
79
|
+
|
|
80
|
+
-- Added separately from the column so the statement above stays re-runnable:
|
|
81
|
+
-- `ADD COLUMN IF NOT EXISTS ... CHECK (...)` would skip the constraint entirely
|
|
82
|
+
-- on a box where the column somehow already exists.
|
|
83
|
+
ALTER TABLE stackbone_platform.eval_runs
|
|
84
|
+
DROP CONSTRAINT IF EXISTS eval_runs_reply_cap_check;
|
|
85
|
+
|
|
86
|
+
ALTER TABLE stackbone_platform.eval_runs
|
|
87
|
+
ADD CONSTRAINT eval_runs_reply_cap_check
|
|
88
|
+
CHECK (reply_cap IS NULL OR reply_cap >= 0);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
-- 0045_eval_case_description.sql — a case says, in prose, what it is for.
|
|
2
|
+
|
|
3
|
+
-- A case list is read far more often than it is written, and until now the only
|
|
4
|
+
-- thing a reader got was the `name`. A name has to be short and unique, so it
|
|
5
|
+
-- ends up as a slug — `prof-asunto-y-cuerpo-discrepan` — and everything that
|
|
6
|
+
-- makes the case worth having lives in whoever wrote it.
|
|
7
|
+
--
|
|
8
|
+
-- WHERE THIS WAS BEING KEPT INSTEAD, and why that is the actual argument for the
|
|
9
|
+
-- column. Case lists in the wild already carry the explanation; they just have
|
|
10
|
+
-- nowhere to put it, so it goes inside `expected` as an extra key. That field is
|
|
11
|
+
-- the REFERENCE ANSWER — the thing scorers compare against — so an explanation
|
|
12
|
+
-- parked there is prose sitting inside the ground truth, surviving only because
|
|
13
|
+
-- each scorer is taught to skip that one key by name. A scorer that compares the
|
|
14
|
+
-- whole object (`exact-match` does) fails every case that has one.
|
|
15
|
+
--
|
|
16
|
+
-- So this is not a new capability so much as moving a fact onto the row that owns
|
|
17
|
+
-- it, which leaves `expected` holding only what is actually compared.
|
|
18
|
+
--
|
|
19
|
+
-- `text` and NULLABLE, not `NOT NULL DEFAULT ''`:
|
|
20
|
+
--
|
|
21
|
+
-- * Every case stored before today genuinely has no description, and NULL is
|
|
22
|
+
-- what "nobody wrote one" looks like. A default of '' would claim all of them
|
|
23
|
+
-- had been described with an empty sentence, and a screen cannot tell that
|
|
24
|
+
-- apart from a description somebody cleared on purpose.
|
|
25
|
+
-- * No length CHECK, matching `eval_cases.name` beside it: the bound is a form
|
|
26
|
+
-- concern and lives with the form, in `@stackbone/validators` →
|
|
27
|
+
-- `studio-evals.ts`. A number repeated here is a second bound that drifts.
|
|
28
|
+
--
|
|
29
|
+
-- A separate file rather than an edit to 0038: the migrator hashes each body and
|
|
30
|
+
-- verifies it on every boot, so rewriting a landed migration bricks a box that
|
|
31
|
+
-- already ran it. Comments count towards that hash too.
|
|
32
|
+
|
|
33
|
+
ALTER TABLE stackbone_platform.eval_cases
|
|
34
|
+
ADD COLUMN IF NOT EXISTS description text;
|