Skip to content

feat(verifiers): user_sim_judge grades the user sim of a run, record-only - #682

Open
sarah-chitty-1 wants to merge 13 commits into
sarahchitty/fd-5491-satisfaction-stepfrom
sarahchitty/fd-5490-user-sim-judge-v2
Open

sarah-chitty-1 wants to merge 13 commits into
sarahchitty/fd-5491-satisfaction-stepfrom
sarahchitty/fd-5490-user-sim-judge-v2

Conversation

@sarah-chitty-1

@sarah-chitty-1 sarah-chitty-1 commented Nov 14, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked on #681 (files in the opening, the rating step, the two new persona sections); merge that first.

A new verifier step, user_sim_judge, grades the user sim of a run (the AI playing the user), not the assistant (FD-5490). It covers always_on runs and prompt_agent chats with the built-in user sim (deploy_user_sim_agent).

It only records verdicts for now. Nothing marks a run or changes what graders read until the judge is calibrated against a person's labels. Marking is behind mark_user_sim_error, off by default.

What it does

  • Rebuilds the user sim's exact instructions from its recorded settings, and checks them against the recorded prompt_sha256.
  • Reads every check-in the user sim got and every answer it gave: always_on's stored timeline, or the chat's user_sim_reads entries.
  • Asks one narrow question per check, mapped to Anisha's User Simulator Rubric Taxonomy (PSI Bench SSOT):
Check Families
guidance: gave the requirements and decisions the assistant needed A1, A3
oversharing: leaked look-up facts, private details or credentials G1, G2
delegation: permission given or withheld per its rules; asks once about a missing "tell you after" item A4
style: sounds like the person, reacts in character, fits its moment F1–F4
rogue: acted like the assistant, broke character, let the agent off G3, G5
made_up: invented facts or contradicted itself A2
ending: said done at the right time G4
  • A task can add its own rubric_items under any family, in the shape of a criterion in a task's usersim.yaml (psi-benchmark-tasks turns one into the other). Each item rides along with the check that owns its family, at most 12 to a call:
rubric_persona_sha256: 3f1c...   # all 64 hex digits
rubric_items:
- {id: a1_call_rae, family: A1, kind: hard, name: "Rae: yes if it fits",
   check: "...", na_when: "The assistant didn't raise Rae's order."}
