agent-apprenticeship 0.1.6 → 0.1.8
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.
- package/bin/agent-apprenticeship.js +85 -37
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/agent_apprenticeship_trace/__init__.py +1 -1
- package/src/agent_apprenticeship_trace/actual_outputs_normalizer.py +104 -28
- package/src/agent_apprenticeship_trace/apprentice_adapters.py +3 -1
- package/src/agent_apprenticeship_trace/artifact_resolver.py +6 -0
- package/src/agent_apprenticeship_trace/bundle_exporter.py +132 -2
- package/src/agent_apprenticeship_trace/cli.py +1171 -262
- package/src/agent_apprenticeship_trace/codex_runner.py +89 -6
- package/src/agent_apprenticeship_trace/config.py +251 -32
- package/src/agent_apprenticeship_trace/ecosystem_r2.py +515 -0
- package/src/agent_apprenticeship_trace/env.py +5 -5
- package/src/agent_apprenticeship_trace/evaluator.py +127 -11
- package/src/agent_apprenticeship_trace/grader.py +12 -11
- package/src/agent_apprenticeship_trace/learning.py +270 -1
- package/src/agent_apprenticeship_trace/llm_output_normalizer.py +8 -7
- package/src/agent_apprenticeship_trace/loop.py +103 -4
- package/src/agent_apprenticeship_trace/loop_review.py +686 -0
- package/src/agent_apprenticeship_trace/mentor_checkpoints.py +49 -21
- package/src/agent_apprenticeship_trace/openai_structured.py +84 -14
- package/src/agent_apprenticeship_trace/package_exporter.py +1 -1
- package/src/agent_apprenticeship_trace/progress.py +2 -1
- package/src/agent_apprenticeship_trace/public_run.py +369 -57
- package/src/agent_apprenticeship_trace/public_sanitizer.py +37 -1
- package/src/agent_apprenticeship_trace/rubric_generation.py +11 -10
- package/src/agent_apprenticeship_trace/schemas.py +378 -3
- package/src/agent_apprenticeship_trace/task_intake.py +43 -9
- package/src/agent_apprenticeship_trace/tdo.py +2304 -0
- package/src/agent_apprenticeship_trace/trace_normalizer.py +351 -17
- package/src/agent_apprenticeship_trace/trace_prompt.py +38 -11
- package/src/agent_apprenticeship_trace/training_signals.py +1 -1
- package/src/agent_apprenticeship_trace/verifier.py +12 -6
|
@@ -0,0 +1,686 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import Counter
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .io import read_json, write_json
|
|
9
|
+
from .public_sanitizer import sanitize_public_obj, sanitize_public_text
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _now() -> str:
|
|
13
|
+
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _as_dict(value: Any) -> dict[str, Any]:
|
|
17
|
+
if value is None:
|
|
18
|
+
return {}
|
|
19
|
+
if isinstance(value, dict):
|
|
20
|
+
return dict(value)
|
|
21
|
+
if hasattr(value, "model_dump"):
|
|
22
|
+
return value.model_dump(mode="json")
|
|
23
|
+
return {}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _read(path: Path) -> dict[str, Any]:
|
|
27
|
+
try:
|
|
28
|
+
obj = read_json(path)
|
|
29
|
+
except Exception:
|
|
30
|
+
return {}
|
|
31
|
+
return obj if isinstance(obj, dict) else {}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _rel(pkg: Path, path: Path) -> str:
|
|
35
|
+
try:
|
|
36
|
+
return path.relative_to(pkg).as_posix()
|
|
37
|
+
except ValueError:
|
|
38
|
+
return path.as_posix()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _existing_ref(pkg: Path, ref: str | None) -> str | None:
|
|
42
|
+
if not ref:
|
|
43
|
+
return None
|
|
44
|
+
text = str(ref).replace("\\", "/").strip()
|
|
45
|
+
if not text or text.startswith("/") or ".." in Path(text).parts:
|
|
46
|
+
return None
|
|
47
|
+
return text
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _list_refs(pkg: Path, values: Any) -> list[str]:
|
|
51
|
+
raw = values if isinstance(values, list) else [values]
|
|
52
|
+
refs: list[str] = []
|
|
53
|
+
for value in raw:
|
|
54
|
+
ref = _existing_ref(pkg, str(value) if value is not None else None)
|
|
55
|
+
if ref and ref not in refs:
|
|
56
|
+
refs.append(ref)
|
|
57
|
+
return refs
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _attempt_kind_from_actual(actual: dict[str, Any]) -> str:
|
|
61
|
+
return str(actual.get("attempt_kind") or "baseline")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _loop_id(pkg: Path) -> str:
|
|
65
|
+
return f"loop_{pkg.name}"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _iteration_dir(pkg: Path, iteration: int) -> Path:
|
|
69
|
+
return pkg / "loops" / "iterations" / f"{iteration:03d}"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _trace_refs(pkg: Path, attempt_kind: str) -> dict[str, str | None]:
|
|
73
|
+
base = pkg / "attempts" / attempt_kind
|
|
74
|
+
refs = {
|
|
75
|
+
"canonical": f"attempts/{attempt_kind}/agent_trace.json" if (base / "agent_trace.json").exists() else None,
|
|
76
|
+
"raw": f"attempts/{attempt_kind}/agent_trace.raw.json" if (base / "agent_trace.raw.json").exists() else None,
|
|
77
|
+
"normalized": f"attempts/{attempt_kind}/agent_trace.normalized.json" if (base / "agent_trace.normalized.json").exists() else None,
|
|
78
|
+
"normalization_report": f"attempts/{attempt_kind}/trace_normalization_report.json" if (base / "trace_normalization_report.json").exists() else None,
|
|
79
|
+
}
|
|
80
|
+
return refs
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _key_trace_steps(trace: dict[str, Any]) -> list[dict[str, Any]]:
|
|
84
|
+
out: list[dict[str, Any]] = []
|
|
85
|
+
for step in trace.get("steps") or []:
|
|
86
|
+
if not isinstance(step, dict):
|
|
87
|
+
continue
|
|
88
|
+
if step.get("action") == "user_message":
|
|
89
|
+
continue
|
|
90
|
+
out.append(
|
|
91
|
+
sanitize_public_obj(
|
|
92
|
+
{
|
|
93
|
+
"step": step.get("step"),
|
|
94
|
+
"actor": step.get("actor"),
|
|
95
|
+
"actor_kind": step.get("actor_kind"),
|
|
96
|
+
"operation": step.get("operation"),
|
|
97
|
+
"interaction_surface": step.get("interaction_surface"),
|
|
98
|
+
"environment_domain": step.get("environment_domain"),
|
|
99
|
+
"input_source": step.get("input_source"),
|
|
100
|
+
"output": sanitize_public_text(str(step.get("output") or ""))[:500],
|
|
101
|
+
"state_change": sanitize_public_text(str(step.get("state_change") or ""))[:500],
|
|
102
|
+
"step_outcome": step.get("step_outcome"),
|
|
103
|
+
"success": step.get("success"),
|
|
104
|
+
"artifact_refs": step.get("artifact_refs") or [],
|
|
105
|
+
"missing_surface_fields": step.get("missing_surface_fields") or [],
|
|
106
|
+
"trace_quality_warnings": step.get("trace_quality_warnings") or [],
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
if len(out) >= 12:
|
|
111
|
+
break
|
|
112
|
+
return out
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _surface_summary(trace: dict[str, Any]) -> dict[str, Any]:
|
|
116
|
+
surfaces: Counter[str] = Counter()
|
|
117
|
+
domains: Counter[str] = Counter()
|
|
118
|
+
warnings: list[dict[str, Any]] = []
|
|
119
|
+
for step in trace.get("steps") or []:
|
|
120
|
+
if not isinstance(step, dict):
|
|
121
|
+
continue
|
|
122
|
+
surface = step.get("interaction_surface") or "unknown"
|
|
123
|
+
domain = step.get("environment_domain") or "unknown"
|
|
124
|
+
surfaces[str(surface)] += 1
|
|
125
|
+
domains[str(domain)] += 1
|
|
126
|
+
missing = step.get("missing_surface_fields") or []
|
|
127
|
+
if missing:
|
|
128
|
+
warnings.append(
|
|
129
|
+
{
|
|
130
|
+
"step": step.get("step"),
|
|
131
|
+
"interaction_surface": surface,
|
|
132
|
+
"surface_capture_status": step.get("surface_capture_status"),
|
|
133
|
+
"missing_surface_fields": missing[:12],
|
|
134
|
+
}
|
|
135
|
+
)
|
|
136
|
+
return {
|
|
137
|
+
"interaction_surfaces": dict(surfaces),
|
|
138
|
+
"environment_domains": dict(domains),
|
|
139
|
+
"surface_capture_warnings": warnings[:20],
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _artifact_index(pkg: Path, actual: dict[str, Any]) -> list[dict[str, Any]]:
|
|
144
|
+
rows: list[dict[str, Any]] = []
|
|
145
|
+
index = pkg / "artifacts_index.json"
|
|
146
|
+
if index.exists():
|
|
147
|
+
raw = read_json(index)
|
|
148
|
+
if isinstance(raw, list):
|
|
149
|
+
for row in raw[:100]:
|
|
150
|
+
if isinstance(row, dict):
|
|
151
|
+
rows.append(sanitize_public_obj(row))
|
|
152
|
+
if rows:
|
|
153
|
+
return rows
|
|
154
|
+
refs = []
|
|
155
|
+
for key in ["primary_output_ref", "deliverable_refs", "artifact_refs", "files_created", "input_artifact_refs"]:
|
|
156
|
+
refs.extend(_list_refs(pkg, actual.get(key)))
|
|
157
|
+
for ref in dict.fromkeys(refs):
|
|
158
|
+
rows.append({"artifact_ref": ref, "exists": (pkg / ref).exists()})
|
|
159
|
+
return rows
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _rubric_status(pkg: Path) -> dict[str, Any]:
|
|
163
|
+
rubric = _read(pkg / "rubric" / "rubric.json")
|
|
164
|
+
quality = _read(pkg / "rubric" / "rubric_quality_report.json")
|
|
165
|
+
return sanitize_public_obj(
|
|
166
|
+
{
|
|
167
|
+
"rubric_refs": [ref for ref in ["rubric/rubric.json", "rubric/worker_visible_rubric.md"] if (pkg / ref).exists()],
|
|
168
|
+
"rubric_item_count": len(rubric.get("rubric_items") or []),
|
|
169
|
+
"mentor_audited": bool((rubric.get("metadata_json") or {}).get("mentor_audited")),
|
|
170
|
+
"audit_status": (rubric.get("metadata_json") or {}).get("audit_status"),
|
|
171
|
+
"quality_report": quality,
|
|
172
|
+
}
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _status_from_grader_verifier(grader: dict[str, Any], verifier: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any], list[str]]:
|
|
177
|
+
failed = list(grader.get("failed_criteria") or [])
|
|
178
|
+
verifier_failed = list(verifier.get("failed_checks") or verifier.get("failed_criteria") or [])
|
|
179
|
+
known_failures = [str(v) for v in failed + verifier_failed if v]
|
|
180
|
+
grade_score = grader.get("final_score", grader.get("score"))
|
|
181
|
+
rubric_status = {
|
|
182
|
+
"grader_result_ref": None,
|
|
183
|
+
"score": grade_score,
|
|
184
|
+
"passed": grader.get("passed"),
|
|
185
|
+
"failed_criteria": failed,
|
|
186
|
+
"score_reliability": grader.get("score_reliability"),
|
|
187
|
+
}
|
|
188
|
+
verifier_status = {
|
|
189
|
+
"verifier_result_ref": None,
|
|
190
|
+
"passed": verifier.get("passed"),
|
|
191
|
+
"verdict": verifier.get("verdict"),
|
|
192
|
+
"failed_checks": verifier_failed,
|
|
193
|
+
"evidence_refs": verifier.get("evidence_refs") or [],
|
|
194
|
+
"notes": verifier.get("notes") or verifier.get("verifier_notes"),
|
|
195
|
+
}
|
|
196
|
+
return rubric_status, verifier_status, known_failures
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _evaluator_status(feedback: dict[str, Any]) -> dict[str, Any]:
|
|
200
|
+
return sanitize_public_obj(
|
|
201
|
+
{
|
|
202
|
+
"feedback_ref": "feedback/baseline_evaluator_feedback.json" if feedback else None,
|
|
203
|
+
"feedback_type": feedback.get("feedback_type"),
|
|
204
|
+
"revision_priority": feedback.get("revision_priority"),
|
|
205
|
+
"failed_rubric_items": feedback.get("failed_rubric_items") or feedback.get("failed_or_weak_rubric_items") or [],
|
|
206
|
+
"summary": feedback.get("feedback_summary"),
|
|
207
|
+
"actionable_feedback": feedback.get("actionable_feedback") or [],
|
|
208
|
+
"provider": feedback.get("provider"),
|
|
209
|
+
"model": feedback.get("model"),
|
|
210
|
+
}
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _output_refs(pkg: Path, actual: dict[str, Any], attempt_kind: str) -> list[str]:
|
|
215
|
+
refs = [f"attempts/{attempt_kind}/actual_outputs.json"]
|
|
216
|
+
for key in ["primary_output_ref", "deliverable_refs", "artifact_refs", "files_created", "stdout_ref", "stderr_ref", "raw_log_refs"]:
|
|
217
|
+
refs.extend(_list_refs(pkg, actual.get(key)))
|
|
218
|
+
return list(dict.fromkeys(refs))
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _missing_outputs(actual: dict[str, Any]) -> list[str]:
|
|
222
|
+
missing: list[str] = []
|
|
223
|
+
for key in ["primary_output_ref", "deliverable_refs", "artifact_refs", "files_created"]:
|
|
224
|
+
value = actual.get(key)
|
|
225
|
+
if value in (None, "", []):
|
|
226
|
+
missing.append(key)
|
|
227
|
+
if actual.get("status") not in {"success", "completed"}:
|
|
228
|
+
missing.append("successful_status")
|
|
229
|
+
return missing
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _diff_from_previous(pkg: Path, iteration: int, actual: dict[str, Any]) -> dict[str, Any]:
|
|
233
|
+
if iteration <= 1:
|
|
234
|
+
return {"available": False, "reason": "First loop iteration has no previous output snapshot."}
|
|
235
|
+
previous_path = _iteration_dir(pkg, iteration - 1) / "output_snapshot.json"
|
|
236
|
+
previous = _read(previous_path)
|
|
237
|
+
previous_refs = set(_output_refs(pkg, previous, _attempt_kind_from_actual(previous)))
|
|
238
|
+
current_refs = set(_output_refs(pkg, actual, _attempt_kind_from_actual(actual)))
|
|
239
|
+
return sanitize_public_obj(
|
|
240
|
+
{
|
|
241
|
+
"available": True,
|
|
242
|
+
"previous_iteration_ref": f"loops/iterations/{iteration - 1:03d}/output_snapshot.json",
|
|
243
|
+
"added_refs": sorted(current_refs - previous_refs),
|
|
244
|
+
"removed_refs": sorted(previous_refs - current_refs),
|
|
245
|
+
"changed_summary": {
|
|
246
|
+
"previous_output_summary": previous.get("output_summary"),
|
|
247
|
+
"current_output_summary": actual.get("output_summary"),
|
|
248
|
+
},
|
|
249
|
+
}
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def reviewer_feedback_from_evaluator(
|
|
254
|
+
feedback: Any,
|
|
255
|
+
grader: Any,
|
|
256
|
+
verifier: Any,
|
|
257
|
+
*,
|
|
258
|
+
reviewer_type: str = "mentor_model_provider",
|
|
259
|
+
review_source: str = "review_packet",
|
|
260
|
+
fallback_reviewer_type: str | None = None,
|
|
261
|
+
) -> dict[str, Any]:
|
|
262
|
+
fb = _as_dict(feedback)
|
|
263
|
+
gr = _as_dict(grader)
|
|
264
|
+
ve = _as_dict(verifier)
|
|
265
|
+
score = gr.get("final_score", gr.get("score"))
|
|
266
|
+
try:
|
|
267
|
+
numeric_score = float(score)
|
|
268
|
+
except (TypeError, ValueError):
|
|
269
|
+
numeric_score = 0.0
|
|
270
|
+
failed = list(fb.get("failed_rubric_items") or fb.get("failed_or_weak_rubric_items") or gr.get("failed_criteria") or [])
|
|
271
|
+
blocking = [str(item) for item in failed if item]
|
|
272
|
+
verdict = "pass"
|
|
273
|
+
if blocking or ve.get("passed") is False or numeric_score < 0.75:
|
|
274
|
+
verdict = "revise"
|
|
275
|
+
if fb.get("metadata_json", {}).get("mentor_evaluator_status") == "unavailable":
|
|
276
|
+
reviewer_type = fallback_reviewer_type or "mentor_model_unavailable"
|
|
277
|
+
verdict = "needs_expert" if blocking else "stop_with_recorded_outcome"
|
|
278
|
+
return sanitize_public_obj(
|
|
279
|
+
{
|
|
280
|
+
"reviewer_type": reviewer_type,
|
|
281
|
+
"review_source": review_source,
|
|
282
|
+
"verdict": verdict,
|
|
283
|
+
"score": numeric_score,
|
|
284
|
+
"blocking_issues": blocking,
|
|
285
|
+
"non_blocking_issues": [str(x) for x in fb.get("artifact_specific_comments") or []],
|
|
286
|
+
"evidence_refs": list(dict.fromkeys((fb.get("evidence_refs") or []) + (fb.get("artifact_refs") or []) + (ve.get("evidence_refs") or []))),
|
|
287
|
+
"feedback": fb.get("feedback_summary") or "No reviewer feedback summary was available.",
|
|
288
|
+
"revision_instructions": fb.get("actionable_feedback") or [],
|
|
289
|
+
"stop_condition": "pass" if verdict == "pass" else "revise_or_request_expert_feedback",
|
|
290
|
+
"training_value_notes": "Loop reviewer feedback can supervise critique, revision, verifier, and reward-model rows.",
|
|
291
|
+
"created_at": _now(),
|
|
292
|
+
}
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def human_feedback_from_checkpoint(checkpoint: dict[str, Any], *, loop_id: str, iteration: int) -> dict[str, Any]:
|
|
297
|
+
input_source = checkpoint.get("checkpoint_input_source")
|
|
298
|
+
if input_source == "auto_approve":
|
|
299
|
+
reviewer_type = "simulated_for_test"
|
|
300
|
+
source = "auto_expert_checkpoint"
|
|
301
|
+
elif checkpoint.get("source") == "hybrid_human_approval":
|
|
302
|
+
reviewer_type = "org_custom_reviewer"
|
|
303
|
+
source = "hybrid_human_approval"
|
|
304
|
+
else:
|
|
305
|
+
reviewer_type = "human_expert"
|
|
306
|
+
source = "interactive" if input_source == "interactive" else "recorded"
|
|
307
|
+
return sanitize_public_obj(
|
|
308
|
+
{
|
|
309
|
+
"feedback_id": f"feedback_{loop_id}_{iteration:03d}",
|
|
310
|
+
"loop_id": loop_id,
|
|
311
|
+
"loop_iteration": iteration,
|
|
312
|
+
"reviewer_type": reviewer_type,
|
|
313
|
+
"expert_feedback_source": source,
|
|
314
|
+
"feedback_text": checkpoint.get("feedback") or checkpoint.get("notes") or "Expert checkpoint recorded.",
|
|
315
|
+
"requested_changes": [checkpoint.get("notes")] if checkpoint.get("revision_should_run") else [],
|
|
316
|
+
"accepted_outputs": [] if checkpoint.get("revision_should_run") else ["current_attempt_outputs"],
|
|
317
|
+
"rejected_outputs": ["current_attempt_outputs"] if checkpoint.get("revision_should_run") else [],
|
|
318
|
+
"evidence_refs": checkpoint.get("evidence_refs") or [],
|
|
319
|
+
"created_at": checkpoint.get("created_at") or _now(),
|
|
320
|
+
}
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _default_loop_decision(
|
|
325
|
+
*,
|
|
326
|
+
loop_id: str,
|
|
327
|
+
iteration: int,
|
|
328
|
+
reviewer_feedback: dict[str, Any] | None,
|
|
329
|
+
stop_reason: str | None,
|
|
330
|
+
continue_loop: bool | None,
|
|
331
|
+
) -> dict[str, Any]:
|
|
332
|
+
verdict = (reviewer_feedback or {}).get("verdict")
|
|
333
|
+
if continue_loop is None:
|
|
334
|
+
continue_loop = verdict == "revise"
|
|
335
|
+
return sanitize_public_obj(
|
|
336
|
+
{
|
|
337
|
+
"loop_id": loop_id,
|
|
338
|
+
"loop_iteration": iteration,
|
|
339
|
+
"decision": "continue_loop" if continue_loop else "stop",
|
|
340
|
+
"verdict": verdict or ("revise" if continue_loop else "pass"),
|
|
341
|
+
"stop_condition": stop_reason or ("revision_requested" if continue_loop else "review_complete"),
|
|
342
|
+
"created_at": _now(),
|
|
343
|
+
}
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _normalized_revision_plan(
|
|
348
|
+
raw_plan: Any,
|
|
349
|
+
*,
|
|
350
|
+
loop_id: str,
|
|
351
|
+
iteration: int,
|
|
352
|
+
reviewer_feedback: dict[str, Any] | None,
|
|
353
|
+
loop_decision: dict[str, Any],
|
|
354
|
+
) -> dict[str, Any]:
|
|
355
|
+
plan = _as_dict(raw_plan)
|
|
356
|
+
decision = str(loop_decision.get("decision") or "")
|
|
357
|
+
should_continue = decision == "continue_loop"
|
|
358
|
+
feedback_ref = f"loops/iterations/{iteration:03d}/reviewer_feedback.json"
|
|
359
|
+
issues = (
|
|
360
|
+
plan.get("issues_to_fix")
|
|
361
|
+
or plan.get("revision_items")
|
|
362
|
+
or (reviewer_feedback or {}).get("blocking_issues")
|
|
363
|
+
or (reviewer_feedback or {}).get("revision_instructions")
|
|
364
|
+
or []
|
|
365
|
+
)
|
|
366
|
+
verification_steps = (
|
|
367
|
+
plan.get("verification_steps")
|
|
368
|
+
or plan.get("checks_to_rerun")
|
|
369
|
+
or ["Reinspect actual_outputs.json, artifacts, trace refs, rubric status, and verifier status."]
|
|
370
|
+
)
|
|
371
|
+
deferred_feedback_reason = plan.get("feedback_not_applied_reason")
|
|
372
|
+
if issues and not should_continue and not deferred_feedback_reason:
|
|
373
|
+
deferred_feedback_reason = (
|
|
374
|
+
loop_decision.get("stop_condition")
|
|
375
|
+
or "Loop stopped because the reviewer verdict did not require another revision iteration."
|
|
376
|
+
)
|
|
377
|
+
defaults = {
|
|
378
|
+
"loop_id": loop_id,
|
|
379
|
+
"from_iteration": iteration,
|
|
380
|
+
"to_iteration": iteration + 1 if should_continue else None,
|
|
381
|
+
"feedback_refs": [feedback_ref] if reviewer_feedback else [],
|
|
382
|
+
"issues_to_fix": issues,
|
|
383
|
+
"artifacts_to_update": plan.get("artifacts_to_update") or [],
|
|
384
|
+
"outputs_to_update": plan.get("outputs_to_update") or [],
|
|
385
|
+
"verification_steps": verification_steps,
|
|
386
|
+
"stop_conditions": plan.get("stop_conditions") or [loop_decision.get("stop_condition") or "review_complete"],
|
|
387
|
+
"revision_required": should_continue,
|
|
388
|
+
"feedback_applied": plan.get("feedback_applied") if plan.get("feedback_applied") is not None else (None if should_continue or not issues else False),
|
|
389
|
+
"feedback_not_applied_reason": deferred_feedback_reason,
|
|
390
|
+
"created_at": plan.get("created_at") or _now(),
|
|
391
|
+
}
|
|
392
|
+
merged = {**defaults, **{k: v for k, v in plan.items() if v not in (None, "", [])}}
|
|
393
|
+
return sanitize_public_obj(merged)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def write_loop_review_iteration(
|
|
397
|
+
package_root: Path,
|
|
398
|
+
*,
|
|
399
|
+
iteration: int,
|
|
400
|
+
mode: str | None,
|
|
401
|
+
actual_outputs: Any,
|
|
402
|
+
trace: Any,
|
|
403
|
+
grader_result: Any | None = None,
|
|
404
|
+
verifier_result: Any | None = None,
|
|
405
|
+
evaluator_feedback: Any | None = None,
|
|
406
|
+
revision_plan: Any | None = None,
|
|
407
|
+
reviewer_feedback: dict[str, Any] | None = None,
|
|
408
|
+
loop_decision: dict[str, Any] | None = None,
|
|
409
|
+
stop_reason: str | None = None,
|
|
410
|
+
continue_loop: bool | None = None,
|
|
411
|
+
) -> Path:
|
|
412
|
+
package_root = Path(package_root)
|
|
413
|
+
actual = _as_dict(actual_outputs)
|
|
414
|
+
trace_data = _as_dict(trace)
|
|
415
|
+
grader = _as_dict(grader_result)
|
|
416
|
+
verifier = _as_dict(verifier_result)
|
|
417
|
+
feedback = _as_dict(evaluator_feedback)
|
|
418
|
+
attempt_kind = _attempt_kind_from_actual(actual)
|
|
419
|
+
loop_id = _loop_id(package_root)
|
|
420
|
+
out_dir = _iteration_dir(package_root, iteration)
|
|
421
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
422
|
+
|
|
423
|
+
rubric_result_status, verifier_status, known_failures = _status_from_grader_verifier(grader, verifier)
|
|
424
|
+
if attempt_kind:
|
|
425
|
+
rubric_result_status["grader_result_ref"] = f"grading/{attempt_kind}_grader_result.json" if (package_root / "grading" / f"{attempt_kind}_grader_result.json").exists() else None
|
|
426
|
+
verifier_status["verifier_result_ref"] = f"grading/{attempt_kind}_verifier_result.json" if (package_root / "grading" / f"{attempt_kind}_verifier_result.json").exists() else None
|
|
427
|
+
output_refs = _output_refs(package_root, actual, attempt_kind)
|
|
428
|
+
diff = _diff_from_previous(package_root, iteration, actual)
|
|
429
|
+
if reviewer_feedback is None and feedback:
|
|
430
|
+
reviewer_feedback = reviewer_feedback_from_evaluator(feedback, grader, verifier)
|
|
431
|
+
if loop_decision is None:
|
|
432
|
+
loop_decision = _default_loop_decision(
|
|
433
|
+
loop_id=loop_id,
|
|
434
|
+
iteration=iteration,
|
|
435
|
+
reviewer_feedback=reviewer_feedback,
|
|
436
|
+
stop_reason=stop_reason,
|
|
437
|
+
continue_loop=continue_loop,
|
|
438
|
+
)
|
|
439
|
+
plan = _normalized_revision_plan(
|
|
440
|
+
revision_plan,
|
|
441
|
+
loop_id=loop_id,
|
|
442
|
+
iteration=iteration,
|
|
443
|
+
reviewer_feedback=reviewer_feedback,
|
|
444
|
+
loop_decision=loop_decision,
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
packet = sanitize_public_obj(
|
|
448
|
+
{
|
|
449
|
+
"loop_id": loop_id,
|
|
450
|
+
"loop_iteration": iteration,
|
|
451
|
+
"mode": mode,
|
|
452
|
+
"task_id": package_root.name,
|
|
453
|
+
"package_id": package_root.name,
|
|
454
|
+
"run_id": package_root.parent.parent.name if package_root.parent.name == "packages" else None,
|
|
455
|
+
"current_goal": (_read(package_root / "task" / "task_intake_spec.json").get("normalized_instruction") or _read(package_root / "task" / "raw_task_record.json").get("raw_description")),
|
|
456
|
+
"current_attempt_summary": actual.get("output_summary"),
|
|
457
|
+
"current_output_refs": output_refs,
|
|
458
|
+
"actual_outputs_ref": f"attempts/{attempt_kind}/actual_outputs.json",
|
|
459
|
+
"artifact_refs": list(dict.fromkeys((actual.get("artifact_refs") or []) + (actual.get("deliverable_refs") or []) + (actual.get("files_created") or []))),
|
|
460
|
+
"artifact_previews": _artifact_index(package_root, actual)[:25],
|
|
461
|
+
"trace_refs": _trace_refs(package_root, attempt_kind),
|
|
462
|
+
"key_trace_steps": _key_trace_steps(trace_data),
|
|
463
|
+
"rubric_refs": ["rubric/rubric.json", "rubric/worker_visible_rubric.md"],
|
|
464
|
+
"rubric_status": {**_rubric_status(package_root), "attempt_result": rubric_result_status},
|
|
465
|
+
"verifier_status": verifier_status,
|
|
466
|
+
"evaluator_status": _evaluator_status(feedback),
|
|
467
|
+
"known_failures": known_failures,
|
|
468
|
+
"missing_outputs": _missing_outputs(actual),
|
|
469
|
+
"open_questions": [] if not known_failures else ["Can the listed failures be repaired in the next iteration?"],
|
|
470
|
+
"surface_summary": _surface_summary(trace_data),
|
|
471
|
+
"environment_summary": _surface_summary(trace_data).get("environment_domains"),
|
|
472
|
+
"previous_iteration_refs": [f"loops/iterations/{iteration - 1:03d}/review_packet.json"] if iteration > 1 else [],
|
|
473
|
+
"diff_from_previous_iteration": diff,
|
|
474
|
+
"reviewer_instructions": [
|
|
475
|
+
"Inspect actual_outputs.json, artifacts, trace evidence, rubric status, verifier status, and this packet before judging.",
|
|
476
|
+
"Do not rely on Apprentice Agent self-evaluation as outcome truth.",
|
|
477
|
+
"Choose accept, revise, ask for expert feedback, stop with a recorded outcome, or continue loop based on grounded evidence.",
|
|
478
|
+
],
|
|
479
|
+
"recommended_review_focus": known_failures or ["Confirm deliverables, evidence refs, and verifier/rubric alignment."],
|
|
480
|
+
"stop_continue_options": ["accept", "revise", "ask_for_expert_feedback", "stop_with_recorded_outcome", "continue_loop"],
|
|
481
|
+
"created_at": _now(),
|
|
482
|
+
}
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
write_json(out_dir / "output_snapshot.json", actual)
|
|
486
|
+
write_json(out_dir / "artifact_index.json", _artifact_index(package_root, actual))
|
|
487
|
+
write_json(out_dir / "trace_refs.json", _trace_refs(package_root, attempt_kind))
|
|
488
|
+
write_json(out_dir / "rubric_status.json", packet["rubric_status"])
|
|
489
|
+
write_json(out_dir / "verifier_status.json", verifier_status)
|
|
490
|
+
write_json(out_dir / "evaluator_feedback.json", feedback)
|
|
491
|
+
write_json(out_dir / "review_packet.json", packet)
|
|
492
|
+
write_json(out_dir / "diff_report.json", diff)
|
|
493
|
+
write_json(out_dir / "reviewer_feedback.json", reviewer_feedback or {})
|
|
494
|
+
write_json(out_dir / "revision_plan.json", plan)
|
|
495
|
+
write_json(out_dir / "loop_decision.json", loop_decision)
|
|
496
|
+
_write_review_packet_md(out_dir / "review_packet.md", packet)
|
|
497
|
+
_write_loop_manifest(package_root)
|
|
498
|
+
return out_dir / "review_packet.json"
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def _write_review_packet_md(path: Path, packet: dict[str, Any]) -> None:
|
|
502
|
+
lines = [
|
|
503
|
+
f"# Loop Iteration {packet.get('loop_iteration')} Review Packet",
|
|
504
|
+
"",
|
|
505
|
+
f"Task: {packet.get('task_id')}",
|
|
506
|
+
f"Mode: {packet.get('mode') or 'unknown'}",
|
|
507
|
+
"",
|
|
508
|
+
"## Task Goal",
|
|
509
|
+
str(packet.get("current_goal") or "No goal recorded."),
|
|
510
|
+
"",
|
|
511
|
+
"## What Changed This Loop",
|
|
512
|
+
str((packet.get("diff_from_previous_iteration") or {}).get("changed_summary") or "Initial attempt."),
|
|
513
|
+
"",
|
|
514
|
+
"## Outputs Produced",
|
|
515
|
+
]
|
|
516
|
+
for ref in packet.get("current_output_refs") or []:
|
|
517
|
+
lines.append(f"- {ref}")
|
|
518
|
+
lines.extend(["", "## Evidence To Inspect"])
|
|
519
|
+
for ref in [packet.get("actual_outputs_ref"), *((packet.get("trace_refs") or {}).values())]:
|
|
520
|
+
if ref:
|
|
521
|
+
lines.append(f"- {ref}")
|
|
522
|
+
lines.extend(["", "## Rubric And Verifier Status"])
|
|
523
|
+
lines.append(f"- Rubric score/status: {(packet.get('rubric_status') or {}).get('attempt_result')}")
|
|
524
|
+
lines.append(f"- Verifier status: {packet.get('verifier_status')}")
|
|
525
|
+
lines.extend(["", "## Problems Or Uncertainty"])
|
|
526
|
+
failures = packet.get("known_failures") or packet.get("missing_outputs") or []
|
|
527
|
+
if failures:
|
|
528
|
+
lines.extend(f"- {item}" for item in failures)
|
|
529
|
+
else:
|
|
530
|
+
lines.append("- No blocking issue was recorded in this packet.")
|
|
531
|
+
lines.extend(["", "## Suggested Next Decision"])
|
|
532
|
+
lines.append("- accept")
|
|
533
|
+
lines.append("- revise")
|
|
534
|
+
lines.append("- ask for expert feedback")
|
|
535
|
+
lines.append("- stop with recorded outcome")
|
|
536
|
+
lines.append("- continue loop")
|
|
537
|
+
path.write_text("\n".join(lines).rstrip() + "\n")
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def _write_loop_manifest(pkg: Path) -> None:
|
|
541
|
+
iterations: list[dict[str, Any]] = []
|
|
542
|
+
reviewer_types: list[str] = []
|
|
543
|
+
final_verdict = None
|
|
544
|
+
for packet_path in sorted((pkg / "loops" / "iterations").glob("*/review_packet.json")):
|
|
545
|
+
iteration_dir = packet_path.parent
|
|
546
|
+
packet = _read(packet_path)
|
|
547
|
+
feedback = _read(iteration_dir / "reviewer_feedback.json")
|
|
548
|
+
decision = _read(iteration_dir / "loop_decision.json")
|
|
549
|
+
reviewer_type = feedback.get("reviewer_type")
|
|
550
|
+
if reviewer_type and reviewer_type not in reviewer_types:
|
|
551
|
+
reviewer_types.append(str(reviewer_type))
|
|
552
|
+
final_verdict = decision.get("verdict") or feedback.get("verdict") or final_verdict
|
|
553
|
+
iterations.append(
|
|
554
|
+
{
|
|
555
|
+
"loop_iteration": packet.get("loop_iteration"),
|
|
556
|
+
"review_packet_ref": _rel(pkg, packet_path),
|
|
557
|
+
"review_packet_md_ref": _rel(pkg, iteration_dir / "review_packet.md"),
|
|
558
|
+
"reviewer_feedback_ref": _rel(pkg, iteration_dir / "reviewer_feedback.json"),
|
|
559
|
+
"revision_plan_ref": _rel(pkg, iteration_dir / "revision_plan.json"),
|
|
560
|
+
"loop_decision_ref": _rel(pkg, iteration_dir / "loop_decision.json"),
|
|
561
|
+
"diff_report_ref": _rel(pkg, iteration_dir / "diff_report.json"),
|
|
562
|
+
"reviewer_type": reviewer_type,
|
|
563
|
+
"verdict": decision.get("verdict") or feedback.get("verdict"),
|
|
564
|
+
}
|
|
565
|
+
)
|
|
566
|
+
manifest = sanitize_public_obj(
|
|
567
|
+
{
|
|
568
|
+
"loop_id": _loop_id(pkg),
|
|
569
|
+
"package_id": pkg.name,
|
|
570
|
+
"loop_iteration_count": len(iterations),
|
|
571
|
+
"has_loop_review_packets": bool(iterations),
|
|
572
|
+
"reviewer_types": reviewer_types,
|
|
573
|
+
"final_loop_verdict": final_verdict,
|
|
574
|
+
"iterations": iterations,
|
|
575
|
+
"created_at": _now(),
|
|
576
|
+
"updated_at": _now(),
|
|
577
|
+
}
|
|
578
|
+
)
|
|
579
|
+
(pkg / "loops").mkdir(parents=True, exist_ok=True)
|
|
580
|
+
write_json(pkg / "loops" / "loop_manifest.json", manifest)
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def record_human_loop_feedback(package_root: Path, *, iteration: int, checkpoint: dict[str, Any], revision_requested: bool) -> None:
|
|
584
|
+
loop_id = _loop_id(package_root)
|
|
585
|
+
feedback = human_feedback_from_checkpoint(checkpoint, loop_id=loop_id, iteration=iteration)
|
|
586
|
+
decision = _default_loop_decision(
|
|
587
|
+
loop_id=loop_id,
|
|
588
|
+
iteration=iteration,
|
|
589
|
+
reviewer_feedback={"verdict": "revise" if revision_requested else "pass"},
|
|
590
|
+
stop_reason="expert_requested_revision" if revision_requested else "expert_finished_loop",
|
|
591
|
+
continue_loop=revision_requested,
|
|
592
|
+
)
|
|
593
|
+
out_dir = _iteration_dir(package_root, iteration)
|
|
594
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
595
|
+
write_json(out_dir / "reviewer_feedback.json", feedback)
|
|
596
|
+
write_json(out_dir / "loop_decision.json", decision)
|
|
597
|
+
existing_plan = _read(out_dir / "revision_plan.json")
|
|
598
|
+
requested_changes = feedback.get("requested_changes") or []
|
|
599
|
+
existing_plan.update(
|
|
600
|
+
{
|
|
601
|
+
"loop_id": loop_id,
|
|
602
|
+
"from_iteration": iteration,
|
|
603
|
+
"to_iteration": iteration + 1 if revision_requested else None,
|
|
604
|
+
"feedback_refs": [f"loops/iterations/{iteration:03d}/reviewer_feedback.json"],
|
|
605
|
+
"issues_to_fix": requested_changes,
|
|
606
|
+
"revision_required": revision_requested,
|
|
607
|
+
"feedback_applied": (True if not revision_requested and not requested_changes else existing_plan.get("feedback_applied")),
|
|
608
|
+
"feedback_not_applied_reason": (None if not revision_requested and not requested_changes else existing_plan.get("feedback_not_applied_reason")),
|
|
609
|
+
"created_at": existing_plan.get("created_at") or _now(),
|
|
610
|
+
}
|
|
611
|
+
)
|
|
612
|
+
write_json(out_dir / "revision_plan.json", sanitize_public_obj(existing_plan))
|
|
613
|
+
_write_loop_manifest(package_root)
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def record_followup_review_update(run_root: Path, *, followup_index: int, record_only: bool, note: str) -> Path:
|
|
617
|
+
root = run_root / "loops" / "followups" / f"{followup_index:03d}"
|
|
618
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
619
|
+
packages = sorted((run_root / "packages").glob("*")) if (run_root / "packages").exists() else []
|
|
620
|
+
latest = packages[-1] if packages else None
|
|
621
|
+
refs: list[str] = []
|
|
622
|
+
if latest and (latest / "loops" / "loop_manifest.json").exists():
|
|
623
|
+
refs.append(f"packages/{latest.name}/loops/loop_manifest.json")
|
|
624
|
+
payload = sanitize_public_obj(
|
|
625
|
+
{
|
|
626
|
+
"followup_index": followup_index,
|
|
627
|
+
"record_only": record_only,
|
|
628
|
+
"note": note,
|
|
629
|
+
"previous_loop_refs": refs,
|
|
630
|
+
"new_execution_data_recorded": not record_only,
|
|
631
|
+
"reason_no_new_loop_execution": "Record-only follow-up captured user feedback without running the Apprentice Agent." if record_only else None,
|
|
632
|
+
"created_at": _now(),
|
|
633
|
+
}
|
|
634
|
+
)
|
|
635
|
+
write_json(root / "followup_review_update.json", payload)
|
|
636
|
+
(root / "followup_review_update.md").write_text(
|
|
637
|
+
"# Follow-up Review Update\n\n"
|
|
638
|
+
f"Follow-up: {followup_index}\n\n"
|
|
639
|
+
f"Record-only: {record_only}\n\n"
|
|
640
|
+
f"{payload.get('reason_no_new_loop_execution') or note}\n"
|
|
641
|
+
)
|
|
642
|
+
return root / "followup_review_update.json"
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
def summarize_loop_reviews(root: Path) -> dict[str, Any]:
|
|
646
|
+
root = Path(root)
|
|
647
|
+
manifests = list(root.glob("loops/loop_manifest.json"))
|
|
648
|
+
manifests.extend(root.glob("packages/*/loops/loop_manifest.json"))
|
|
649
|
+
if not manifests:
|
|
650
|
+
return {
|
|
651
|
+
"has_loop_review_packets": False,
|
|
652
|
+
"loop_iteration_count": 0,
|
|
653
|
+
"reviewer_types": [],
|
|
654
|
+
"final_loop_verdict": None,
|
|
655
|
+
"loop_review_refs": [],
|
|
656
|
+
}
|
|
657
|
+
total = 0
|
|
658
|
+
reviewers: list[str] = []
|
|
659
|
+
refs: list[str] = []
|
|
660
|
+
final = None
|
|
661
|
+
package_manifest_count = len(list(root.glob("packages/*/loops/loop_manifest.json")))
|
|
662
|
+
for path in sorted(manifests):
|
|
663
|
+
data = _read(path)
|
|
664
|
+
total += int(data.get("loop_iteration_count") or 0)
|
|
665
|
+
for reviewer in data.get("reviewer_types") or []:
|
|
666
|
+
if reviewer and reviewer not in reviewers:
|
|
667
|
+
reviewers.append(str(reviewer))
|
|
668
|
+
final = data.get("final_loop_verdict") or final
|
|
669
|
+
for iteration in data.get("iterations") or []:
|
|
670
|
+
ref = iteration.get("review_packet_ref")
|
|
671
|
+
if ref:
|
|
672
|
+
if path.parent.parent.parent.name == "packages":
|
|
673
|
+
if package_manifest_count > 1:
|
|
674
|
+
inner = str(ref).removeprefix("loops/")
|
|
675
|
+
refs.append(f"loops/{path.parent.parent.name}/{inner}")
|
|
676
|
+
else:
|
|
677
|
+
refs.append(str(ref))
|
|
678
|
+
else:
|
|
679
|
+
refs.append(str(ref))
|
|
680
|
+
return {
|
|
681
|
+
"has_loop_review_packets": total > 0,
|
|
682
|
+
"loop_iteration_count": total,
|
|
683
|
+
"reviewer_types": reviewers,
|
|
684
|
+
"final_loop_verdict": final,
|
|
685
|
+
"loop_review_refs": refs[:50],
|
|
686
|
+
}
|