@@ -14,6 +14,9 @@ use super::artifact_optimizer::{
1414use super :: artifact_policy:: TaskArtifactPolicy ;
1515use super :: artifact_refs:: { automation_run_artifact_api, automation_run_artifacts_api} ;
1616use super :: backend:: { AgentTaskKind , AgentTaskRequest , AgentTaskResponse } ;
17+ use super :: outcomes:: {
18+ outcome_eval_definitions, outcome_feedback_section, AutomationOutcomesSnapshot ,
19+ } ;
1720use super :: run_ledger:: { AutomationRunArtifactKind , AutomationRunLedgerRecord } ;
1821use super :: text:: truncate_chars_for_prompt;
1922
@@ -26,6 +29,9 @@ pub(super) struct ArtifactPayloadContext<'a> {
2629 pub ( super ) request : & ' a AgentTaskRequest ,
2730 pub ( super ) response : & ' a AgentTaskResponse ,
2831 pub ( super ) record : & ' a AutomationRunLedgerRecord ,
32+ /// Post-approval outcomes of previously applied changes, when a snapshot
33+ /// has been recorded for this project.
34+ pub ( super ) outcomes : & ' a AutomationOutcomesSnapshot ,
2935}
3036
3137pub ( super ) struct GeneratedEvalPayloads {
@@ -35,6 +41,10 @@ pub(super) struct GeneratedEvalPayloads {
3541 pub ( super ) replay_results : Vec < Value > ,
3642 pub ( super ) status : & ' static str ,
3743 pub ( super ) validation_decision : & ' static str ,
44+ /// Evals derived from real post-approval outcomes; tracked separately
45+ /// from the validation-replay definitions so the replay gate semantics
46+ /// stay unchanged.
47+ pub ( super ) outcome_definitions : Vec < Value > ,
3848}
3949
4050pub ( super ) struct ImprovementGatePayload {
@@ -87,6 +97,7 @@ pub(super) fn feedback_payload(ctx: &ArtifactPayloadContext<'_>, trace_ref: &Val
8797 } ,
8898 "human" : [ ] ,
8999 "model" : validation_feedback_entries( ctx. record) ,
100+ "applied_change_outcomes" : outcome_feedback_section( ctx. task, ctx. outcomes) ,
90101 } )
91102}
92103
@@ -101,6 +112,7 @@ pub(super) fn generated_eval_payloads(ctx: &ArtifactPayloadContext<'_>) -> Gener
101112 replay_results,
102113 status : generated_evals_status ( count, runner_status) ,
103114 validation_decision : validation_gate_decision ( ctx. record ) ,
115+ outcome_definitions : outcome_eval_definitions ( ctx. task , ctx. task_key , ctx. outcomes ) ,
104116 }
105117}
106118
@@ -130,6 +142,7 @@ pub(super) fn generated_evals_payload(
130142 "eval_count" : evals. count,
131143 "accepted_count" : ctx. record. accepted_count,
132144 "rejected_count" : ctx. record. rejected_count,
145+ "outcome_eval_count" : evals. outcome_definitions. len( ) ,
133146 } ,
134147 "runner" : {
135148 "type" : "validation_replay" ,
@@ -160,6 +173,7 @@ pub(super) fn generated_evals_payload(
160173 "auto_apply" : false ,
161174 } ,
162175 "eval_definitions" : evals. definitions. clone( ) ,
176+ "outcome_eval_definitions" : evals. outcome_definitions. clone( ) ,
163177 "result_refs" : [ {
164178 "kind" : "validation_report" ,
165179 "hash" : validation_report_hash( ctx. record. validation_report. as_ref( ) ) ,
@@ -383,3 +397,161 @@ pub(super) fn codex_handoff_payload(
383397 "tests_to_run" : ctx. policy. handoff_tests( ) ,
384398 } )
385399}
400+
401+ #[ cfg( test) ]
402+ #[ allow( clippy:: unwrap_used, clippy:: expect_used) ]
403+ mod tests {
404+ use super :: super :: artifact_policy:: artifact_policy;
405+ use super :: super :: outcomes:: { SkillOutcomeRecord , SkillOutcomeVerdict } ;
406+ use super :: super :: run_ledger:: { AutomationRunStatus , AutomationTrigger } ;
407+ use super :: * ;
408+
409+ fn payload_fixture ( ) -> (
410+ AgentTaskRequest ,
411+ AgentTaskResponse ,
412+ AutomationRunLedgerRecord ,
413+ ) {
414+ let request = AgentTaskRequest :: new (
415+ "run-outcomes" . to_string ( ) ,
416+ AgentTaskKind :: SkillWriter ,
417+ "propose skills" . to_string ( ) ,
418+ Some ( "sha256:evidence" . to_string ( ) ) ,
419+ json ! ( { } ) ,
420+ ) ;
421+ let response = AgentTaskResponse {
422+ run_id : "run-outcomes" . to_string ( ) ,
423+ task : AgentTaskKind :: SkillWriter ,
424+ output_text : "{\" skills\" :[]}" . to_string ( ) ,
425+ output_json : Some ( json ! ( { "skills" : [ ] } ) ) ,
426+ model : None ,
427+ input_tokens : None ,
428+ output_tokens : None ,
429+ } ;
430+ let record = AutomationRunLedgerRecord {
431+ schema_version : 2 ,
432+ run_id : "run-outcomes" . to_string ( ) ,
433+ trigger : AutomationTrigger :: ManualCli ,
434+ task : AgentTaskKind :: SkillWriter ,
435+ task_key : Some ( "skill_writer" . to_string ( ) ) ,
436+ backend : "codex_app_server" . to_string ( ) ,
437+ host_mode : None ,
438+ prompt_version : None ,
439+ response_schema : None ,
440+ strict_json : None ,
441+ model : None ,
442+ status : AutomationRunStatus :: Succeeded ,
443+ evidence_hash : Some ( "sha256:evidence" . to_string ( ) ) ,
444+ input_hash : None ,
445+ output_hash : None ,
446+ proposed_ops : None ,
447+ applied_ops : None ,
448+ rejected_ops : None ,
449+ validation_report : None ,
450+ reviewed_count : 0 ,
451+ accepted_count : 0 ,
452+ rejected_count : 0 ,
453+ skipped_count : 0 ,
454+ fallback_status : None ,
455+ error : None ,
456+ error_classification : None ,
457+ error_retryable : None ,
458+ report_ref : None ,
459+ artifacts : Vec :: new ( ) ,
460+ started_at : "0" . to_string ( ) ,
461+ completed_at : "0" . to_string ( ) ,
462+ } ;
463+ ( request, response, record)
464+ }
465+
466+ fn outcomes_snapshot ( ) -> AutomationOutcomesSnapshot {
467+ AutomationOutcomesSnapshot {
468+ schema_version : 1 ,
469+ skills : vec ! [ SkillOutcomeRecord {
470+ skill_id: "ignored-skill" . to_string( ) ,
471+ title: Some ( "Ignored skill" . to_string( ) ) ,
472+ approved_at: 1_000 ,
473+ days_since_approval: 30 ,
474+ views_since_approval: 2 ,
475+ uses_since_approval: 0 ,
476+ verdict: SkillOutcomeVerdict :: Ignored ,
477+ } ] ,
478+ facts : Vec :: new ( ) ,
479+ skills_refreshed_at : Some ( 2_000 ) ,
480+ facts_refreshed_at : None ,
481+ }
482+ }
483+
484+ #[ test]
485+ fn feedback_payload_includes_applied_change_outcomes ( ) {
486+ let ( request, response, record) = payload_fixture ( ) ;
487+ let outcomes = outcomes_snapshot ( ) ;
488+ let ctx = ArtifactPayloadContext {
489+ run_id : "run-outcomes" ,
490+ task : AgentTaskKind :: SkillWriter ,
491+ task_key : "skill_writer" ,
492+ prompt_version : "skill_writer:v1" ,
493+ policy : artifact_policy ( AgentTaskKind :: SkillWriter ) ,
494+ request : & request,
495+ response : & response,
496+ record : & record,
497+ outcomes : & outcomes,
498+ } ;
499+
500+ let payload = feedback_payload ( & ctx, & json ! ( { "kind" : "traces" } ) ) ;
501+ let section = payload. get ( "applied_change_outcomes" ) . unwrap ( ) ;
502+ assert_eq ! ( section. get( "status" ) . unwrap( ) , & json!( "available" ) ) ;
503+ assert_eq ! (
504+ section. pointer( "/skill_verdicts/ignored" ) . unwrap( ) ,
505+ & json!( 1 )
506+ ) ;
507+ assert_eq ! (
508+ section. pointer( "/skills/0/skill_id" ) . unwrap( ) ,
509+ & json!( "ignored-skill" )
510+ ) ;
511+
512+ let evals = generated_eval_payloads ( & ctx) ;
513+ assert_eq ! ( evals. outcome_definitions. len( ) , 1 ) ;
514+ let generated = generated_evals_payload (
515+ & ctx,
516+ ( & json ! ( { "kind" : "traces" } ) , & json ! ( { "kind" : "feedback" } ) ) ,
517+ & evals,
518+ ) ;
519+ assert_eq ! (
520+ generated. pointer( "/summary/outcome_eval_count" ) . unwrap( ) ,
521+ & json!( 1 )
522+ ) ;
523+ assert_eq ! (
524+ generated
525+ . pointer( "/outcome_eval_definitions/0/observed_outcome" )
526+ . unwrap( ) ,
527+ & json!( "ignored" )
528+ ) ;
529+ // The validation-replay definitions must stay outcome-free so the
530+ // replay gate semantics are unchanged.
531+ assert_eq ! ( evals. count, 0 ) ;
532+ }
533+
534+ #[ test]
535+ fn empty_outcomes_snapshot_reports_none_recorded ( ) {
536+ let ( request, response, record) = payload_fixture ( ) ;
537+ let outcomes = AutomationOutcomesSnapshot :: default ( ) ;
538+ let ctx = ArtifactPayloadContext {
539+ run_id : "run-outcomes" ,
540+ task : AgentTaskKind :: SessionReflector ,
541+ task_key : "session_reflector" ,
542+ prompt_version : "session_reflector:v1" ,
543+ policy : artifact_policy ( AgentTaskKind :: SessionReflector ) ,
544+ request : & request,
545+ response : & response,
546+ record : & record,
547+ outcomes : & outcomes,
548+ } ;
549+
550+ let payload = feedback_payload ( & ctx, & json ! ( { "kind" : "traces" } ) ) ;
551+ assert_eq ! (
552+ payload. pointer( "/applied_change_outcomes/status" ) . unwrap( ) ,
553+ & json!( "no_outcomes_recorded" )
554+ ) ;
555+ assert ! ( generated_eval_payloads( & ctx) . outcome_definitions. is_empty( ) ) ;
556+ }
557+ }
0 commit comments