- {id: f2_her_length, family: F2, kind: auto, check: "Every reply is at most 25 words.", metric: max_words, max: 25}
  • kind: hard (an exact rule from the user sim's instructions, failed on any breach), general (judged), or auto (counted, not judged).
  • na_when: when the item never came up. always makes it n/a with no call and needs a static_na_reason; never means it always applies. An auto item takes only always.
  • Task items are tied to the persona, not the task version. rubric_persona_sha256 is the fingerprint of the persona and opening the items were written for (persona_fingerprint, which deploy_user_sim_agent records as the run's persona_sha256). If they differ, the task items aren't graded and the entry says why; the general checks still run. Other task edits don't affect it.
  • Satisfaction (A5) is the separate rate_user_satisfaction step (feat(user sim): files in the first message, and a rating step after the run #681).

Settings and output

  • One judge model, judge_model, defaulting to the same model rubrics_verifier uses. A run-wide --judge-model doesn't change it; only this step's own step_params do.
  • Each check writes its reason before its verdict. A fail that cites no graded turn, or whose reason ends by saying no failure was found, is asked again; a second bad answer is unknown.
  • Verdicts, reasons and cited turns go to metadata["user_sim_judge"][<verifier id>]. There's no verifications entry, so nothing mixes into the assistant's scores.
  • A run that ended in an error is skipped. A broken judge never fails the task (fail_task_on_error false).
  • The run's verdict is yes / no / unknown. Unknown means a check that could mark the run kept failing to answer; only yes marks, when marking is on. Style never marks a run on its own.
  • If the user sim's replies reach its max_replies, the judge is told the reply limit, which the user sim itself never sees, so the ending check can allow a short sign-off there.
  • Cost: 7 calls per run, roughly $0.08 for a short chat and $0.24 for a multi-day run on Sonnet, uncached.

Tests

tst/unit/task_step/verifiers/test_user_sim_judge.py (28) uses a fake model, with one always_on and one chat run. tst/unit/task_step and task/episode: 3,217 passed; the two test_mcp_cli_builder tests that need click were deselected. No paid calls.

Not done: calibration. The old test labels are AI-written, so the judge will be calibrated on a person's own labels of about 60 answers before marking is turned on.

🤖 Generated with Claude Code

RetriggerView in GreptileConfidence Score: 5/5

The PR appears safe to merge after its stated dependency, PR #681.

Summary

Adds a record-only judge for the user simulator, separate from the assistant’s scores.

  • Runs can now record a separate grade of the simulated user.

Reviews (12) · Last reviewed commit: "user_sim_judge: an auto item can't pass ..." · Reviewed by Greptile

sarah-chitty-1 and others added 4 commits November 12, 2026 13:55
…an always_on run

Started Nov 10 on the old user-sim branch and moved onto main unchanged, apart from the user sim modules' new
task_steps/agent/ path. Not reviewed or calibrated; the design is still open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…with one judge model, and records only

- Both kinds of run with the built-in user sim: an always_on step's stored timeline, or a prompt_agent step's
  user_sim_reads, each read whole from its object_url. run_step_id names the step; unset, the run must have one.
- The user sim's instructions are rebuilt from its user_sims record with user_sim_versions.setup, as
  deploy_user_sim_agent wrote them, and checked against the recorded prompt_sha256.
- One judge_model, defaulting to rubrics_verifier's (the config's judge role, else claude-sonnet-4-6); the two-judge
  split is gone. user_sim_error is yes / no / unknown (a check that could make it one couldn't be judged).
- Record only: mark_user_sim_error (off by default) sets the prompt response's error_type to user_sim_error, only
  when the instructions match the recorded hash. The step doesn't fail the task by default.
- Tests: a fake judge, one always_on and one prompt_agent run, every check's verdict, task items, the hash check,
  errored runs skipped, marking off by default and on when opted in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…k (G4)

- guidance: an unanswered question fails only when the assistant needs it answered; giving
  something again when asked is now a fail line. Self-contradiction moves to made_up.
- oversharing: something the assistant should know from its notes, credentials, and answering
  a may-I-share question against PRIVATE lines or rules.
- delegation: a broken "ask first" or "never" item must be objected to (the user sim's prompt
  always says so); a "tell you after" item left out of the final report must be asked about once.
- rogue: letting the assistant off for a broken limit or skipped step.
- style: same-length messages, described traits and caricature, missing pushback or reactions,
  and acting out of step with its situation (F4).
- ending (G4): a new check, run by default and able to mark a run. The step tells the judge
  when the run ended at the user sim's max_replies, which its instructions never mention.
- docstring: A5 is the separate rate_user_satisfaction step's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tty/fd-5490-user-sim-judge-v2

# Conflicts:
#	src/agent_env/task/episode/rules.py
@sarah-chitty-1
sarah-chitty-1 requested a review from a team as a code owner November 14, 2026 00:30
parse_answer kept a "fail" whose cited turns were all outside the graded
ones (turns=[] but could_change_grade still true), an unsupported failure
that could mark the run. A check's or task item's fail must now cite at
least one graded turn; otherwise the answer is not usable, so the judge is
asked once more and a second such answer gives the check (or item) the
verdict "error". Pass and n/a answers are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sarah-chitty-1 and others added 2 commits November 13, 2026 16:40
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…no failure is asked again

The answer format put the verdict first, so the judge committed to "fail" and then argued itself out of it ("No clear
failure found"); in 2 of 4 real runs that alone made the run a user sim error. The prompt and schema now ask for the
reason, then turns, then verdict and could_change_grade (items alike), and say to decide after reasoning, within the
40-word limit. As a backstop, a fail whose reason says no failure was found is not usable, so it is asked again once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sarah-chitty-1 and others added 2 commits November 13, 2026 17:39
…o failure; name the error-note limit

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An item has id, family, kind (hard / general / auto), check (text still
loads as its old name), name, short, na_when and, for auto, metric and max.
Hard items are told to the judge as exact rules, general ones fail only on a
clear instance. Auto items are counted on the graded answers with no model
call. na_when always is n/a with its static_na_reason and no call; never
asks an n/a again. A call takes at most MAX_ITEMS_PER_CALL (12) items; the
rest go in item-only calls. rubric_task_version is compared with the run's
task instance version: on a mismatch the items aren't graded and the entry
says so, the general checks still run. Bad items are refused with a clear
error per item.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sarah-chitty-1 and others added 3 commits November 13, 2026 18:05
…t's own path; auto items take only na_when always

The judge rebuilt the prompt from the record's opening text alone, so an opening with files never matched
prompt_sha256 (no "(you attached ...)" notes, no ATTACHED_KNOWN line). deploy_user_sim_agent's person() is now a
module function, and recorded_person() builds it from the record (opening_parts when present); the judge calls that
and user_sim_versions.setup, so the files and the new already_told / should_already_know sections rebuild as deployed.

An auto item is counted, never judged, so auto_result ignored any na_when but always: a conditional na_when did
nothing and na_when never still gave n/a with no messages. Any other value is now refused when the task loads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t, not the task version

Any edit to a hub task makes a new version, so rubric_task_version skipped a rubric after the next edit even when the
user sim's person didn't change. user_sim_common.persona_fingerprint hashes the persona's description, permissions and
keep_private and the opening_prompt as written (recipe in its docstring, pinned by a test, since
psi-benchmark-tasks's converter computes it too); deploy_user_sim_agent records it as persona_sha256. The judge's
rubric_persona_sha256 replaces rubric_task_version: a mismatch skips the task items and still runs the checks, a
match grades them, a run with no fingerprint is unknown and grades them. The task-instance lookup is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sarah-chitty-1
sarah-chitty-1 changed the base branch from main to sarahchitty/fd-5491-satisfaction-step November 14, 2026 02:17
…y the start of

When always_on's stored timeline can't be read, the judge falls back to the record's
timeline_tail, which keeps each message's first 300 characters. A length item counted
on that could pass an answer that was over its limit. Such answers are now marked cut;
over the max as kept is still a fail, otherwise the item is error (unknown), not pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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.

1 participant