Skip to content

Translation bench - #2831

Draft
Dominic Nguyen (datduyng) wants to merge 90 commits into
mainfrom
domnguyen/tb-negative-fairness
Draft

Translation bench#2831
Dominic Nguyen (datduyng) wants to merge 90 commits into
mainfrom
domnguyen/tb-negative-fairness

Conversation

@datduyng

@datduyng Dominic Nguyen (datduyng) commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

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 fire chat/help/history, tanking negative pass.

This PR ships a pipeline fairness contract so empty gold is only accepted for hard abstains:

  • Synthesizer + quality-verifier prompts: empty-gold kind is pure_refusal only (leave-alone / don't / never / cancel of the target; no questions, no alternate task).
  • negativeFairness module: path-keyed 1:1 negativeAssessments (no regex path parse); fail-closed on missing/duplicate paths; fairEmptyGold only when kind is in the allowlist (TRANSLATION_BENCH_FAIR_EMPTY_GOLD_KINDS).
  • Partial generation: when complete coverage is optional, emitted caseCount / coverage metadata match the cases actually produced (so validation can succeed).
  • No gold-row hand edits and no empty-gold scorer loosening.

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}.

Metric Result
Rows / empty-gold negs 998 / 998
Kind mix 100% pure_refusal
Unfair rate (LLM audit) 0%
Overall pass 36.8%
Negative pass (zero fire on empty gold) 13.9%
Neg fire-on-empty 47.9%
Positive pass 59.8%
Pos abstention FNR 3.2%
Tool / param 83.5% / ~71.6%
Eval cost ~$190

Finding: 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 build in ts/packages/benchmarks
  • pnpm run jest-esm --testPathPattern='(negativeFairness|datasetGenerator)\\.spec\\.js' (43/43)
  • Smoke + full regen fairness audit (0% unfair, 100% pure_refusal)
  • 3-model headless eval on regenerated draft; scale-metrics recorded

Infra: extract cross-process TPM limiter + run config to core

Follow-on refactor moving the run harness's rate limiter and config loader into @typeagent/benchmarks as tested, reusable modules.

Interface (one owner per concern):

  • createRateLimiter(tpmLimits, { dbPath, estTokensPerCall?, maxWaitMs?, onWait? }){ disabledFor, run, close }. dbPath is required so callers own persistence; no hidden default DB. Optional knobs are orthogonal.
  • runConfig.ts is 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).
  • Credential env (OPENAI_*/AZURE_*) is intentionally kept as the aiclient contract; documented in AGENTS.md.

Correctness:

  • TPM is enforced by a shared SQLite ledger over a rolling 60s window (BEGIN IMMEDIATE, WAL): concurrent awaited run() calls reserve then settle to actual token usage, staying within quota across processes.
  • In-flight claims survive purge to STALE_MS (purge guarded on pending = 0); settle falls back to insert if the claim row was purged.
  • Fail-closed: throws on missing positive token estimate; omits non-positive tpm limits.

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 prettier clean in ts/packages/benchmarks
  • pnpm run jest-esm --testPathPattern='translationBench.(rateLimiter|runConfig).spec.js' (17/17)
  • Commander parse + resolveRunnerConfig smoke against real config.local.json

Dominic Nguyen (datduyng) and others added 17 commits August 7, 2026 22:00
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.
@datduyng Dominic Nguyen (datduyng) changed the title Reject unfair empty-gold TB negatives via LLM fairness gate TB empty-gold fairness: pure_refusal-only gate under full catalog Aug 8, 2026
@datduyng
Dominic Nguyen (datduyng) requested a balanced review from Copilot August 9, 2026 00:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Force rebuild: 558 actions, 318 structural, 600 LLM-verified.
--require-complete-coverage now fills caseCount; full catalog
coverage stays on coverage.complete for the 1k run.
Round-001 accepted 0/100: the verifier scored
anchorFidelity as topic match. Isolated prompts
now require scheduled gold, not the anchor's calls.
Slot cycling reused a few favorite pairs.
Least-used unique combos keep 100-row gold
on many distinct actions.
100-slot catalog must use >=150 actions and
keep 3-call cross slots on two schemas.
OpenAI tool JSON schemas list every property in required[]. Reviewers
then demanded search_filters, and the generator dumped optional
createIfNotExists/fallbackToActiveFile:false into gold.

