agent-apprenticeship 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +0 -197
  2. package/bin/agent-apprenticeship.js +85 -37
  3. package/package.json +1 -1
  4. package/pyproject.toml +1 -1
  5. package/src/agent_apprenticeship_trace/__init__.py +1 -1
  6. package/src/agent_apprenticeship_trace/actual_outputs_normalizer.py +104 -28
  7. package/src/agent_apprenticeship_trace/apprentice_adapters.py +3 -1
  8. package/src/agent_apprenticeship_trace/artifact_resolver.py +6 -0
  9. package/src/agent_apprenticeship_trace/bundle_exporter.py +132 -2
  10. package/src/agent_apprenticeship_trace/cli.py +1229 -269
  11. package/src/agent_apprenticeship_trace/codex_runner.py +89 -6
  12. package/src/agent_apprenticeship_trace/config.py +251 -32
  13. package/src/agent_apprenticeship_trace/ecosystem_r2.py +515 -0
  14. package/src/agent_apprenticeship_trace/env.py +5 -5
  15. package/src/agent_apprenticeship_trace/evaluator.py +127 -11
  16. package/src/agent_apprenticeship_trace/grader.py +12 -11
  17. package/src/agent_apprenticeship_trace/learning.py +270 -1
  18. package/src/agent_apprenticeship_trace/llm_output_normalizer.py +8 -7
  19. package/src/agent_apprenticeship_trace/loop.py +103 -4
  20. package/src/agent_apprenticeship_trace/loop_review.py +686 -0
  21. package/src/agent_apprenticeship_trace/mentor_checkpoints.py +49 -21
  22. package/src/agent_apprenticeship_trace/openai_structured.py +84 -14
  23. package/src/agent_apprenticeship_trace/package_exporter.py +1 -1
  24. package/src/agent_apprenticeship_trace/progress.py +2 -1
  25. package/src/agent_apprenticeship_trace/public_run.py +369 -57
  26. package/src/agent_apprenticeship_trace/public_sanitizer.py +40 -3
  27. package/src/agent_apprenticeship_trace/rubric_generation.py +11 -10
  28. package/src/agent_apprenticeship_trace/schemas.py +378 -3
  29. package/src/agent_apprenticeship_trace/task_intake.py +43 -9
  30. package/src/agent_apprenticeship_trace/tdo.py +2304 -0
  31. package/src/agent_apprenticeship_trace/trace_normalizer.py +351 -17
  32. package/src/agent_apprenticeship_trace/trace_prompt.py +38 -11
  33. package/src/agent_apprenticeship_trace/training_signals.py +1 -1
  34. package/src/agent_apprenticeship_trace/verifier.py +12 -6
@@ -24,33 +24,147 @@ class LLMEvaluatorOutput(BaseModel):
24
24
  revision_plan: RevisionPlan
25
25
 
26
26
  def deterministic_feedback(grader: GraderResult) -> EvaluatorFeedback:
27
- return EvaluatorFeedback(feedback_id=f'feedback_{grader.attempt_id}', task_id=grader.task_id, attempt_id=grader.attempt_id, target_actor='worker', feedback_type='criteria_failure' if grader.failed_criteria else 'other', failed_rubric_items=grader.failed_criteria, evidence_refs=grader.evidence_refs, artifact_refs=grader.evidence_refs, feedback_summary='Improve any failed rubric items and preserve valid artifacts.', actionable_feedback=['Check every required artifact exists.', 'Address failed criteria explicitly.'], suggested_revision='Create or repair missing/weak deliverables.', revision_priority='high' if grader.failed_criteria else 'low', confidence=0.75, hidden_reference_used=False, hidden_reference_leaked=False, failed_or_weak_rubric_items=grader.failed_criteria, artifact_specific_comments=[], trace_specific_comments=[], revision_plan='Create or repair missing/weak deliverables.', model=None, provider=None, metadata_json={})
27
+ failed = grader.failed_criteria if grader.score_source == 'structural_guardrail' else grader.failed_criteria
28
+ summary = 'Mentor evaluator unavailable; no outcome judgment was produced. Structural guardrails suggest preserving valid artifacts and repairing missing deliverables.'
29
+ return EvaluatorFeedback(feedback_id=f'feedback_{grader.attempt_id}', task_id=grader.task_id, attempt_id=grader.attempt_id, target_actor='apprentice', feedback_type='artifact_missing' if failed else 'other', failed_rubric_items=failed, evidence_refs=grader.evidence_refs, artifact_refs=grader.evidence_refs, feedback_summary=summary, actionable_feedback=['Preserve any valid generated artifacts.', 'Repair missing deliverables reported by structural guardrails.'] if failed else ['Preserve valid artifacts for any future revision.'], suggested_revision='Repair missing deliverables if the user or Mentor asks for a revision.', revision_priority='medium' if failed else 'low', confidence=0.35, hidden_reference_used=False, hidden_reference_leaked=False, failed_or_weak_rubric_items=failed, artifact_specific_comments=[], trace_specific_comments=[], revision_plan='Repair missing deliverables only if a revision is requested.', model=None, provider=None, metadata_json={'mentor_evaluator_status':'unavailable','structural_guidance':True})
28
30
 
29
31
  def _revision_plan_from_feedback(fb: EvaluatorFeedback, target_attempt_id: str) -> RevisionPlan:
30
32
  return RevisionPlan(revision_plan_id=f'revision_plan_{fb.task_id}', task_id=fb.task_id, source_attempt_id=fb.attempt_id, target_attempt_id=target_attempt_id, revision_kind='local_fix', revision_reason=fb.feedback_summary, failed_rubric_items=fb.failed_rubric_items, planned_changes=fb.actionable_feedback, expected_score_improvement=0.1, risk_of_regression='low', uses_evaluator_feedback=True, metadata_json={})
31
33
 
