S10 T2: label-agreement eval for AnswerSource and WrongCost::Assessor #1285
Labels
No labels
P0
P1
P2
P3
bug
category/code-change
category/cross-repo
category/eval-run
category/investigation
category/no-test-target
category/owner-ruling
category/prose-artifact
category/recurring-chore
category/structure
category/umbrella
crc-refactor
create
delete
enhancement
filed-by/agent
filed-by/user
frozen
lint-rule
needs-info
needs-triage
next
plugin/cc-architect
plugin/os
plugin/os-adr
plugin/os-aidd-lint
plugin/os-backlog
plugin/os-context
plugin/os-doc-hygiene
plugin/os-sdlc
plugin/os-vault
project/cc-os
ready-for-agent
ready-for-human
recurring
review
rule-proposal
update
waiting
wayfinder:grilling
wayfinder:map
wayfinder:map
wayfinder:research
wayfinder:task
wayfinder:task
wontfix
worklist/deviations
worklist/lint-rule
worklist/new-implement-build
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
jared/cc-os#1285
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
S10 T2: label-agreement eval for AnswerSource and WrongCost::Assessor
Problem
S10 tunes two row levers, and nothing measures them.
OsMe::AnswerSource(plugins/os-me/lib/os_me/answer_source.rb) asks one choice question with 5CRITERIAlines.OsMe::WrongCost::Assessor(plugins/os-me/lib/os_me/wrong_cost/assessor.rb) asks three yes/no (noul) questions from itsINSTRUCTIONS. An S10 iteration changes one of those lines. Without a harness, no iteration can be kept or discarded on a number, and no baseline exists.The
source,hard_to_undo,compounds, andstays_hiddenfields in~/dev/os-me-store/moments.jsonlarenullon all 355 rows, so no label exists to compare with. A separate labeling agent writes one label file for both levers. This ticket adds the harness that reads that file.Approach
One harness with a lever switch. One script,
plugins/os-me/eval/row_labels/run.rb --lever source|wrong_cost, over shared lib objects. Reason: both levers take the same input (OsMe::Row.new(text:), the same callOsMe::RowLabels.ofmakes), read the same label file, and use the same metrics, reps, and threads. The only differences are the questions, the label keys, and how to read the answer. Two small lever-check objects hold those differences. Two scripts would repeat the option parsing, the rep loop, and the output.The harness runs the real lever. It calls
AnswerSource#classifyandWrongCost::Assessor#assess, so a tuning iteration editsAnswerSource::CRITERIAorAssessor::INSTRUCTIONSin lib and runs again. There is no candidate YAML, so the tuned text does not have to be copied back into lib later (thereply_shape/tuned.yamlstep).Label file.
~/dev/os-me-store/samples/row-labels-YYYY-MM-DD.jsonl, not committed. One JSONL row per memo, with exactly these keys. One labeling pass labels both levers.id<session_id>:<ai_index>, theDecisionMoment.id_forform, unique in the filesplit"tune"or"holdout"OsMe::TuneSplit.new.holdout?(id) ? "holdout" : "tune"textEntry#assistant_textof the memo line, verbatim. The harness sends it asRow#textand reads no transcript.suretruewhen the labeler is sure of all four answerssourcerule,precedent,facts,experiment,taste(theAnswerSource::CRITERIAkeys)hard_to_undohard_to_undoinstructioncompoundscompoundsinstructionstays_hiddenstays_hiddeninstructionMalformed rows abort the run. A row is malformed when it is not a JSON object, its keys are not exactly the eight above,
idis not a string or repeats an earlier row,splitdiffers fromTuneSplit,textis not a string,sourceis not one of the five values, orsure,hard_to_undo,compounds, orstays_hiddenis nottrueorfalse. The error names the line number.Shared read.
ConfidenceLabel.readalready numbers lines, refuses held-out sessions in a tune run, and selects the split. Move that loop and the refusal intoOsMe::LabelFile, and call it fromConfidenceLabel.readandLeverLabel.read. Each label class keeps its own row checks. TheConfidenceLabeltests stay unchanged and green. Without this step, the second label class repeats those lines.Held-out refusal. The same as
ConfidenceLabel. With--split tune, a row whose sessionOsMe::Split#held_out?is true aborts the run, and the error names itsid. With--split holdout, the run accepts it. After the checks, only rows of the requested split remain.Questions per lever. The answers are strings, so one comparison serves both levers.
sourcesourcerule precedent facts experiment tastesourceclassify(row).source.to_swrong_costhard_to_undo,compounds,stays_hiddentrue false.to_sassess(row)field.to_swrong_costlevellow medium highWrongCost.new(**labels).level.to_sassess(row).level.to_sOne model call per row and rep, for each lever. When the call raises
Decision::Model::ResponseError, every question of that lever gets the predictionunreadablefor that row and rep. An unreadable answer is a miss.Metrics per question, over all reps of the requested split:
n= rows x reps.agreement= matching answers / n.kappa= Cohen's kappa over the same n pairs, with the expected agreement summed over the question's classes (as inreply_shape/run.rb). Anunreadableprediction is in no class. When the expected agreement is 1.0, kappa isniland printsn/a.unreadable= count ofunreadablepredictions.majority= rows whose most common prediction over the reps equals the label / rows. A tie is a miss.sure_nandsure_agreement= n and agreement on the rows withsure: true.%.1f%%, kappa as%.3f, and a zero denominator asn/a.Output. One stdout line per question, in the order of the table above, for example:
split=tune lever=source question=source rows=2 reps=3 n=6 agreement=33.3% kappa=-0.143 unreadable=1 majority=50.0% sure_n=3 sure_agreement=66.7%and a TSV at
--outwith the headerid rep question label predicted, one row per answer, sorted byid, thenrep, then question order. Flags:--lever,--labels,--split,--out(required),--reps(default 3),--jobs(default 8),--model(defaultOsMe::MomentExtractor::LABELER_MODEL). The script builds the client withOsMe::LabelerClient.for(OsMe::HeadlessClaude.new(runner: OsMe::HeadlessRunner.new, model:)). Bad arguments, an unknown lever, a malformed row, or a held-out refusal printerror: <message>on stderr and exit 2, before any model call.Required work:
LeverRun, writes the TSV, and prints oneAgreementScore#summaryline per question. check: presentLeverCheck.forinplugins/os-me/eval/row_labels/run.rbstays_hiddeninplugins/os-me/eval/row_labels/README.mdConfidenceLabel.readcallOsMe::LabelFilefor the line loop, the held-out refusal, and the split selection. check: presentLabelFileinplugins/os-me/lib/os_me/confidence_label.rbAcceptance criteria
Each test writes its label file to a
Dir.mktmpdirdirectory. It builds ids from generated session ids so thatOsMe::SplitandOsMe::TuneSplitgive the state the test needs (asconfidence_label_fixtures.rbdoes). The client isOsMe::LabelerClient.for(Decision::Model::Adapters::Fixed.new(...)). No test calls a model or the network.OsMe::LeverLabel.read(path, split: 'holdout')runs, Then it returns only the holdout row, and itsid,text,sure,source,hard_to_undo,compounds, andstays_hiddenequal the row values.surekey, orsource"guess", orhard_to_undo"yes", or asplitthat differs fromTuneSplit, or anidthat repeats line 1, WhenOsMe::LeverLabel.readruns, Then it raisesOsMe::Errorwhose message holdsline 2.OsMe::Split#held_out?is true, WhenLeverLabel.read(path, split: 'tune')runs, Then it raisesOsMe::Errorwhose message holds that row'sid; and WhenLeverLabel.read(path, split: 'holdout')runs on the same file, Then it does not raise.sourcefactsandtaste, and a Fixed adapter that answerssourcewith choicefacts, WhenOsMe::LeverRun.new(OsMe::LeverCheck.for('source', client), reps: 1, jobs: 1).results(labels)is scored byOsMe::AgreementScorefor the questionsource, Then agreement is 0.5, kappa is 0.0, and unreadable is 0.hard_to_undo,compounds,stays_hidden) = (true, false, true) and (false, false, false), and a Fixed adapter that answersnoul0.9, 0.2, and 0.9, When thewrong_costlever runs with reps 1, Then agreement is 0.5 forhard_to_undo,stays_hidden, andlevel, agreement is 1.0 forcompounds, and kappa forcompoundsis nil.Decision::Model::ResponseError), When thewrong_costlever runs with reps 2, Then each of the four questions has 2 results, all with the predictionunreadable, and theAgreementScoreof each has unreadable 2 and agreement 0.0.sourcelever with reps 3, WhenLeverRun#resultsruns with jobs 2, Then it returns 3 results whoserepvalues are 1, 2, and 3.sourceresults: row A (sure: true, labelfacts) predictedfacts,facts,tastein reps 1 to 3, and row B (sure: false, labeltaste) predictedfacts,unreadable,facts, WhenAgreementScore#summary(split: 'tune', lever: 'source')is called, Then it returnssplit=tune lever=source question=source rows=2 reps=3 n=6 agreement=33.3% kappa=-0.143 unreadable=1 majority=50.0% sure_n=3 sure_agreement=66.7%.cost, WhenOsMe::LeverCheck.for('cost', client)runs, Then it raisesOsMe::Errorwhose message holdscost.--lever costwith a valid file, Whenruby -I plugins/os-me/lib plugins/os-me/eval/row_labels/run.rb --lever LEVER --labels FILE --split tune --out OUTruns, Then it exits 2 and stderr starts witherror:.Owner sketch
Scope and blocking
OsMe::LeverLabel,OsMe::LabelFile,OsMe::LeverCheck(withSourceandCost),OsMe::LeverResult,OsMe::LeverRun,OsMe::AgreementScore, the move of the shared read out ofConfidenceLabel,plugins/os-me/eval/row_labels/{run.rb,README.md}, and tests underplugins/os-me/test.AnswerSource::CRITERIAorAssessor::INSTRUCTIONS(the S10 tuning loop's job after this lands); the label file, which a separate agent writes in the Store; a baseline run; changes toreply_shape/run.rb; a reversal-rate signal;.gitignorechanges in the Store.Origin
DESIGN(docs/decision-system/build-plan.mdS10).VERDICT: CREATE (user-approved; backlog search only)
Work started on branch ticket-1285 (os-sdlc implement, AFK decision-system build, S10 T2).
Resolution
Done: eval/row_labels/run.rb scores AnswerSource and WrongCost::Assessor against checked labels: agreement, kappa and unreadable count per question and split, over reps.
Evidence: Implementation 147: 8 dispatches, 0 retries, 8 min 36 s, implementation_complete. Suite gate green. Label file of 100 rows (83 tune, 17 holdout) written to the Store samples by an Opus labeler from train sessions.
Follow-ups: Baselines for both levers run next under S10 (eval-run, no ticket); tuning loops wait for Jared because each iteration costs about 300 Sonnet calls.