TB empty-gold fairness: pure_refusal-only gate under full catalog - #2831
TB empty-gold fairness: pure_refusal-only gate under full catalog#2831Dominic Nguyen (datduyng) wants to merge 40 commits into
Conversation
Empty expectedActions is scored as zero actions. Stop minting contrastive adjacent commands and refuse-then-alternate forms as empty-gold negatives; format_checker now hard-rejects them as BAD_NEGATIVE. Allowed negatives: pure refusal, non-action status/howto questions, and missing-info clarifications. Align synthesizer and quality-verifier prompts with the zero-action contract; add unit coverage including adversarial holes.
- Drop ACTION_VP / refusal regex classifier (unmaintainable verb lists). - Require semantic_checker negativeAssessments (kind + fairEmptyGold). - Code hard-fails unfair assessments as BAD_NEGATIVE; format stays structural. - Tests cover assessment parse/enforce and mock-LLM semantic gate.
- Single zod schema; derive OpenAI JSON schema via z.toJSONSchema - Match assessments by order (equal count); drop path-index maps - Simplify force-reject merge; strip explanatory comments
- Drop dead exports; parse via shared parseWithZod - Derive OpenAI schema from zod without void/\$schema clutter - Add test that assessments pair to negatives by order
- Join assessments to negatives by exact genCase path (bijective set); reject unknown/duplicate/missing paths as BAD_NEGATIVE instead of silent index pairing. - Parse reviewer decision before assessments so structured reject issues survive missing/invalid negativeAssessments. - Share TRANSLATION_BENCH_NEGATIVE_FAIRNESS_RULE in synthesizer context; tighten howto/soft-solicit unfair rules and untrusted payload framing. - Extend reviewerDecision mocks with path-keyed fair assessments; replace order-only path tests with multi-negative path-join coverage.
- Fixed short PATH_MSG/FIX only; use LLM reason as issue message - Drop rewrite-hint and per-case string assembly - Keep path 1:1 cover + fairEmptyGold/kind gate
- Honor concurrency with a worker pool and serialized checkpoint commits
- Keep parameters:{} after stripEmpty for required empty-object schemas
- Inject constant string-union fields (e.g. settings id) during gold validate
- Isolate per-slot generation failures so other workers keep committing
- Fair empty gold is pure_refusal/leave-alone only - Reject definition/status/Q&A and missing_info as empty gold - Synthesizer + quality-verifier prompts match any-tool abstain test - Extend unit coverage for definition Q and soft-solicit rejects
Join assessments to genCases via a path→case map built from the same keys the checker emits, then read dimensions.negativeKind directly.
…params - Allow incomplete case sets when requireCompleteCoverage is false - Prompt: nested objects must carry required schema fields (e.g. timeRange)
…ial gen - Partial-gen branch previously emitted planned caseCount/coverage, so validateTranslationBenchBenchmark always threw and the branch was unreachable dead code - Recompute scheduledActionCount, complete, and caseCount from the cases actually emitted; happy-path (complete) output is unchanged - Makes requireCompleteCoverage=false produce a valid draft directly
Derive FAIR_KINDS from TRANSLATION_BENCH_FAIR_EMPTY_GOLD_KINDS so the allowlist cannot drift from the exported constant.
There was a problem hiding this comment.
Pull request overview
Adds a fairness contract ensuring empty-gold translation-bench negatives represent only pure refusals.
Changes:
- Adds path-keyed LLM fairness assessments and enforcement.
- Updates generation/verifier prompts and gold-action validation.
- Supports concurrent generation and accurate partial-result metadata.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
README.AUTOGEN.md |
Refreshes generated documentation metadata. |
actionValidation.ts |
Normalizes gold actions for schema validation. |
benchmark.ts |
Uses normalized gold-action validation. |
dataQualityVerifier.ts |
Enforces semantic negative-fairness assessments. |
datasetGenerator.ts |
Adds concurrency and partial-generation handling. |
generationCandidate.ts |
Preserves required parameter objects during cleanup. |
index.ts |
Exports the fairness module. |
negativeFairness.ts |
Implements fairness parsing and enforcement. |
quality-verifier.prompt.yaml |
Defines full-catalog fairness review requirements. |
synthesizer.prompt.yaml |
Restricts empty-gold negatives to pure refusals. |
utteranceDisambiguation.ts |
Documents separate negative handling. |
translationBench.datasetGenerator.spec.ts |
Updates generation fixtures for fairness assessments. |
translationBench.negativeFairness.spec.ts |
Tests fairness validation and semantic enforcement. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…s, ambiguous-route guard Row-by-row 3-model review of the 1k eval showed most all-models "failures" were dataset/scoring fairness issues, not real misses. Address the generator-side ones: - Wire parameterScore specs into every generated case at finalize (`parameterScoreSpecsForExpectedActions`). Free-text echo params such as `originalRequest` and setting free-text now score `nonempty` instead of exact, matching the grader's own classification. Canonical payload hash excludes parameterScore, so dataset identity is preserved. - Add `HARDCODED_NON_EVAL_ACTION_IDS` (`chat.generateResponse`, `utility.claudeTask`) as a single source of truth, unioned into the packaged exclusion set so they are never targeted. - Drop cross-schema duplicate action names from targeting: when the same bare action name is owned by more than one schema (e.g. `deleteWebFlow`), the single gold route is ambiguous, so exclude every sibling. Tests: parameterScore wiring + canonical-hash stability, non-eval exclusion set, and ambiguous cross-schema guard. Full benchmarks suite green (138).
…eckpoint, param-spec cleanup Code-review + Copilot follow-ups on TB generation fairness: - negativeFairness: require dimensions.negativeKind to exactly equal the accepted empty-gold assessment kind. Previously a fair pure_refusal assessment was accepted even when negativeKind was missing/numeric/boolean, letting unlabeled empty-gold rows through. Now the label must match. - datasetGenerator: persist the checkpoint row BEFORE mutating casesBySlot / usedUtterances in commitAccepted, so an I/O failure can no longer leave an uncheckpointed case that the partial-coverage path would return. - Unify the duplicated param-field-mode union: grader now imports TranslationBenchParamFieldMode / TranslationBenchParameterScoreSpec from benchmark instead of re-declaring RunnerParamFieldMode; collapse the spec derivation to Object.fromEntries. - benchmark: enforce parameterScore aligns 1:1 with expectedActions in the probe payload schema (validateProbePayload). - Extract ambiguousCrossSchemaActionIds helper; drop the size===0 ternary. Tests: new integration coverage for generateTranslationBenchBenchmark (concurrent full run + partial run past a failed slot, asserting checkpoint contents and coverage); negativeKind-missing rejection; fixtures now label negatives pure_refusal. Full suite green (141).
- Seed 24 empirically-mined cross-schema collision pairs into KNOWN_CONFUSABLE_PAIRS (browser tab routes, code/utility file ops, visualStudio/code-debug, desktop/display settings, player queues, etc.) - Add generic cross-schema near-duplicate detector in findTranslationBenchConfusableSiblings: flags equivalent actions in different schemas when BOTH action-name token overlap >=0.5 AND description overlap >=0.34, so shared generic verbs alone do not over-flag unrelated actions - Add significantTokensFromText helper for description-token overlap - Extend unit tests with cross-schema newTextFile<->writeFile case plus a readFile negative control Root cause: synthesizer scheduled every catalog action as a gold target but only compared same-schema siblings for disambiguation, so cross-schema near-synonyms produced no disambiguation constraint and yielded ambiguous-gold cases the models unanimously routed elsewhere.
- Move catalog/grader generation into translationBench/policy - Add action-eligibility hard bans + LLM quality picker (model required) - Ship eligible-gold-actions.generated.json; fail-closed load/integrity - Schedule lattice + eval pin allowlist hash; copyAssets requires assets - Tests for picker, policy, nested llmAsAJudge, schedule allowlist-on
…ypeAgentQuestion from eligible actions
- remove dispatcher.lookup.lookupAndAnswerConversation from packaged eligible-gold allowlist and regenerate grader/policy artifacts - update policyGenerator spec fixture to match
…ntrols in action-quality prompt
- action-quality picker now records include/exclude reason for every catalog action; artifact persists a sorted decisions[] list - fail-closed integrity: allowlist must equal include=true decisions and every scheduled catalog action must have a reasoned decision - prompt requires a non-empty reason per decision; contentHash unchanged (reasons excluded from hash)
…batch config - tpmLimiter.mjs: cross-session per-model TPM limiter backed by a central SQLite ledger (~/.typeagent/benchmark/rate-limitters/tpm.sqlite). Tracks token claims over a rolling 60s window; reserve-then-settle corrects estimates to actual usage; stale in-flight claims (>3min) are reclaimed. Deadlock-free via BEGIN IMMEDIATE + busy_timeout; portable across Windows/macOS/Linux via os.homedir(). - generate.mjs: synthesizer runner wired to the limiter via prop drilling (no env), gating every generator/reviewer model call by measured tokens. - approve-and-eval.mjs + helper scripts: eval runner and pipeline drivers. - tbConfig.mjs: config loader with base/batch deep-merge precedence. - config.schema.json + config.example.json: JSON Schema (editor-side validation) and a committable template with zeroed quotas. Real quotas live in git-ignored config.local.json. - Named batches (synthesizer / eval_fast / eval) for fast early feedback.
…to core - Add src/core/rateLimiter.ts: shared-SQLite tokens-per-minute limiter that reserves against a rolling 60s window and settles to actual usage, so concurrent awaited calls stay within the per-minute quota across processes. - Add src/translationBench/runConfig.ts + config.schema.json: pure JSON run config loader/resolver (batch merge, per-model concurrency derivation). - Barrel-export both; copy schema to dist via copyAssets. - Add jest specs for limiter and config resolution. - Move local run harness out of the tree (gitignore local/); runners now take commander flags and prop-drill config instead of TB_* env. - Add AGENTS.md documenting layout, JSON config, and the credential-env boundary.
- Add estimatePromptTokens() backed by gpt-tokenizer o200k_base with a +5% overhead, used as the rate limiter's pre-flight reservation. - o200k_base is a model-agnostic approximation for all models (GPT and non-GPT); the reservation later settles to actual usage, so cross-tokenizer drift self-corrects. - Pin gpt-tokenizer at ^2.9.0 to avoid string-literal export parse errors under TS 5.4. - Export from index, document in AGENTS.md, cover with specs.
|
Benchmark of record: 1k generation + 3-model eval on the TPM-limiter config (2026-08-10, this branch tip b77c420 — no code changes; run tooling is gitignored Generation: 905/1000 rows accepted (8 checkpoint-resume rounds; 95 data-quality-gate rejections — probe disagreement + duplicate-utterance dominant, zero infra residual). Trust gate PASS against the frozen source pool. Eval (5,430 cells = 905 rows × 2 suite cases × 3 models, recomputed from raw cells):
Schema-valid 98.9%. Zero harness/infra unknown-throw cells: 2,450 transient 429 + 164 no-response cells retried to zero via checkpoint-retry (shared Azure account contention, not per-model quota); the 57 remaining error cells are model-content JSON-validation failures, scored as failures. Review chain: gen-100 full-coverage row review 99/99 PASS; gen-1000 sample review 50/50 PASS; failed-cell adversarial spot-check 21/21 model failures; final adversarial panel (metrics honesty / reproducibility / stale-artifact hunt) PASS after 2 must-fix rounds. Baseline comparison (old gpt-5.6 trio, 74.1%): different models, harness, and dataset — numbers are the new benchmark of record, not an apples-to-apples rerun; deltas documented in eval-summary.txt. Run dir of record: |
# Conflicts: # ts/pnpm-lock.yaml
Fold the utterance-disambiguation branch work into the negative-fairness PR for now (split later): - Restore ACTION_DISAMBIGUATION_CUES + deterministic candidate gate - Ban confusable browser discovery golds at synth/format time - Keep negative-fairness + ambiguity-probe wiring intact - Re-enable format-checker hard reject on double-meaning positives
- Library under translationBench/runner (score, run, checkpoint, report, explainer) - Thin tb-eval / tb-generate CLIs on runConfig + rateLimiter - Benchmark draft → suite adapter with parameterScore passthrough - Share non-eval action IDs between generator and runner scoring - Export dispatcher internals needed by runner (cache, Session, translate helpers) - Package subpath @typeagent/benchmarks/translationBench/runner + unit tests
- Apply case history via isolated chatHistory (runner + explainer) - Per-case dispatcher context to avoid concurrent translate races - Empty-gold counts chat/non-eval fires (align with pure_refusal fairness) - tb-eval no longer auto-approves; draft/approved content drift fails closed - Checkpoint fingerprint includes benchmarkHash; CLI concurrency overrides map - TPM reserve floor + per-attempt rate-limit settle; settle failures logged - Ambiguity probe serializes model config swaps with restore - Drop dead output-reservation helpers and unused runner fixtures - Add scale checkpoint truncated-line resume test
Dead ballast called out in deep-review (no tests/scripts referenced them).
Summary
Empty-gold TB negatives (
expectedActions: []) are scored as zero actions across the full loaded tool catalog. Label-fair Q&A/definition negatives were not zero-action-scorer-fair: models correctly firechat/help/history, tanking negative pass.This PR ships a pipeline fairness contract so empty gold is only accepted for hard abstains:
pure_refusalonly (leave-alone / don't / never / cancel of the target; no questions, no alternate task).negativeFairnessmodule: path-keyed 1:1negativeAssessments(no regex path parse); fail-closed on missing/duplicate paths;fairEmptyGoldonly when kind is in the allowlist (TRANSLATION_BENCH_FAIR_EMPTY_GOLD_KINDS).caseCount/ coverage metadata match the cases actually produced (so validation can succeed).Measured 3-model eval (honest)
Regenerated draft under the new policy (998/1000 rows; 2 slots exhausted data-quality, unrelated to fairness), then evaluated
azure/gpt-5.6-{sol,terra,luna}.pure_refusalFinding: fairness regen fixed the dataset (kind mix + unfair rate), but did not produce a large negative-pass lift. Checkpoint inspection shows models often emit the refused action itself on clear hard-abstains (negation-following failure). Translator/prompt work is out of scope here.
Test plan
pnpm run buildints/packages/benchmarkspnpm run jest-esm --testPathPattern='(negativeFairness|datasetGenerator)\\.spec\\.js'(43/43)Infra: extract cross-process TPM limiter + run config to
coreFollow-on refactor moving the run harness's rate limiter and config loader into
@typeagent/benchmarksas tested, reusable modules.Interface (one owner per concern):
createRateLimiter(tpmLimits, { dbPath, estTokensPerCall?, maxWaitMs?, onWait? })→{ disabledFor, run, close }.dbPathis required so callers own persistence; no hidden default DB. Optional knobs are orthogonal.runConfig.tsis pure: reads a JSON file (schema-validated), merges batch over base, derives per-model concurrency. No env, no process globals — runtime overrides are commander flags prop-drilled by the local runners (TB_*env removed).OPENAI_*/AZURE_*) is intentionally kept as theaiclientcontract; documented inAGENTS.md.Correctness:
BEGIN IMMEDIATE, WAL): concurrent awaitedrun()calls reserve then settle to actual token usage, staying within quota across processes.STALE_MS(purge guarded onpending = 0); settle falls back to insert if the claim row was purged.Tests:
translationBench.rateLimiter.spec+translationBench.runConfig.spec(17 cases) — passthrough for unquota'd models, budget admit/throttle, cross-instance shared budget, estimate fallbacks, batch merge, concurrency derivation, malformed-json handling.Test plan (infra)
pnpm run build+pnpm run prettierclean ints/packages/benchmarkspnpm run jest-esm --testPathPattern='translationBench.(rateLimiter|runConfig).spec.js'(17/17)resolveRunnerConfigsmoke against realconfig.local.json