32
- def _evaluator_prompt(grader: GraderResult, verifier: VerifierResult, outputs: ActualOutputs, trace: AgentTrace | None, target_attempt_id: str, artifact_preview_bundle: dict | None=None) -> str:
34
+ def _latest_review_packet(package_root: Path | None, attempt_kind: str) -> dict:
35
+ if not package_root:
36
+ return {}
37
+ packets = sorted((package_root / "loops" / "iterations").glob("*/review_packet.json"))
38
+ if not packets:
39
+ return {}
40
+ preferred = []
41
+ for path in packets:
42
+ try:
43
+ packet = read_json(path)
44
+ except Exception:
45
+ continue
46
+ actual_ref = str(packet.get("actual_outputs_ref") or "")
47
+ if f"attempts/{attempt_kind}/" in actual_ref:
48
+ preferred.append((path, packet))
49
+ if preferred:
50
+ path, packet = preferred[-1]
51
+ packet = dict(packet)
52
+ packet["review_packet_ref"] = str(path.relative_to(package_root))
53
+ return packet
54
+ try:
55
+ packet = read_json(packets[-1])
56
+ except Exception:
57
+ return {}
58
+ packet = dict(packet)
59
+ packet["review_packet_ref"] = str(packets[-1].relative_to(package_root))
60
+ return packet
61
+
62
+
63
+ def _compact_trace_summary(trace: AgentTrace | None) -> dict:
64
+ if trace is None:
65
+ return {}
66
+ data = trace.model_dump()
67
+ steps = data.get("steps") or []
68
+ return {
69
+ "trace_id": data.get("trace_id"),
70
+ "attempt_id": data.get("attempt_id"),
71
+ "attempt_kind": data.get("attempt_kind"),
72
+ "step_count": len(steps),
73
+ "key_steps": [
74
+ {
75
+ "step": step.get("step"),
76
+ "operation": step.get("operation"),
77
+ "interaction_surface": step.get("interaction_surface"),
78
+ "environment_domain": step.get("environment_domain"),
79
+ "step_outcome": step.get("step_outcome"),
80
+ "artifact_refs": (step.get("artifact_refs") or [])[:5],
81
+ "state_change": str(step.get("state_change") or "")[:240],
82
+ }
83
+ for step in steps[:10]
84
+ if isinstance(step, dict) and step.get("action") != "user_message"
85
+ ],
86
+ }
87
+
88
+
89
+ def _compact_grader(grader: GraderResult) -> dict:
90
+ data = grader.model_dump()
91
+ return {
92
+ "grader_result_id": data.get("grader_result_id"),
93
+ "attempt_id": data.get("attempt_id"),
94
+ "passed": data.get("passed"),
95
+ "score": data.get("score"),
96
+ "final_score": data.get("final_score"),
97
+ "score_source": data.get("score_source"),
98
+ "score_reliability": data.get("score_reliability"),
99
+ "failed_criteria": data.get("failed_criteria") or [],
100
+ "evidence_refs": (data.get("evidence_refs") or [])[:20],
101
+ }
102
+
103
+
104
+ def _compact_verifier(verifier: VerifierResult) -> dict:
105
+ data = verifier.model_dump()
106
+ return {
107
+ "verifier_result_id": data.get("verifier_result_id"),
108
+ "attempt_id": data.get("attempt_id"),
109
+ "passed": data.get("passed"),
110
+ "verdict": data.get("verdict"),
111
+ "failed_checks": data.get("failed_checks") or data.get("failed_criteria") or [],
112
+ "evidence_refs": (data.get("evidence_refs") or [])[:20],
113
+ "notes": str(data.get("notes") or data.get("verifier_notes") or "")[:1000],
114
+ }
115
+
116
+
117
+ def _compact_actual(outputs: ActualOutputs) -> dict:
118
+ data = outputs.model_dump()
119
+ return {
120
+ "task_id": data.get("task_id"),
121
+ "attempt_id": data.get("attempt_id"),
122
+ "attempt_kind": data.get("attempt_kind"),
123
+ "status": data.get("status"),
124
+ "output_summary": data.get("output_summary"),
125
+ "primary_output_ref": data.get("primary_output_ref"),
126
+ "deliverable_refs": data.get("deliverable_refs") or [],
127
+ "artifact_refs": data.get("artifact_refs") or [],
128
+ "files_created": data.get("files_created") or [],
129
+ "error_type": data.get("error_type"),
130
+ "error_message": data.get("error_message"),
131
+ }
132
+
133
+
134
+ def _compact_artifact_previews(bundle: dict | None) -> dict:
135
+ bundle = bundle or {}
136
+ return {
137
+ "artifact_content_refs": (bundle.get("artifact_content_refs") or [])[:12],
138
+ "artifact_content_previews": (bundle.get("artifact_content_previews") or [])[:8],
139
+ "artifact_content_hashes": (bundle.get("artifact_content_hashes") or {}) if len(str(bundle.get("artifact_content_hashes") or {})) < 2000 else {},
140
+ "artifact_content_preview_truncated": bundle.get("artifact_content_preview_truncated"),
141
+ "model_grading_basis": bundle.get("model_grading_basis"),
142
+ }
143
+
144
+
145
+ def _evaluator_prompt(grader: GraderResult, verifier: VerifierResult, outputs: ActualOutputs, trace: AgentTrace | None, target_attempt_id: str, artifact_preview_bundle: dict | None=None, review_packet: dict | None=None) -> str:
33
146
  return """Return only valid JSON. Do not include markdown. Do not add extra top-level fields; place extras under metadata_json.extra_model_fields.
34
- Required skeleton: {"evaluator_feedback":{"feedback_id":"feedback_<task_id>_<attempt_kind>","task_id":"...","attempt_id":"...","target_actor":"worker","feedback_type":"other","failed_rubric_items":[],"evidence_refs":[],"artifact_refs":[],"feedback_summary":"...","actionable_feedback":[],"suggested_revision":"...","revision_priority":"medium","confidence":0.7,"hidden_reference_used":false,"hidden_reference_leaked":false,"failed_or_weak_rubric_items":[],"artifact_specific_comments":[],"trace_specific_comments":[],"metadata_json":{}},"revision_plan":{"revision_plan_id":"revision_plan_<task_id>","task_id":"...","source_attempt_id":"...","target_attempt_id":"...","revision_kind":"local_fix","revision_reason":"...","failed_rubric_items":[],"planned_changes":[],"risk_of_regression":"medium","uses_evaluator_feedback":true,"metadata_json":{}}}.
35
- Evaluate the attempt using grader/verifier outputs, actual outputs, artifact content previews, artifacts, and trace. Produce actionable feedback and a concrete revision plan.
36
- Target revised attempt id: """ + target_attempt_id + "\nGraderResult JSON:\n" + json.dumps(grader.model_dump(), sort_keys=True) + "\nVerifierResult JSON:\n" + json.dumps(verifier.model_dump(), sort_keys=True) + "\nActualOutputs JSON:\n" + json.dumps(outputs.model_dump(), sort_keys=True) + "\nArtifact content previews JSON:\n" + json.dumps(artifact_preview_bundle or {}, sort_keys=True)[:16000] + "\nTrace summary JSON:\n" + json.dumps((trace.model_dump() if trace else {}), sort_keys=True)[:12000]
147
+ Required skeleton: {"evaluator_feedback":{"feedback_id":"feedback_<task_id>_<attempt_kind>","task_id":"...","attempt_id":"...","target_actor":"apprentice","feedback_type":"other","failed_rubric_items":[],"evidence_refs":[],"artifact_refs":[],"feedback_summary":"...","actionable_feedback":[],"suggested_revision":"...","revision_priority":"medium","confidence":0.7,"hidden_reference_used":false,"hidden_reference_leaked":false,"failed_or_weak_rubric_items":[],"artifact_specific_comments":[],"trace_specific_comments":[],"metadata_json":{}},"revision_plan":{"revision_plan_id":"revision_plan_<task_id>","task_id":"...","source_attempt_id":"...","target_attempt_id":"...","revision_kind":"local_fix","revision_reason":"...","failed_rubric_items":[],"planned_changes":[],"risk_of_regression":"medium","uses_evaluator_feedback":true,"metadata_json":{}}}.
148
+ Evaluate the attempt by first reading the loop review packet when provided, then checking compact Mentor grader/verifier outputs, actual_outputs.json, artifact refs, artifact content previews, and trace evidence. Treat the review packet as the current iteration dossier: it summarizes outputs, artifacts, trace refs, rubric/verifier status, known failures, diff evidence, and recommended review focus. Produce grounded actionable feedback and a concrete revision plan whose revision guidance can be mapped to feedback_type/feedback_content in future traces. Keep the response concise. Do not rely on Apprentice Agent self-assessment or self evaluation as outcome evidence. Do not use deterministic fallback judgement; structural guardrails are integrity checks only and are not outcome judgments.
149
+ Target revised attempt id: """ + target_attempt_id + "\nLoop Review Packet JSON:\n" + json.dumps(review_packet or {}, sort_keys=True)[:5000] + "\nCompact GraderResult JSON:\n" + json.dumps(_compact_grader(grader), sort_keys=True) + "\nCompact VerifierResult JSON:\n" + json.dumps(_compact_verifier(verifier), sort_keys=True) + "\nCompact ActualOutputs JSON:\n" + json.dumps(_compact_actual(outputs), sort_keys=True) + "\nArtifact content previews JSON:\n" + json.dumps(_compact_artifact_previews(artifact_preview_bundle), sort_keys=True)[:5000] + "\nCompact Trace summary JSON:\n" + json.dumps(_compact_trace_summary(trace), sort_keys=True)[:3000]
37
150
 
38
151
  def evaluate_attempt(grader: GraderResult, verifier: VerifierResult, outputs: ActualOutputs, trace: AgentTrace | None, target_attempt_id: str, role_root: Path | None=None, package_root: Path | None=None) -> tuple[EvaluatorFeedback, RevisionPlan]:
39
152
  settings=get_settings(); role_root=role_root or Path('outputs/roles')
40
153
  artifact_preview_bundle=build_artifact_previews(package_root, grader.evidence_refs or outputs.deliverable_refs or outputs.artifact_refs or outputs.files_created)
41
- if _model_evaluator_active(settings) and _mentor_provider_can_attempt() and settings.llm_evaluator_enabled:
42
- prompt=_evaluator_prompt(grader, verifier, outputs, trace, target_attempt_id, artifact_preview_bundle)
154
+ review_packet=_latest_review_packet(package_root, outputs.attempt_kind)
155
+ if _model_evaluator_active(settings) and settings.llm_evaluator_enabled:
156
+ prompt=_evaluator_prompt(grader, verifier, outputs, trace, target_attempt_id, artifact_preview_bundle, review_packet)
43
157
  try:
44
158
  provider=_mentor_provider_id()
45
159
  model_override=settings.llm_evaluator_model if provider == 'openai' else None
46
- rr=run_structured_role('evaluator_agent', prompt, LLMEvaluatorOutput, role_root/'evaluator_agent'/outputs.attempt_kind, allow_fallback=settings.allow_deterministic_eval_fallback, model_override=model_override, normalizer_context={'task_id': outputs.task_id, 'attempt_id': outputs.attempt_id, 'attempt_kind': outputs.attempt_kind, 'target_attempt_id': target_attempt_id, 'grader_result_id': grader.grader_result_id, 'verifier_result_id': verifier.verifier_result_id, 'artifact_content_refs': artifact_preview_bundle.get('artifact_content_refs'), 'artifact_content_previews': artifact_preview_bundle.get('artifact_content_previews'), 'artifact_content_hashes': artifact_preview_bundle.get('artifact_content_hashes'), 'artifact_content_preview_truncated': artifact_preview_bundle.get('artifact_content_preview_truncated'), 'model_grading_basis': artifact_preview_bundle.get('model_grading_basis'), 'model': model_override or settings.model_provider_model, 'provider':provider})
160
+ rr=run_structured_role('evaluator_agent', prompt, LLMEvaluatorOutput, role_root/'evaluator_agent'/outputs.attempt_kind, allow_fallback=settings.allow_deterministic_eval_fallback, model_override=model_override, normalizer_context={'task_id': outputs.task_id, 'attempt_id': outputs.attempt_id, 'attempt_kind': outputs.attempt_kind, 'target_attempt_id': target_attempt_id, 'grader_result_id': grader.grader_result_id, 'verifier_result_id': verifier.verifier_result_id, 'artifact_content_refs': artifact_preview_bundle.get('artifact_content_refs'), 'artifact_content_previews': artifact_preview_bundle.get('artifact_content_previews'), 'artifact_content_hashes': artifact_preview_bundle.get('artifact_content_hashes'), 'artifact_content_preview_truncated': artifact_preview_bundle.get('artifact_content_preview_truncated'), 'model_grading_basis': artifact_preview_bundle.get('model_grading_basis'), 'review_packet_ref': review_packet.get('review_packet_ref'), 'model': model_override or settings.model_provider_model, 'provider':provider})
47
161
  if rr.live_call_ok and rr.structured_output_validation_ok:
48
162
  parsed=read_json(role_root/'evaluator_agent'/outputs.attempt_kind/'parsed_output.json')
49
163
  fb=EvaluatorFeedback.model_validate(parsed['evaluator_feedback'])
50
164
  rp=RevisionPlan.model_validate(parsed['revision_plan'])
51
165
  fb.provider=rr.provider; fb.model=rr.model; fb.revision_plan=fb.revision_plan or rp.revision_reason
52
- fb.metadata_json.update({'llm_prompt_ref_internal':str(role_root/'evaluator_agent'/outputs.attempt_kind/'prompt.md'),'llm_response_ref_internal':str(role_root/'evaluator_agent'/outputs.attempt_kind/'raw_output.txt'),'prompt_hash':sha256_text(prompt),'public_response_summary':'Model evaluator generated actionable feedback and revision plan.', **artifact_preview_bundle})
53
- rp.metadata_json.update({'provider':rr.provider,'model':rr.model,'llm_prompt_ref_internal':str(role_root/'evaluator_agent'/outputs.attempt_kind/'prompt.md'),'llm_response_ref_internal':str(role_root/'evaluator_agent'/outputs.attempt_kind/'raw_output.txt')})
166
+ fb.metadata_json.update({'llm_prompt_ref_internal':str(role_root/'evaluator_agent'/outputs.attempt_kind/'prompt.md'),'llm_response_ref_internal':str(role_root/'evaluator_agent'/outputs.attempt_kind/'raw_output.txt'),'prompt_hash':sha256_text(prompt),'public_response_summary':'Mentor Model evaluator generated grounded feedback and a revision plan from the loop review packet and package evidence.','review_source':'review_packet','review_packet_ref':review_packet.get('review_packet_ref'), **artifact_preview_bundle})
167
+ rp.metadata_json.update({'provider':rr.provider,'model':rr.model,'review_source':'review_packet','review_packet_ref':review_packet.get('review_packet_ref'),'llm_prompt_ref_internal':str(role_root/'evaluator_agent'/outputs.attempt_kind/'prompt.md'),'llm_response_ref_internal':str(role_root/'evaluator_agent'/outputs.attempt_kind/'raw_output.txt')})
54
168
  return fb,rp
55
169
  if settings.evaluation_mode == 'llm_required' or settings.llm_fail_closed:
56
170
  raise RuntimeError(rr.error_message or 'Model evaluator failed')
@@ -59,6 +173,8 @@ def evaluate_attempt(grader: GraderResult, verifier: VerifierResult, outputs: Ac
59
173
  raise
60
174
  fb=deterministic_feedback(grader)
61
175
  rp=_revision_plan_from_feedback(fb, target_attempt_id)
176
+ fb.metadata_json.update({'review_source':'review_packet', 'review_packet_ref':review_packet.get('review_packet_ref')})
177
+ rp.metadata_json.update({'review_source':'review_packet', 'review_packet_ref':review_packet.get('review_packet_ref')})
62
178
  if _mentor_provider_can_attempt() and settings.llm_evaluator_enabled and settings.evaluation_mode != 'deterministic_only':
63
- fb.metadata_json.update({'model_evaluator_enabled': True, 'llm_evaluator_enabled': True, 'llm_unavailable': True, 'deterministic_fallback': True, **artifact_preview_bundle})
179
+ fb.metadata_json.update({'model_evaluator_enabled': True, 'llm_evaluator_enabled': True, 'llm_unavailable': True, 'mentor_evaluator_status': 'unavailable', 'review_source':'review_packet', 'review_packet_ref':review_packet.get('review_packet_ref'), **artifact_preview_bundle})
64
180
  return fb,rp
@@ -124,15 +124,15 @@ def deterministic_grade(rubric: RubricSpec, outputs: ActualOutputs, attempt_kind
124
124
  item_evidence=[e for e in evidence if Path(e).name in req] or evidence
125
125
  item_missing=[m for m in missing if m in req]
126
126
  ok = outputs.status == 'success' and not item_missing and bool(item_evidence)
127
- item_scores.append(RubricItemScore(rubric_item_id=item.rubric_item_id, criterion_name=item.criterion_name, score=1.0 if ok else 0.0, max_score=1.0, passed=ok, evidence_refs=item_evidence, failure_mode=None if ok else 'missing_artifact', notes='Deterministic artifact-contract precheck only; semantic correctness is not evaluated.', confidence=0.8, artifact_presence_ok=ok, semantic_correctness_score=None, reasoning_summary='Required artifact paths were resolved.' if ok else 'One or more required artifacts were not resolved.', improvement_suggestion=None if ok else 'Create the missing required artifact files.'))
127
+ item_scores.append(RubricItemScore(rubric_item_id=item.rubric_item_id, criterion_name=item.criterion_name, score=1.0 if ok else 0.0, max_score=1.0, passed=ok, evidence_refs=item_evidence, failure_mode=None if ok else 'missing_artifact', notes='Structural artifact-contract guardrail only; semantic correctness is not judged.', confidence=0.8, artifact_presence_ok=ok, semantic_correctness_score=None, reasoning_summary='Required artifact paths were resolved.' if ok else 'One or more required artifacts were not resolved.', improvement_suggestion=None if ok else 'Create the missing required artifact files.'))
128
128
  score=artifact_contract_score
129
- return GraderResult(grader_result_id=f'grader_{outputs.attempt_id}', task_id=outputs.task_id, attempt_id=outputs.attempt_id, attempt_kind=attempt_kind, rubric_id=rubric.rubric_id, grader_kind='deterministic', score_source='deterministic_artifact_contract', score=score, max_score=1.0, passed=score>=rubric.pass_threshold, rubric_item_scores=item_scores, failed_criteria=[s.rubric_item_id for s in item_scores if not s.passed], passed_criteria=[s.rubric_item_id for s in item_scores if s.passed], evidence_refs=evidence, confidence=0.8, reasoning_summary='Deterministic precheck verifies artifact presence/readiness only; semantic correctness is not evaluated.', limitations=['semantic_correctness_not_evaluated'], hidden_reference_used=False, hidden_reference_leaked=False, artifact_contract_score=artifact_contract_score, semantic_score=None, final_score=score, model=None, provider=None, deterministic_precheck_ref=None, public_response_summary='Artifact-contract precheck completed.', metadata_json={'artifact_contract_passed': artifact_contract_passed, 'semantic_correctness_not_evaluated': True, 'missing_artifacts': missing, **match})
129
+ return GraderResult(grader_result_id=f'grader_{outputs.attempt_id}', task_id=outputs.task_id, attempt_id=outputs.attempt_id, attempt_kind=attempt_kind, rubric_id=rubric.rubric_id, grader_kind='structural_guardrail', score_source='structural_guardrail', score=score, max_score=1.0, passed=artifact_contract_passed, rubric_item_scores=item_scores, failed_criteria=[s.rubric_item_id for s in item_scores if not s.passed], passed_criteria=[s.rubric_item_id for s in item_scores if s.passed], evidence_refs=evidence, confidence=0.8, reasoning_summary='Structural guardrail checked artifact presence/readiness only; Mentor grading is unavailable unless a Mentor Model or human grader runs.', limitations=['semantic_quality_not_judged','mentor_grade_unavailable'], hidden_reference_used=False, hidden_reference_leaked=False, artifact_contract_score=artifact_contract_score, semantic_score=None, final_score=score, model=None, provider=None, deterministic_precheck_ref=None, public_response_summary='Structural artifact-contract guardrail completed.', score_reliability='needs_review', metadata_json={'structural_guardrail': True, 'role_status': 'mentor_grader_unavailable', 'artifact_contract_passed': artifact_contract_passed, 'semantic_quality_not_judged': True, 'missing_artifacts': missing, **match})
130
130
 
131
131
  def _grader_prompt(rubric: RubricSpec, outputs: ActualOutputs, trace: AgentTrace | None, precheck: GraderResult, artifact_preview_bundle: dict | None=None) -> str:
132
132
  return """Return only valid JSON matching GraderResult. Do not include markdown.