- Rewrite gold-facing required[] to TypeAgent optionality
- Strip optional false booleans after empty-placeholder hygiene
- Bind negativeAssessments 1:1 to $.genCases[N].utterance
- Treat probe unknown-throws as abstention and retry translate blips
Round-003 died 0/100: TypeAgent probe errors were
treated as bad gold. Only unscheduled extras reject.
Least-used picks joined Bluetooth with editor
split. Cross gold now prefers one product surface.
Probe errors and omitted calls stay valid gold.
Round-004 died 99/100: slot 74 named exactMatch
off but omitted it from setSearchParameters.
Probes posted model=None because createChatModel("azure/gpt-5.5")
ignored the name on the OpenAI-compatible path. Slash ids are now
the request model. Stop stripping optional false booleans so
enable:false survives when the utterance asks to turn something off.
Round-005 review failed: history replayed
SystemThemeMode/findMusic; any-order on start/stop.
- Require adapted history on seed/positives when anchor has history
- Format-gate BAD_HISTORY; prompt history_rule REQUIRED/FORBIDDEN
- Curate browseIssue/openTab vs openWebPage confusable siblings
- Strengthen synthesizer/quality-verifier disambiguation cues

Round-003 failed 6/100 on history-followups and browser confusables.
Slots 19/20/24 were always clear/add or start/stop
under any. Slot 69 was always gotoLine + theme.
r007 slot 69 became openFile + theme. Editor
family was too coarse; VS and VS Code stay apart.
r008 000079 seed asked for a symbol; the
positive said go to line 240. Same gold.
Round-009 died 99/100: slot 27 sent an extra
parameters field on a no-input tool.
- Treat create+add as order-sensitive (strict only)
- Never schedule clear+remove or deleteAll+play (void later call)
- Isolate discord family from chat/calendar comms
- Format-check and unit tests cover the bans
- Drop conflicting "gold may not depend on hidden prior state" rule
- Identity lives in history; utterance uses anaphor; gold params full
- Quality verifier rejects fully self-contained restated history
- Treat change/rename+add as order-sensitive (strict only)
- Never schedule create+remove or play+delete pairs
- Split system and github-cli off network/web families
- Treat create+open as order-sensitive (strict only)
- Void bulk-clear with toggle/set breakpoint
- Never schedule player playback with montage mutate
- Split montage and system.notify into own families
- Never schedule addPhotos+deleteAllMontages
- Ban clock-tab+cancelReminder, bluetooth+nightLight, wifi+touchpad
- Require cursor/caret phrasing for moveCursor line gold
- Treat list add+clear and any montage mutate+deleteAll as void-dependent
- Expand unbound chores: any windowsClock+cancelReminder, bluetooth+rotation,
  wifi+pointer input, in-app browser nav+external open, clear+create+deleteTaskFlow
- Mark login/logout order-sensitive; cover r015 confirmed flags in unit tests
- Treat visualStudio build+clean as void-dependent
- Expand unbound chores: bluetooth/wifi with input or rotation, in-app
  open+external open, list add/create triples with deleteTaskFlow,
  desktop launch/vdesk + pointer precision
- Cover r016 confirmed flags in unit tests
- Split desktop sub-schemas and root actions into network/shell/power/av/input/display
- Ban network or shell mixed with other desktop domains; list mutate+deleteTaskFlow
- Ban wifi/bluetooth with theme or power; shell+rotation; keep theme+scaling allowed
- Cover r017 confirmed flags in unit tests
- Reject ephemeral editor chrome (zen/fullscreen) paired with reloadWindow
- Reject network connect+disconnect pairs as void-dependent
- Mark connect/disconnect order-sensitive so any-order slots skip them
- Cover r018 flags 000074 and 000079 in multiAction tests
- Treat enable/disable pairs as void-dependent multi gold
- Ban any createList or list-mutate with deleteTaskFlow as unbound chore
- Cover r019 flags 000006 and 000027 in multiAction tests
- Set OPENAI_ENDPOINT/API_KEY from run-config + LOCAL_LITELLM when unset
- tb-generate/tb-eval call bootstrap before initRuntimeConfig
- History prompt: history state must match utterance presuppositions

Round-004 failed 100/100: probes threw Missing AZURE_OPENAI_ENDPOINT
while synthesizer used run-config endpoints via modelPool.
- Reject attached history when the request restates every action fact
- Add specialized code-file vs deprecated createFile confusables
- Lock prompt and sibling behavior with focused tests

Round-005 reached 98/100; only these two causes remained.
- Select semantically coherent 2-10 action compositions through LLM and Zod review.\n- Enforce self-contained rows, exact complete-run coverage, and meaningful strict order.\n- Persist contract-3 selection evidence while preserving contract-2 limits.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants