LibreChat/scripts/activity-labels/variants.js
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

239 lines
7.3 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Instruction variants under test. Each is a SINGLE-factor change against the
* production instruction so a result implicates one hypothesis:
*
* - baseline — ACTIVITY_INSTRUCTION exactly as the branch ships it
* - verbs — H: the Good-example verb distribution seeds register
* collapse (6/9 production labels opened "Confirmed")
* - ordered — H: the 49 word cap gets crowded out mid-paragraph; moving
* format constraints last improves adherence
* - continuity — H: showing the run's previous headers kills cross-batch
* redundancy (production pairs 2/3 and 7/8)
*
* The baseline is required from packages/api/dist so drift against the branch
* is impossible; the sentence table below is asserted against it so composed
* variants can never silently diverge from what production actually sends.
*/
const path = require('path');
const ROOT = path.resolve(__dirname, '..', '..');
/**
* The shipped instruction, read from the BUILT package so a variant can never
* be graded against a stale copy of it. Tries the workspace resolution first
* (an installed checkout), then the dist path directly, so the harness works
* whether or not `node_modules` is populated. `LABEL_EVAL_DIST` points it at
* another checkout's build — useful for grading one branch's instruction from
* a worktree that has not been built.
*/
function loadShippedInstruction() {
const candidates = [
process.env.LABEL_EVAL_DIST,
'@librechat/api',
path.join(ROOT, 'packages/api/dist/index.cjs'),
].filter(Boolean);
for (const candidate of candidates) {
try {
const { ACTIVITY_INSTRUCTION } = require(candidate);
if (typeof ACTIVITY_INSTRUCTION === 'string' && ACTIVITY_INSTRUCTION.length > 0) {
return ACTIVITY_INSTRUCTION;
}
} catch {
/* try the next candidate */
}
}
throw new Error(
'Could not load ACTIVITY_INSTRUCTION from a built @librechat/api.\n' +
'Build it first (from the repo root):\n' +
' npm run build:data-provider && npm run build:data-schemas && npm run build:api\n' +
'Or point at an existing build:\n' +
' LABEL_EVAL_DIST=/path/to/packages/api/dist/index.cjs node scripts/activity-labels/run.js',
);
}
const ACTIVITY_INSTRUCTION = loadShippedInstruction();
const S = {
role: 'You write the one-line header above a group of tool calls an AI agent just made.',
register:
'Write it like a git commit subject: past tense, verb first, leading with the most distinctive file, name, or finding.',
outcome:
'Say what the calls established or produced — the outcome, not the attempt. If they answered a question, the answer is the line.',
prohibitions:
'Never name the tools, never count them, never echo the arguments: the cards below the header already show all three.',
format: 'Write 4 to 9 words, sentence case, no trailing punctuation, no quotes or markdown.',
good: 'Good: "Confirmed /mnt/data resets between calls". "Traced the leak to formatAgentMessages". "Found 3 failing auth tests".',
bad: 'Bad: "Ran 1 command". "Used bash_tool twice". "Executed ls /mnt/data". "Searched the codebase".',
failure: 'If every call failed, say what failed and why, plainly.',
output: 'Output only the line.',
};
const CONTINUITY_SENTENCE =
'A "Previous headers" list may precede the batch: never restate one — if this batch continues that activity, say only what is new.';
/** Pre-P1 instruction — kept as the `legacy` variant for regression sweeps. */
const LEGACY_ORDER = [
S.role,
S.register,
S.outcome,
S.prohibitions,
S.format,
S.good,
S.bad,
S.failure,
S.output,
];
/** The shipped instruction (P1): ordered structure + continuity clause. */
const SHIPPED_ORDER = [
S.role,
S.outcome,
S.register,
S.good,
S.bad,
S.failure,
CONTINUITY_SENTENCE,
S.prohibitions,
S.format,
S.output,
];
/** `baseline` is whatever the BUILT dist ships. Before the P1 rebuild that is
* the legacy order, after it the shipped order; anything else means the
* sentence table here has drifted and composed variants are stale. */
if (
LEGACY_ORDER.join(' ') !== ACTIVITY_INSTRUCTION &&
SHIPPED_ORDER.join(' ') !== ACTIVITY_INSTRUCTION
) {
console.warn(
'WARN: variants.js sentence table has drifted from ACTIVITY_INSTRUCTION — composed variants are stale',
);
}
const VERB_CHOICE =
'Open with whichever past-tense verb the outcome dictates — confirmed, found, traced, measured, wrote, ruled out, failed — not the same verb every time.';
const DIVERSE_GOOD =
'Good: "Traced the leak to formatAgentMessages". "Ruled out DNS as the failure cause". "Measured cold start at 412ms". "Found 3 failing auth tests".';
const CONTINUITY =
'A "Previous headers" list may precede the batch: those lines already stand above earlier groups, so never write a line that merely restates one. If this batch continues that same activity, lead with what is new or different in THIS batch.';
const CONTINUITY_TIGHT =
'A "Previous headers" list may precede the batch: never restate one — if this batch continues that activity, say only what is new.';
const FORMAT_HARD =
'Write 4 to 9 words, sentence case, no trailing punctuation, no quotes or markdown; when a batch found many things, keep only the most load-bearing one or two.';
const variants = [
{
name: 'baseline',
usePreviousLabels: SHIPPED_ORDER.join(' ') === ACTIVITY_INSTRUCTION,
instruction: ACTIVITY_INSTRUCTION,
},
{
name: 'legacy',
usePreviousLabels: false,
instruction: LEGACY_ORDER.join(' '),
},
{
name: 'verbs',
usePreviousLabels: false,
instruction: [
S.role,
S.register,
VERB_CHOICE,
S.outcome,
S.prohibitions,
S.format,
DIVERSE_GOOD,
S.bad,
S.failure,
S.output,
].join(' '),
},
{
name: 'ordered',
usePreviousLabels: false,
instruction: [
S.role,
S.outcome,
S.register,
S.good,
S.bad,
S.failure,
S.prohibitions,
S.format,
S.output,
].join(' '),
},
{
name: 'continuity',
usePreviousLabels: true,
instruction: [
S.role,
S.register,
S.outcome,
S.prohibitions,
S.format,
S.good,
S.bad,
S.failure,
CONTINUITY,
S.output,
].join(' '),
},
{
name: 'examples',
usePreviousLabels: false,
instruction: [
S.role,
S.register,
S.outcome,
S.prohibitions,
S.format,
DIVERSE_GOOD,
S.bad,
S.failure,
S.output,
].join(' '),
},
{
name: 'composed',
usePreviousLabels: true,
instruction: [
S.role,
S.outcome,
S.register,
DIVERSE_GOOD,
S.bad,
S.failure,
CONTINUITY_TIGHT,
S.prohibitions,
FORMAT_HARD,
S.output,
].join(' '),
},
{
name: 'shipping-full',
usePreviousLabels: true,
/** Whole-run history instead of the 3-label recency window: does more
* story beat recency, or does it dilute the batch content? */
previousLabelCap: Infinity,
instruction: SHIPPED_ORDER.join(' '),
},
{
name: 'shipping',
usePreviousLabels: true,
instruction: [
S.role,
S.outcome,
S.register,
S.good,
S.bad,
S.failure,
CONTINUITY_TIGHT,
S.prohibitions,
S.format,
S.output,
].join(' '),
},
];
module.exports = { variants };