133
- You are the model-based grader. Score each rubric item using the task rubric, attempt trace, actual outputs, and artifact content previews when available. Return a JSON object with exactly these required top-level fields: grader_result_id, task_id, attempt_id, attempt_kind, rubric_id, grader_kind, score_source, artifact_contract_score, model_score, semantic_score, final_score, score, max_score, passed, confidence, evidence_refs, rubric_item_scores, limitations, hidden_reference_used, hidden_reference_leaked, metadata_json. Each rubric_item_scores entry must include artifact_presence_ok, semantic_correctness_score, evidence_refs, reasoning_summary, confidence, failure_mode, and improvement_suggestion. Cite evidence_refs, distinguish artifact presence from model-judged correctness, and set score_source=model_judged and grader_kind=model. Deterministic artifact-contract precheck is auxiliary only.
133
+ You are the Mentor Model grader. Score each rubric item using the Apprentice-generated plus Mentor-audited task rubric, attempt trace, actual_outputs.json, artifact refs, and artifact content previews when available. Return a JSON object with exactly these required top-level fields: grader_result_id, task_id, attempt_id, attempt_kind, rubric_id, grader_kind, score_source, artifact_contract_score, model_score, semantic_score, final_score, score, max_score, passed, confidence, evidence_refs, rubric_item_scores, limitations, hidden_reference_used, hidden_reference_leaked, metadata_json. Each rubric_item_scores entry must include artifact_presence_ok, semantic_correctness_score, evidence_refs, reasoning_summary, confidence, failure_mode, and improvement_suggestion. Cite evidence_refs from actual_outputs/artifacts/trace, distinguish artifact presence from Mentor-judged correctness, and set score_source=model_judged and grader_kind=model. Do not rely on Apprentice Agent self-assessment or self evaluation. Do not use deterministic fallback judgement; structural artifact-contract guardrails are auxiliary only and are not grader judgments.
134
134
  Rubric JSON:
