LibreChat/scripts/activity-labels
Danny Avila a07c0e4ae8
🧪 chore: Add the Activity-Label Prose Eval Harness (#14527)
Grades fast-model activity-label headers against a fixed corpus so
instruction changes are measured rather than eyeballed on one
conversation. This existed untracked while the continuity work was
developed; committing it because it is the only reproducible record of
WHY `ACTIVITY_INSTRUCTION` is ordered and capped the way it is.

- captured.json: 9 real production payloads pulled verbatim from Langfuse
  with the headers that shipped. Irreplaceable — traces age out.
- corpus.js: 17 cases / 28 steps. The captured run replays as one
  sequence, plus synthetic cases for the modes it never exercised
  (all-failed, partial, parallel batches, rapid near-duplicates, entry
  overflow, truncated output, error-shaped success). Multi-step cases
  chain each generated label into the next step's context, which is what
  makes cross-batch redundancy measurable at all.
- prompt.js: faithful port of the SDK's buildActivityLabelPrompt so
  synthetic cases render the bytes production sends, plus a
  previousLabelCap knob for continuity-window experiments.
- variants.js: single-factor instruction variants. The baseline is read
  from the BUILT package (workspace resolution, then dist, then
  LABEL_EVAL_DIST) so a variant can never be graded against a stale copy
  of the shipped instruction.
- checks.js: length/punctuation/markdown/tool-echo/count-echo, plus
  overlap split into `restate` (adds nothing over an earlier header) vs
  `template` (same frame, new payload — often fine).
- run.js / rescore.js: live runner on the production wire shape
  (max_tokens 256) and an offline re-grader, so metric fixes never
  require re-spending on the API.

Results are gitignored — regenerable, and 292K of the 364K. A full sweep
is ~$0.03 per variant and ~45s.

Findings are recorded in the README, two of them counter-intuitive:
enumerating acceptable opening verbs ANCHORED the model rather than
diversifying it (Confirmed 18→23, opener diversity halved), and diverse
examples alone changed nothing. Sentence order is load-bearing, so a
tidying reshuffle of ACTIVITY_INSTRUCTION regresses real output.
2026-07-30 09:22:34 -04:00
..
captured.json 🧪 chore: Add the Activity-Label Prose Eval Harness (#14527) 2026-07-30 09:22:34 -04:00
checks.js 🧪 chore: Add the Activity-Label Prose Eval Harness (#14527) 2026-07-30 09:22:34 -04:00
corpus.js 🧪 chore: Add the Activity-Label Prose Eval Harness (#14527) 2026-07-30 09:22:34 -04:00
prompt.js 🧪 chore: Add the Activity-Label Prose Eval Harness (#14527) 2026-07-30 09:22:34 -04:00
README.md 🧪 chore: Add the Activity-Label Prose Eval Harness (#14527) 2026-07-30 09:22:34 -04:00
report.js 🧪 chore: Add the Activity-Label Prose Eval Harness (#14527) 2026-07-30 09:22:34 -04:00
rescore.js 🧪 chore: Add the Activity-Label Prose Eval Harness (#14527) 2026-07-30 09:22:34 -04:00
run.js 🧪 chore: Add the Activity-Label Prose Eval Harness (#14527) 2026-07-30 09:22:34 -04:00
variants.js 🧪 chore: Add the Activity-Label Prose Eval Harness (#14527) 2026-07-30 09:22:34 -04:00

Activity-label eval harness

Measures the prose quality of fast-model activity-label headers (the collapsed group headers produced by activityLabel). Instruction changes are graded against a fixed corpus instead of eyeballed on one conversation — the two hypotheses that felt most obvious when this was built both turned out wrong under measurement (see Findings).

node scripts/activity-labels/run.js                       # every variant, 1 sample
node scripts/activity-labels/run.js --samples 3           # 3 samples each
node scripts/activity-labels/run.js --variants baseline,shipping --cases fib-rapid
node scripts/activity-labels/run.js --dry --cases mega-batch    # render prompts, no API calls
node scripts/activity-labels/rescore.js                   # re-grade stored results, no re-spend

Requires ANTHROPIC_API_KEY (env or .env). A full sweep is roughly $0.03 per variant and ~45s. Results land in results/ (gitignored): a timestamped JSON of every record plus latest.md with per-case tables.

Why the tables matter more than the aggregate

The mechanical checks catch format violations and lexical repetition, but the failure this feature actually had in production — headers that were informationally redundant while lexically varied — scores below the overlap threshold. results/latest.md read by eye is the recall instrument; the aggregate is the regression guard.

Layout

File Role
captured.json 9 real production payloads pulled verbatim from Langfuse, with the headers that shipped. Irreplaceable — traces age out.
corpus.js 17 cases / 28 steps: the captured run as one sequence, plus synthetic cases for modes it never hit (all-failed, partial, parallel, rapid duplicates, entry overflow, truncated output, error-shaped success). Multi-step cases chain each generated label into the next step's context.
prompt.js Faithful port of the SDK's buildActivityLabelPrompt, so synthetic cases render the bytes production would send. Adds previousLabelCap for continuity-window experiments.
variants.js Single-factor instruction variants. baseline is read from the built packages/api dist, so drift from the shipped instruction is impossible.
checks.js Mechanical grading: length, punctuation, markdown, tool-name echo, count echo, and overlap split into restate (adds nothing) vs template (same frame, new payload).
run.js / rescore.js Live runner (production wire shape, max_tokens: 256) and offline re-grader.

Findings this produced

  • Sentence order is load-bearing. Moving format constraints after content rules cut length violations and register collapse measurably. This is why ACTIVITY_INSTRUCTION is ordered the way it is — a "tidying" reshuffle regresses real output.
  • Enumerating verbs backfires. An instruction listing acceptable opening verbs anchored the model: Confirmed went from 18 to 23 occurrences and opener diversity halved.
  • Diverse examples alone changed nothing. Register collapse is task-shaped, not example-seeded.
  • Continuity context fixed the real defect. Feeding committed headers back eliminated restatement and, unexpectedly, stopped setup batches from being labeled with conclusions their tools had not yet established.
  • A 3-label window is enough. Unbounded history scored no better — restatement is inherently a recency problem — while prompt growth is linear (+82 input tokens by batch 9, extrapolating to ~+250 at the activityMaxPerRun default of 20).