135
- """ + json.dumps(rubric.model_dump(), sort_keys=True) + "\nActualOutputs JSON:\n" + json.dumps(outputs.model_dump(), sort_keys=True) + "\nArtifact content previews JSON:\n" + json.dumps(artifact_preview_bundle or {}, sort_keys=True)[:16000] + "\nTrace summary JSON:\n" + json.dumps((trace.model_dump() if trace else {}), sort_keys=True)[:12000] + "\nDeterministic precheck JSON:\n" + json.dumps(precheck.model_dump(), sort_keys=True)
135
+ """ + json.dumps(rubric.model_dump(), sort_keys=True) + "\nActualOutputs JSON:\n" + json.dumps(outputs.model_dump(), sort_keys=True) + "\nArtifact content previews JSON:\n" + json.dumps(artifact_preview_bundle or {}, sort_keys=True)[:16000] + "\nTrace summary JSON:\n" + json.dumps((trace.model_dump() if trace else {}), sort_keys=True)[:12000] + "\nStructural guardrail JSON:\n" + json.dumps(precheck.model_dump(), sort_keys=True)
136
136
 
137
137
  def grade_attempt(rubric: RubricSpec, outputs: ActualOutputs, attempt_kind: str, trace: AgentTrace | None=None, role_root: Path | None=None, package_root: Path | None=None) -> GraderResult:
138
138
  settings=get_settings()
@@ -140,10 +140,11 @@ def grade_attempt(rubric: RubricSpec, outputs: ActualOutputs, attempt_kind: str,
140
140
  role_root=role_root or Path('outputs/roles')
141
141
  artifact_preview_bundle=build_artifact_previews(package_root, pre.evidence_refs or outputs.deliverable_refs or outputs.artifact_refs or outputs.files_created)
142
142
  pre.metadata_json.update(artifact_preview_bundle)
143
- if _model_grading_active(settings) and _mentor_provider_can_attempt() and settings.llm_grader_enabled:
143
+ if _model_grading_active(settings) and settings.llm_grader_enabled:
144
144
  prompt=_grader_prompt(rubric, outputs, trace, pre, artifact_preview_bundle)
145
145
  (role_root/'grader_agent'/attempt_kind).mkdir(parents=True, exist_ok=True)
146
- write_json(role_root/'grader_agent'/attempt_kind/'deterministic_precheck.json', pre)
146
+ structural_ref = role_root/'grader_agent'/attempt_kind/'structural_guardrail.json'
147
+ write_json(structural_ref, pre)
147
148
  try:
148
149
  provider=_mentor_provider_id()
149
150
  model_override=settings.llm_grader_model if provider == 'openai' else None
@@ -151,8 +152,8 @@ def grade_attempt(rubric: RubricSpec, outputs: ActualOutputs, attempt_kind: str,
151
152
  if rr.live_call_ok and rr.structured_output_validation_ok:
152
153
  parsed=read_json(role_root/'grader_agent'/attempt_kind/'parsed_output.json')
153
154
  g=GraderResult.model_validate(parsed)
154
- g.grader_kind='model'; g.score_source='model_judged'; g.legacy_score_source='llm_semantic'; g.artifact_contract_score=pre.artifact_contract_score; g.semantic_score=g.semantic_score if g.semantic_score is not None else g.score; g.model_score=g.model_score if g.model_score is not None else g.semantic_score; g.legacy_semantic_score=g.legacy_semantic_score if g.legacy_semantic_score is not None else g.semantic_score; g.final_score=g.final_score if g.final_score is not None else g.semantic_score; g.score=g.final_score; g.model=rr.model; g.provider=rr.provider; g.deterministic_precheck_ref=str(role_root/'grader_agent'/attempt_kind/'deterministic_precheck.json'); g.llm_prompt_ref_internal=str(role_root/'grader_agent'/attempt_kind/'prompt.md'); g.llm_response_ref_internal=str(role_root/'grader_agent'/attempt_kind/'raw_output.txt'); g.public_prompt_hash=sha256_text(prompt); g.public_response_summary=g.public_response_summary or 'Model grader scored rubric items with evidence refs and artifact previews when available.'
155
- g.metadata_json.update({'deterministic_precheck': pre.model_dump(), **artifact_preview_bundle, 'llm_role_result_ref_internal':str(role_root/'grader_agent'/attempt_kind/'role_result.json')})
155
+ g.grader_kind='model'; g.score_source='model_judged'; g.legacy_score_source='llm_semantic'; g.artifact_contract_score=pre.artifact_contract_score; g.semantic_score=g.semantic_score if g.semantic_score is not None else g.score; g.model_score=g.model_score if g.model_score is not None else g.semantic_score; g.legacy_semantic_score=g.legacy_semantic_score if g.legacy_semantic_score is not None else g.semantic_score; g.final_score=g.final_score if g.final_score is not None else g.semantic_score; g.score=g.final_score; g.model=rr.model; g.provider=rr.provider; g.deterministic_precheck_ref=None; g.llm_prompt_ref_internal=str(role_root/'grader_agent'/attempt_kind/'prompt.md'); g.llm_response_ref_internal=str(role_root/'grader_agent'/attempt_kind/'raw_output.txt'); g.public_prompt_hash=sha256_text(prompt); g.public_response_summary=g.public_response_summary or 'Mentor Model grader scored rubric items with evidence refs and artifact previews when available.'
156
+ g.metadata_json.update({'structural_guardrail_ref': str(structural_ref), 'structural_guardrail': pre.model_dump(), **artifact_preview_bundle, 'llm_role_result_ref_internal':str(role_root/'grader_agent'/attempt_kind/'role_result.json')})
156
157
  return g
157
158
  if settings.evaluation_mode == 'llm_required' or settings.llm_fail_closed:
158
159
  raise RuntimeError(rr.error_message or 'Model grader failed')
@@ -162,11 +163,11 @@ def grade_attempt(rubric: RubricSpec, outputs: ActualOutputs, attempt_kind: str,
162
163
  if settings.evaluation_mode == 'deterministic_only':
163
164
  pre.semantic_score=None; pre.final_score=pre.score
164
165
  elif _mentor_provider_can_attempt() and settings.llm_grader_enabled:
165
- pre.grader_kind='hybrid'; pre.score_source='deterministic_fallback'; pre.semantic_score=None; pre.final_score=pre.score
166
+ pre.grader_kind='structural_guardrail'; pre.score_source='structural_guardrail'; pre.semantic_score=None; pre.final_score=pre.score
166
167
  provider=_mentor_provider_id()
167
168
  pre.model=settings.model_provider_model or settings.llm_grader_model; pre.provider=provider
168
- pre.limitations.append('Model grader unavailable; deterministic fallback explicitly marked.')
169
- pre.metadata_json.update({'llm_grader_enabled': True, 'llm_unavailable': True, 'deterministic_fallback': True})
169
+ pre.limitations.append('Mentor Model grader unavailable; only structural guardrails ran.')
170
+ pre.metadata_json.update({'llm_grader_enabled': True, 'llm_unavailable': True, 'role_status': 'mentor_grader_unavailable'})
170
171
  return pre
171
172
 
172
173
 
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import hashlib
4
4
  import json
5
5
  import os
6
+ import re
6
7
  import shutil
7
8
  from datetime import datetime, timezone
8
9
  from pathlib import Path
@@ -10,7 +11,7 @@ from typing import Any
10
11
 
11
12
  from .config import Settings, get_settings
12
13
  from .env import contains_secret
13
- from .io import read_json, read_jsonl, write_json
14
+ from .io import append_jsonl, read_json, read_jsonl, write_json
14
15
 
15
16
 
16
17
  PACK_STATUSES = {"draft", "active", "reverted", "removed"}
@@ -69,6 +70,18 @@ def experience_root(settings: Settings | None = None) -> Path:
69
70
  return root
70
71
 
71
72
 
73
+ def installed_skills_root(settings: Settings | None = None) -> Path:
74
+ s = settings or get_settings()
75
+ root = s.app_home / "installed_skills"
76
+ root.mkdir(parents=True, exist_ok=True)
77
+ return root
78
+
79
+
80
+ def installed_skills_index_path(settings: Settings | None = None) -> Path:
81
+ s = settings or get_settings()
82
+ return s.app_home / "installed_skills_index.jsonl"
83
+
84
+
72
85
  def _load_json_or_jsonl(path: Path) -> list[dict[str, Any]]:
73
86
  if not path.exists():
74
87
  return []
@@ -620,6 +633,262 @@ def compare_replay(before: dict[str, Any], after: dict[str, Any]) -> dict[str, A
620
633
  }
621
634
 
622
635
 
636
+ def _find_bundle_root(path: Path) -> Path:
637
+ path = path.expanduser()
638
+ if path.is_file():
639
+ path = path.parent
640
+ candidates = [
641
+ path,
642
+ path / "contribution_bundle",
643
+ path / "package",
644
+ ]
645
+ for candidate in candidates:
646
+ if (candidate / "contribution_manifest.json").exists() or (candidate / "experience_compiler").exists():
647
+ return candidate
648
+ for candidate in path.rglob("contribution_manifest.json"):
649
+ return candidate.parent
650
+ raise FileNotFoundError(f"Experience Compilation not found: {path}")
651
+
652
+
653
+ def _safe_copy_file(src: Path, dst: Path, files: list[str], root: Path) -> None:
654
+ if not src.exists() or not src.is_file():
655
+ return
656
+ dst.parent.mkdir(parents=True, exist_ok=True)
657
+ shutil.copy2(src, dst)
658
+ files.append(dst.relative_to(root).as_posix())
659
+
660
+
661
+ def _safe_copytree(src: Path, dst: Path, files: list[str], root: Path) -> None:
662
+ if not src.exists() or not src.is_dir():
663
+ return
664
+ shutil.copytree(src, dst, dirs_exist_ok=True)
665
+ for file_path in sorted(dst.rglob("*")):
666
+ if file_path.is_file():
667
+ files.append(file_path.relative_to(root).as_posix())
668
+
669
+
670
+ def _read_manifest_field(bundle: Path, key: str, default: Any = None) -> Any:
671
+ for rel in ["contribution_manifest.json", "experience_compiler/compiler_manifest.json"]:
672
+ path = bundle / rel
673
+ if path.exists():
674
+ try:
675
+ data = read_json(path)
676
+ except Exception:
677
+ continue
678
+ if isinstance(data, dict) and data.get(key) is not None:
679
+ return data.get(key)
680
+ return default
681
+
682
+
683
+ def install_runtime_training_from_bundle(
684
+ source_item_id: str,
685
+ bundle_path: Path,
686
+ *,
687
+ settings: Settings | None = None,
688
+ agent_target: str = "current",
689
+ dry_run: bool = False,
690
+ ) -> dict[str, Any]:
691
+ """Install runtime-useful Experience Compiler outputs into the local registry."""
692
+ s = settings or get_settings()
693
+ bundle = _find_bundle_root(bundle_path)
694
+ compiler = bundle / "experience_compiler"
695
+ skill_dir = compiler / "skill_pack"
696
+ runtime_dir = compiler / "runtime_learning_package"
697
+ skill_md = skill_dir / "skill.md"
698
+ if not skill_md.exists():
699
+ raise FileNotFoundError(f"Runtime training skill.md not found in {bundle}")
700
+ package_id = str(_read_manifest_field(bundle, "bundle_id") or _read_manifest_field(bundle, "source_package_id") or bundle.name)
701
+ title = str(_read_manifest_field(bundle, "title") or _read_manifest_field(bundle, "source_task_id") or source_item_id)
702
+ digest = hashlib.sha256(f"{source_item_id}|{package_id}|{skill_md.read_text(errors='replace')[:5000]}".encode()).hexdigest()[:10]
703
+ installed_skill_id = f"aa-skill-{slugify(title, 'runtime-training')}-{digest}"
704
+ target = installed_skills_root(s) / installed_skill_id
705
+ files: list[str] = []
706
+ if dry_run:
707
+ return {
708
+ "installed_skill_id": installed_skill_id,
709
+ "source_item_id": source_item_id,
710
+ "source_package_id": package_id,
711
+ "dry_run": True,
712
+ "would_install": [
713
+ "skill.md",
714
+ "canonical_skill.json",
715
+ "retrieval_card.json",
716
+ "runtime_learning_package/",
717
+ "install_manifest.json",
718
+ ],
719
+ "target": str(target),
720
+ }
721
+ if target.exists():
722
+ shutil.rmtree(target)
723
+ target.mkdir(parents=True, exist_ok=True)
724
+ _safe_copy_file(skill_md, target / "skill.md", files, target)
725
+ _safe_copytree(runtime_dir, target / "runtime_learning_package", files, target)
726
+ for src_name, dst_name in [
727
+ ("canonical_skill.json", "canonical_skill.json"),
728
+ ("retrieval_card.json", "retrieval_card.json"),
729
+ ("skill_card.json", "skill_card.json"),
730
+ ("verifier_spec.json", "verifier_spec.json"),
731
+ ("verifier_checklist.json", "verifier_checklist.json"),
732
+ ("tool_recipe.json", "tool_recipe.json"),
733
+ ]:
734
+ _safe_copy_file(skill_dir / src_name, target / dst_name, files, target)
735
+ if not (target / "canonical_skill.json").exists():
736
+ write_json(
737
+ target / "canonical_skill.json",
738
+ {
739
+ "skill_id": installed_skill_id,
740
+ "title": title,
741
+ "source_item_id": source_item_id,
742
+ "source_package_id": package_id,
743
+ "skill_ref": "skill.md",
744
+ },
745
+ )
746
+ files.append("canonical_skill.json")
747
+ if not (target / "retrieval_card.json").exists():
748
+ write_json(
749
+ target / "retrieval_card.json",
750
+ {
751
+ "skill_id": installed_skill_id,
752
+ "title": title,
753
+ "domains": _read_manifest_field(bundle, "domains", []) or [],
754
+ "source_item_id": source_item_id,
755
+ "summary": f"Runtime training compiled from {title}.",
756
+ },
757
+ )
758
+ files.append("retrieval_card.json")
759
+ manifest = {
760
+ "installed_skill_id": installed_skill_id,
761
+ "source_item_id": source_item_id,
762
+ "source_package_id": package_id,
763
+ "source_url": _read_manifest_field(bundle, "source_url"),
764
+ "agent_target": agent_target,
765
+ "install_mode": "agent_apprenticeship_runtime",
766
+ "installed_at": utc_now(),
767
+ "enabled": True,
768
+ "title": title,
769
+ "files_installed": sorted(set(files + ["install_manifest.json"])),
770
+ "backup_refs": [],
771
+ "reversible": True,
772
+ "generation_notes": ["native_agent_files_unchanged"],
773
+ }
774
+ write_json(target / "install_manifest.json", manifest)
775
+ append_jsonl(installed_skills_index_path(s), manifest)
776
+ return manifest
777
+
778
+
779
+ def list_installed_skills(settings: Settings | None = None, include_disabled: bool = True) -> list[dict[str, Any]]:
780
+ root = installed_skills_root(settings)
781
+ rows: list[dict[str, Any]] = []
782
+ for path in sorted(root.iterdir()) if root.exists() else []:
783
+ manifest = path / "install_manifest.json"
784
+ if not manifest.exists():
785
+ continue
786
+ try:
787
+ data = read_json(manifest)
788
+ except Exception:
789
+ continue
790
+ if not include_disabled and not data.get("enabled", True):
791
+ continue
792
+ data = dict(data)
793
+ data["path"] = str(path)
794
+ rows.append(data)
795
+ return rows
796
+
797
+
798
+ def _installed_skill_path(installed_skill_id: str, settings: Settings | None = None) -> Path:
799
+ path = installed_skills_root(settings) / installed_skill_id
800
+ if not (path / "install_manifest.json").exists():
801
+ raise FileNotFoundError(f"Installed runtime training not found: {installed_skill_id}")
802
+ return path
803
+
804
+
805
+ def set_installed_skill_enabled(installed_skill_id: str, enabled: bool, settings: Settings | None = None) -> dict[str, Any]:
806
+ path = _installed_skill_path(installed_skill_id, settings)
807
+ manifest = read_json(path / "install_manifest.json")
808
+ manifest["enabled"] = bool(enabled)
809
+ manifest["updated_at"] = utc_now()
810
+ write_json(path / "install_manifest.json", manifest)
811
+ append_jsonl(installed_skills_index_path(settings), manifest)
812
+ return manifest
813
+
814
+
815
+ def uninstall_installed_skill(installed_skill_id: str, settings: Settings | None = None) -> Path:
816
+ root = installed_skills_root(settings)
817
+ path = _installed_skill_path(installed_skill_id, settings)
818
+ manifest = read_json(path / "install_manifest.json")
819
+ manifest["enabled"] = False
820
+ manifest["uninstalled_at"] = utc_now()
821
+ removed = root / "_removed" / installed_skill_id
822
+ removed.parent.mkdir(parents=True, exist_ok=True)
823
+ if removed.exists():
824
+ shutil.rmtree(removed)
825
+ write_json(path / "install_manifest.json", manifest)
826
+ shutil.move(str(path), str(removed))
827
+ append_jsonl(installed_skills_index_path(settings), manifest)
828
+ return removed
829
+
830
+
831
+ def _skill_keywords(skill: dict[str, Any]) -> set[str]:
832
+ text = " ".join(
833
+ str(skill.get(key) or "")
834
+ for key in ["installed_skill_id", "title", "source_item_id", "source_package_id"]
835
+ )
836
+ path = Path(str(skill.get("path") or ""))
837
+ for rel in ["retrieval_card.json", "canonical_skill.json", "skill.md"]:
838
+ file_path = path / rel
839
+ if file_path.exists():
840
+ text += " " + file_path.read_text(errors="ignore")[:3000]
841
+ return {token.lower() for token in re.findall(r"[A-Za-z][A-Za-z0-9_-]{2,}", text)}
842
+
843
+
844
+ def select_runtime_training_for_run(
845
+ instruction: str,
846
+ *,
847
+ settings: Settings | None = None,
848
+ force: bool = False,
849
+ limit: int = 2,
850
+ ) -> tuple[list[dict[str, Any]], str]:
851
+ enabled = list_installed_skills(settings, include_disabled=False)
852
+ if not enabled:
853
+ return [], ""
854
+ query_terms = {token.lower() for token in re.findall(r"[A-Za-z][A-Za-z0-9_-]{2,}", instruction)}
855
+ scored: list[tuple[int, dict[str, Any]]] = []
856
+ for skill in enabled:
857
+ overlap = len(query_terms & _skill_keywords(skill))
858
+ if force or overlap:
859
+ scored.append((overlap, skill))
860
+ scored.sort(key=lambda item: item[0], reverse=True)
861
+ selected = [skill for _, skill in scored[:limit]]
862
+ if not selected:
863
+ return [], ""
864
+ sections = ["\n\n---\nInstalled Runtime Training\nUse these enabled skills only when they are relevant. The task instruction remains authoritative.\n"]
865
+ for skill in selected:
866
+ path = Path(str(skill.get("path")))
867
+ skill_text = (path / "skill.md").read_text(errors="replace") if (path / "skill.md").exists() else ""
868
+ sections.append(
869
+ f"\n## {skill.get('title') or skill.get('installed_skill_id')}\n"
870
+ f"installed_skill_id: {skill.get('installed_skill_id')}\n"
871
+ f"selection_reason: matched runtime training against the task request.\n\n"
872
+ f"{skill_text[:5000]}\n"
873
+ )
874
+ sections.append("---\n")
875
+ return selected, "\n".join(sections)
876
+
877
+
878
+ def installed_skill_run_refs(skills: list[dict[str, Any]]) -> list[dict[str, Any]]:
879
+ refs: list[dict[str, Any]] = []
880
+ for skill in skills:
881
+ refs.append(
882
+ {
883
+ "installed_skill_id": skill.get("installed_skill_id"),
884
+ "title": skill.get("title"),
885
+ "source_item_id": skill.get("source_item_id"),
886
+ "source_package_id": skill.get("source_package_id"),
887
+ }
888
+ )
889
+ return refs
890
+
891
+
623
892
  def write_before_after_result(pack_path: Path, result: dict[str, Any]) -> Path:
624
893
  result = dict(result)
625
894
  result["created_at"] = utc_now()
@@ -34,6 +34,7 @@ class RoleContext:
34
34
  task_instruction: str | None = None
35
35
  target_attempt_id: str | None = None
36
36
  verifier_result_id: str | None = None
37
+ review_packet_ref: str | None = None
37
38
 
38
39
 
39
40
  def _as_list(v: Any) -> list[Any]:
@@ -188,7 +189,7 @@ def _rubric_item(raw_item: Any, idx: int, weight: float, required_artifacts: lis
188
189
  evidence=raw_item.get('observable_evidence') or raw_item.get('evidence') or raw_item.get('evidence_requirements') or raw_item.get('success_criteria') or required_artifacts or ['final answer and artifacts']
189
190
  artifacts=raw_item.get('required_artifacts') or raw_item.get('artifacts') or required_artifacts or ['final_answer']
190
191
  scoring_method=raw_item.get('scoring_method')
191
- if not isinstance(scoring_method, str) or scoring_method not in {'llm_rubric_judge','deterministic','schema_match','regex','unit_test','hybrid','human_future'}:
192
+ if not isinstance(scoring_method, str) or scoring_method not in {'llm_rubric_judge','deterministic','schema_match','regex','unit_test','hybrid','human_future','structural_guardrail'}:
192
193
  scoring_method='llm_rubric_judge'
193
194
  return {
194
195
  'rubric_item_id': str(raw_item.get('rubric_item_id') or raw_item.get('id') or f'ri_{idx}'),
@@ -253,7 +254,7 @@ def normalize_rubric_result(raw: dict[str, Any], context: RoleContext | None=Non
253
254
  'required_artifacts': required,
254
255
  'disqualifying_errors': [str(x) for x in _as_list(rub_raw.get('disqualifying_errors'))],
255
256
  'partial_credit_allowed': _bool(rub_raw.get('partial_credit_allowed'), True),
256
- 'grader_kind': rub_raw.get('grader_kind') if rub_raw.get('grader_kind') in {'llm_rubric_judge','deterministic','hybrid','human_future'} else 'llm_rubric_judge',
257
+ 'grader_kind': rub_raw.get('grader_kind') if rub_raw.get('grader_kind') in {'llm_rubric_judge','deterministic','hybrid','human_future','structural_guardrail','unavailable'} else 'llm_rubric_judge',
257
258
  'rubric_generation_source': rub_raw.get('rubric_generation_source') if rub_raw.get('rubric_generation_source') in {'agent_assisted','family_template','task_specific_agent_draft','expert_override','deterministic_seed'} else 'task_specific_agent_draft',
258
259
  'rubric_generation_agent_provider': rub_raw.get('rubric_generation_agent_provider') or context.provider,
259
260
  'rubric_generation_agent_model': rub_raw.get('rubric_generation_agent_model') or context.model,
@@ -282,12 +283,12 @@ def normalize_evaluator_result(raw: dict[str, Any], context: RoleContext | None=
282
283
  actionable=[str(x) for x in _as_list(actionable)]
283
284
  weak=[str(x) for x in _as_list(fb_raw.get('failed_or_weak_rubric_items') or fb_raw.get('failed_rubric_items'))]
284
285
  md=_metadata(fb_raw, EVALUATOR_ALLOWED)
285
- md.update({'attempt_kind': fb_raw.get('attempt_kind') or context.attempt_kind, 'grader_result_id': context.grader_result_id, 'verifier_result_id': context.verifier_result_id})
286
+ md.update({'attempt_kind': fb_raw.get('attempt_kind') or context.attempt_kind, 'grader_result_id': context.grader_result_id, 'verifier_result_id': context.verifier_result_id, 'review_source': 'review_packet' if context.review_packet_ref else md.get('review_source'), 'review_packet_ref': context.review_packet_ref})
286
287
  feedback={
287
288
  'feedback_id': str(fb_raw.get('feedback_id') or f'feedback_{task_id}_{context.attempt_kind}'),
288
289
  'task_id': task_id,
289
290
  'attempt_id': attempt_id,
290
- 'target_actor': fb_raw.get('target_actor') if fb_raw.get('target_actor') in {'worker','reviser','apprentice'} else 'worker',
291
+ 'target_actor': fb_raw.get('target_actor') if fb_raw.get('target_actor') in {'worker','reviser','apprentice'} else 'apprentice',
291
292
  'feedback_type': fb_raw.get('feedback_type') if fb_raw.get('feedback_type') in {'criteria_failure','artifact_missing','format_error','logic_error','tool_error','quality_gap','strategy_gap','safety_or_privacy','other'} else 'other',
292
293
  'failed_rubric_items': weak,
293
294
  'evidence_refs': [str(x) for x in _as_list(fb_raw.get('evidence_refs'))],
@@ -322,7 +323,7 @@ def normalize_evaluator_result(raw: dict[str, Any], context: RoleContext | None=
322
323
  'uses_evaluator_feedback': _bool(rp_raw.get('uses_evaluator_feedback'), True),
323
324
  'metadata_json': rp_md,
324
325
  }
325
- revision['metadata_json'].update({'source_attempt_kind': context.attempt_kind, 'revision_goal': summary, 'priority_items': weak, 'expected_improvements': []})
326
+ revision['metadata_json'].update({'source_attempt_kind': context.attempt_kind, 'revision_goal': summary, 'priority_items': weak, 'expected_improvements': [], 'review_source': 'review_packet' if context.review_packet_ref else revision['metadata_json'].get('review_source'), 'review_packet_ref': context.review_packet_ref})
326
327
  return {'evaluator_feedback': feedback, 'revision_plan': revision}
327
328
 
328
329
  def normalize_grader_result(raw: dict[str, Any], context: RoleContext | None=None) -> dict[str, Any]:
@@ -339,7 +340,7 @@ def normalize_grader_result(raw: dict[str, Any], context: RoleContext | None=Non
339
340
  semantic_score=0.0
340
341
  final_score=0.0
341
342
  limitations.append('Semantic grading impossible because artifact content was unavailable; score reflects missing outputs/log evidence.')
342
- md['deterministic_precheck']={'artifact_contract_score': artifact_contract_score}
343
+ md['structural_guardrail']={'artifact_contract_score': artifact_contract_score}
343
344
  basis=context.model_grading_basis or ('artifact_content' if artifact_contract_score else ('logs_only' if refs else 'missing_outputs'))
344
345
  md['evidence_materialization_status']='materialized' if artifact_contract_score else 'missing_or_unverified'
345
346
  md['referenced_but_missing_artifacts']=[str(x) for x in _as_list(raw.get('referenced_but_missing_artifacts'))]
@@ -386,7 +387,7 @@ def normalize_grader_result(raw: dict[str, Any], context: RoleContext | None=Non
386
387
  'final_score': final_score,
387
388
  'model': raw.get('model') or context.model,
388
389
  'provider': raw.get('provider') or context.provider,
389
- 'deterministic_precheck_ref': raw.get('deterministic_precheck_ref'),
390
+ 'deterministic_precheck_ref': None,
390
391
  'llm_prompt_ref_internal': raw.get('llm_prompt_ref_internal'),
391
392
  'llm_response_ref_internal': raw.get('llm_response_ref_internal'),
392
393
  'public_prompt_hash': raw.get('public_prompt_hash'),