self-evolve-framework 1.5.0 → 1.6.0
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/package.json +1 -1
- package/template/skills/skillopt-sleep/SKILL.md +42 -0
- package/template/skills/skillopt-sleep/configs/_base_/default.yaml +103 -0
- package/template/skills/skillopt-sleep/configs/alfworld/default.yaml +29 -0
- package/template/skills/skillopt-sleep/configs/docvqa/default.yaml +28 -0
- package/template/skills/skillopt-sleep/configs/features/soft_gate.yaml +47 -0
- package/template/skills/skillopt-sleep/configs/livemathematicianbench/default.yaml +22 -0
- package/template/skills/skillopt-sleep/configs/officeqa/default.yaml +34 -0
- package/template/skills/skillopt-sleep/configs/searchqa/default.yaml +32 -0
- package/template/skills/skillopt-sleep/configs/spreadsheetbench/default.yaml +34 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/__init__.py +28 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/config.py +282 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/datasets/__init__.py +7 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/datasets/base.py +512 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/engine/__init__.py +9 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/engine/trainer.py +2379 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/__init__.py +1 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/_template/README.md +43 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/_template/config_template.yaml +55 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/_template/env_template.py +151 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/_template/loader_template.py +87 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/__init__.py +5 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/adapter.py +428 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/dataloader.py +123 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/prompts/analyst_error.md +55 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/prompts/analyst_success.md +33 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/prompts/rollout_no_history.md +8 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/prompts/rollout_with_history.md +9 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/prompts/rollout_with_memory.md +16 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/reflect.py +4 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/rollout.py +366 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/skills/initial.md +45 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/__init__.py +9 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/alfworld_envs.py +221 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/alfworld_projection.py +60 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/alfworld_prompts.py +8 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/config_tw.yaml +145 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/env_base.py +84 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/env_manager.py +139 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/memory.py +87 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/base.py +329 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/__init__.py +1 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/adapter.py +90 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/dataloader.py +61 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/evaluator.py +113 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/prompts/analyst_error.md +35 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/prompts/analyst_success.md +24 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/prompts/rollout_system.md +12 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/rollout.py +391 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/skills/initial.md +11 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/__init__.py +1 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/adapter.py +129 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/dataloader.py +308 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/evaluator.py +62 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/prompts/analyst_error.md +37 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/prompts/analyst_success.md +25 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/prompts/rollout_system.md +12 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/reflect.py +4 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/rollout.py +434 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/skills/initial.md +16 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/__init__.py +1 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/adapter.py +112 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/dataloader.py +71 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/evaluator.py +46 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/prompts/analyst_error.md +37 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/prompts/analyst_success.md +25 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/prompts/rollout_system.md +15 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/rollout.py +799 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/skills/initial.md +15 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/tool_runtime.py +552 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/__init__.py +1 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/adapter.py +96 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/dataloader.py +42 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/evaluator.py +100 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/prompts/analyst_error.md +46 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/prompts/analyst_success.md +32 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/prompts/rollout_system.md +13 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/reflect.py +4 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/rollout.py +494 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/skills/initial.md +3 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/__init__.py +5 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/adapter.py +159 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/codegen_agent.py +731 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/dataloader.py +37 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/evaluator.py +158 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/executor.py +67 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/prompts/analyst_error.md +46 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/prompts/analyst_success.md +32 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/prompts/codegen_system.md +1 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/prompts/critical_rules.md +9 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/prompts/react_system.md +21 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/react_agent.py +395 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/reflect.py +4 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/rollout.py +979 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/skills/initial.md +56 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/evaluation/__init__.py +13 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/evaluation/gate.py +148 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/gradient/__init__.py +15 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/gradient/aggregate.py +253 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/gradient/reflect.py +635 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/__init__.py +514 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/azure_openai.py +915 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/backend_config.py +185 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/claude_backend.py +371 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/codex_backend.py +666 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/codex_harness.py +1057 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/common.py +229 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/minimax_backend.py +277 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/qwen_backend.py +456 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/router.py +236 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/__init__.py +15 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/appendix.py +156 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/clip.py +109 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/lr_autonomous.py +108 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/meta_skill.py +79 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/rewrite.py +59 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/scheduler.py +127 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/select.py +4 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/skill.py +201 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/skill_aware.py +206 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/slow_update.py +396 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/update_modes.py +135 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/__init__.py +63 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_error.md +41 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_error_full_rewrite.md +32 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_error_rewrite.md +44 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_success.md +36 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_success_full_rewrite.md +30 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_success_rewrite.md +33 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/lr_autonomous.md +20 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_failure.md +30 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_failure_full_rewrite.md +28 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_failure_rewrite.md +26 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_final.md +33 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_final_full_rewrite.md +28 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_final_rewrite.md +25 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_success.md +28 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_success_full_rewrite.md +28 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_success_rewrite.md +25 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/meta_skill.md +40 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/ranking.md +20 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/ranking_rewrite.md +15 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/rewrite_skill.md +25 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/slow_update.md +60 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/scheduler/__init__.py +8 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/types.py +306 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/utils/__init__.py +4 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/utils/json_utils.py +172 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/utils/scoring.py +28 -0
|
@@ -0,0 +1,2379 @@
|
|
|
1
|
+
"""ReflACT Trainer — the main training loop.
|
|
2
|
+
|
|
3
|
+
Orchestrates the 6-stage ReflACT pipeline:
|
|
4
|
+
1. Rollout — execute episodes with current skill
|
|
5
|
+
2. Reflect — analyze trajectories, generate patches
|
|
6
|
+
3. Aggregate — hierarchical merge of patches
|
|
7
|
+
4. Select — rank and select top edits
|
|
8
|
+
5. Update — apply edits to skill document
|
|
9
|
+
6. Evaluate — validate candidate skill, accept/reject
|
|
10
|
+
|
|
11
|
+
The trainer is environment-agnostic; all environment-specific logic is
|
|
12
|
+
delegated to an :class:`~skillopt.envs.base.EnvAdapter` instance.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import glob
|
|
17
|
+
import json
|
|
18
|
+
import math
|
|
19
|
+
import os
|
|
20
|
+
import random
|
|
21
|
+
import re
|
|
22
|
+
import time
|
|
23
|
+
from collections import defaultdict
|
|
24
|
+
|
|
25
|
+
from skillopt.datasets.base import BatchSpec
|
|
26
|
+
from skillopt.envs.base import EnvAdapter
|
|
27
|
+
from skillopt.evaluation.gate import GateResult, evaluate_gate, select_gate_score
|
|
28
|
+
from skillopt.gradient.aggregate import merge_patches
|
|
29
|
+
from skillopt.optimizer.meta_skill import run_meta_skill
|
|
30
|
+
from skillopt.optimizer.clip import rank_and_select
|
|
31
|
+
from skillopt.optimizer.lr_autonomous import decide_autonomous_learning_rate
|
|
32
|
+
from skillopt.optimizer.rewrite import rewrite_skill_from_suggestions
|
|
33
|
+
from skillopt.optimizer.scheduler import build_scheduler
|
|
34
|
+
from skillopt.optimizer.skill import apply_patch_with_report
|
|
35
|
+
from skillopt.optimizer.appendix import (
|
|
36
|
+
append_to_appendix_field,
|
|
37
|
+
extract_appendix_notes as extract_appendix_notes_from_skill,
|
|
38
|
+
inject_empty_appendix_field,
|
|
39
|
+
_strip_all_appendix_fields,
|
|
40
|
+
)
|
|
41
|
+
from skillopt.optimizer.skill_aware import (
|
|
42
|
+
configure_skill_aware_reflection,
|
|
43
|
+
consolidate_appendix_notes,
|
|
44
|
+
extract_appendix_notes as extract_appendix_notes_from_result,
|
|
45
|
+
)
|
|
46
|
+
from skillopt.optimizer.slow_update import (
|
|
47
|
+
build_comparison_pairs,
|
|
48
|
+
extract_slow_update_field,
|
|
49
|
+
inject_empty_slow_update_field,
|
|
50
|
+
replace_slow_update_field,
|
|
51
|
+
run_slow_update,
|
|
52
|
+
save_comparison_pairs,
|
|
53
|
+
)
|
|
54
|
+
from skillopt.optimizer.update_modes import (
|
|
55
|
+
get_payload_items,
|
|
56
|
+
is_full_rewrite_minibatch_mode,
|
|
57
|
+
normalize_update_mode,
|
|
58
|
+
payload_label,
|
|
59
|
+
short_item_summary,
|
|
60
|
+
)
|
|
61
|
+
from skillopt.model import (
|
|
62
|
+
chat_optimizer,
|
|
63
|
+
configure_azure_openai,
|
|
64
|
+
configure_claude_code_exec,
|
|
65
|
+
configure_codex_exec,
|
|
66
|
+
configure_minimax_chat,
|
|
67
|
+
configure_qwen_chat,
|
|
68
|
+
get_token_summary,
|
|
69
|
+
reset_token_tracker,
|
|
70
|
+
set_reasoning_effort,
|
|
71
|
+
set_target_backend,
|
|
72
|
+
set_target_deployment,
|
|
73
|
+
set_optimizer_backend,
|
|
74
|
+
set_optimizer_deployment,
|
|
75
|
+
)
|
|
76
|
+
from skillopt.utils import compute_score, skill_hash
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ── Skill-aware reflection: appendix flush ───────────────────────────────────
|
|
80
|
+
|
|
81
|
+
def _flush_skill_aware_appendix(
|
|
82
|
+
current_skill: str,
|
|
83
|
+
all_raw_patches: list,
|
|
84
|
+
step_rec: dict,
|
|
85
|
+
step_dir: str,
|
|
86
|
+
cfg: dict,
|
|
87
|
+
) -> str:
|
|
88
|
+
"""Append this step's EXECUTION_LAPSE notes into the protected appendix.
|
|
89
|
+
|
|
90
|
+
Returns the (possibly) updated skill. Must be called on BOTH the normal
|
|
91
|
+
update path and the skip branches: a lapse-only step yields no body
|
|
92
|
+
patches by design (analysts return ``edits: []`` carriers), so the skip
|
|
93
|
+
paths would otherwise silently drop every note of the step.
|
|
94
|
+
"""
|
|
95
|
+
step_appendix_notes: list[str] = []
|
|
96
|
+
for rp in all_raw_patches:
|
|
97
|
+
if isinstance(rp, dict):
|
|
98
|
+
step_appendix_notes.extend(extract_appendix_notes_from_result(rp))
|
|
99
|
+
if not step_appendix_notes:
|
|
100
|
+
return current_skill
|
|
101
|
+
|
|
102
|
+
before_notes = extract_appendix_notes_from_skill(current_skill)
|
|
103
|
+
current_skill = append_to_appendix_field(
|
|
104
|
+
current_skill, step_appendix_notes,
|
|
105
|
+
)
|
|
106
|
+
after_notes = extract_appendix_notes_from_skill(current_skill)
|
|
107
|
+
n_added = len(after_notes) - len(before_notes)
|
|
108
|
+
step_rec["n_execution_lapse_notes"] = len(step_appendix_notes)
|
|
109
|
+
step_rec["n_appendix_notes_added"] = n_added
|
|
110
|
+
step_rec["n_appendix_notes_total"] = len(after_notes)
|
|
111
|
+
with open(os.path.join(step_dir, "appendix_notes.json"), "w") as f:
|
|
112
|
+
json.dump(
|
|
113
|
+
{
|
|
114
|
+
"step_notes": step_appendix_notes,
|
|
115
|
+
"appendix_after": after_notes,
|
|
116
|
+
},
|
|
117
|
+
f, indent=2, ensure_ascii=False,
|
|
118
|
+
)
|
|
119
|
+
print(
|
|
120
|
+
f" [skill-aware] +{n_added} appendix note(s) "
|
|
121
|
+
f"(total {len(after_notes)}) from {len(step_appendix_notes)} lapse signal(s)"
|
|
122
|
+
)
|
|
123
|
+
# Threshold-gated LLM consolidation (paper Eq.11): when the
|
|
124
|
+
# appendix grows past N notes, compact it with one optimizer
|
|
125
|
+
# call (dedupe / merge / shorten). 0 disables it. Any failure
|
|
126
|
+
# leaves the appendix unchanged.
|
|
127
|
+
consolidate_threshold = int(
|
|
128
|
+
cfg.get("skill_aware_consolidate_threshold", 0) or 0
|
|
129
|
+
)
|
|
130
|
+
if consolidate_threshold > 0 and len(after_notes) > consolidate_threshold:
|
|
131
|
+
compacted = consolidate_appendix_notes(
|
|
132
|
+
after_notes, chat_fn=chat_optimizer,
|
|
133
|
+
)
|
|
134
|
+
if compacted and len(compacted) < len(after_notes):
|
|
135
|
+
current_skill = append_to_appendix_field(
|
|
136
|
+
_strip_all_appendix_fields(current_skill), compacted,
|
|
137
|
+
)
|
|
138
|
+
step_rec["n_appendix_notes_consolidated"] = len(compacted)
|
|
139
|
+
step_rec["n_appendix_notes_total"] = len(compacted)
|
|
140
|
+
print(
|
|
141
|
+
f" [skill-aware] consolidated appendix "
|
|
142
|
+
f"{len(after_notes)} -> {len(compacted)} notes"
|
|
143
|
+
)
|
|
144
|
+
return current_skill
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# ── Patch normalization ───────────────────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
def _normalise_patches(
|
|
150
|
+
raw_patches: list[dict | None],
|
|
151
|
+
update_mode: str = "patch",
|
|
152
|
+
) -> tuple[list[dict], list[dict]]:
|
|
153
|
+
"""Extract inner 'patch' sub-dict, split into failure/success lists.
|
|
154
|
+
|
|
155
|
+
Each element is expected to conform to :class:`~skillopt.types.RawPatch`.
|
|
156
|
+
"""
|
|
157
|
+
mode = normalize_update_mode(update_mode)
|
|
158
|
+
failure: list[dict] = []
|
|
159
|
+
success: list[dict] = []
|
|
160
|
+
for p in raw_patches:
|
|
161
|
+
if not isinstance(p, dict):
|
|
162
|
+
continue
|
|
163
|
+
inner = p.get("patch", p)
|
|
164
|
+
if not isinstance(inner, dict):
|
|
165
|
+
continue
|
|
166
|
+
items = get_payload_items(inner, mode)
|
|
167
|
+
if not items:
|
|
168
|
+
continue
|
|
169
|
+
support = max(int(p.get("batch_size", 0) or 0), 1)
|
|
170
|
+
for item in items:
|
|
171
|
+
if isinstance(item, dict):
|
|
172
|
+
item.setdefault("source_type", p.get("source_type", "failure"))
|
|
173
|
+
item.setdefault("support_count", support)
|
|
174
|
+
if p.get("source_type", "failure") == "success":
|
|
175
|
+
success.append(inner)
|
|
176
|
+
else:
|
|
177
|
+
failure.append(inner)
|
|
178
|
+
return failure, success
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _normalise_longitudinal_pair_policy(policy: str | None) -> str:
|
|
182
|
+
raw = str(policy or "mixed").strip().lower()
|
|
183
|
+
aliases = {
|
|
184
|
+
"mixed": "mixed",
|
|
185
|
+
"default": "mixed",
|
|
186
|
+
"random": "mixed",
|
|
187
|
+
"all": "mixed",
|
|
188
|
+
"changed": "changed",
|
|
189
|
+
"change": "changed",
|
|
190
|
+
"delta": "changed",
|
|
191
|
+
"10_01": "changed",
|
|
192
|
+
"01_10": "changed",
|
|
193
|
+
"unchanged": "unchanged",
|
|
194
|
+
"stable": "unchanged",
|
|
195
|
+
"same": "unchanged",
|
|
196
|
+
"00_11": "unchanged",
|
|
197
|
+
}
|
|
198
|
+
if raw not in aliases:
|
|
199
|
+
raise ValueError(
|
|
200
|
+
"optimizer.longitudinal_pair_policy must be one of "
|
|
201
|
+
"mixed, changed, unchanged"
|
|
202
|
+
)
|
|
203
|
+
return aliases[raw]
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _normalise_lr_control_mode(mode: str | None) -> str:
|
|
207
|
+
raw = str(mode or "fixed").strip().lower()
|
|
208
|
+
aliases = {
|
|
209
|
+
"fixed": "fixed",
|
|
210
|
+
"manual": "fixed",
|
|
211
|
+
"scheduler": "fixed",
|
|
212
|
+
"scheduled": "fixed",
|
|
213
|
+
"autonomous": "autonomous",
|
|
214
|
+
"auto": "autonomous",
|
|
215
|
+
"optimizer": "autonomous",
|
|
216
|
+
"none": "none",
|
|
217
|
+
"off": "none",
|
|
218
|
+
"no_lr": "none",
|
|
219
|
+
}
|
|
220
|
+
if raw not in aliases:
|
|
221
|
+
raise ValueError("optimizer.lr_control_mode must be one of fixed, autonomous, none")
|
|
222
|
+
return aliases[raw]
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _filter_longitudinal_pairs(pairs: list[dict], policy: str) -> list[dict]:
|
|
226
|
+
if policy == "mixed":
|
|
227
|
+
return pairs
|
|
228
|
+
if policy == "changed":
|
|
229
|
+
keep = {"improved", "regressed"}
|
|
230
|
+
elif policy == "unchanged":
|
|
231
|
+
keep = {"persistent_fail", "stable_success"}
|
|
232
|
+
else:
|
|
233
|
+
raise ValueError(f"Unknown longitudinal pair policy: {policy}")
|
|
234
|
+
return [p for p in pairs if p.get("category") in keep]
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _pair_category_counts(pairs: list[dict]) -> dict[str, int]:
|
|
238
|
+
counts = {
|
|
239
|
+
"improved": 0,
|
|
240
|
+
"regressed": 0,
|
|
241
|
+
"persistent_fail": 0,
|
|
242
|
+
"stable_success": 0,
|
|
243
|
+
}
|
|
244
|
+
for pair in pairs:
|
|
245
|
+
cat = str(pair.get("category", ""))
|
|
246
|
+
counts[cat] = counts.get(cat, 0) + 1
|
|
247
|
+
return counts
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _safe_pair_id(value: str) -> str:
|
|
251
|
+
safe = re.sub(r"[^A-Za-z0-9_.-]+", "_", str(value)).strip("_")
|
|
252
|
+
return safe[:80] or "item"
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _build_longitudinal_pairs(
|
|
256
|
+
*,
|
|
257
|
+
adapter: EnvAdapter,
|
|
258
|
+
dataloader,
|
|
259
|
+
prev_skill: str,
|
|
260
|
+
curr_skill: str,
|
|
261
|
+
initial_items: list[dict],
|
|
262
|
+
initial_prev_results: list[dict],
|
|
263
|
+
initial_curr_results: list[dict],
|
|
264
|
+
prev_rollout_dir: str,
|
|
265
|
+
curr_rollout_dir: str,
|
|
266
|
+
policy: str,
|
|
267
|
+
target_n: int,
|
|
268
|
+
seed: int,
|
|
269
|
+
out_root: str,
|
|
270
|
+
) -> tuple[list[dict], list[dict]]:
|
|
271
|
+
"""Build longitudinal pairs, optionally filtering by change category.
|
|
272
|
+
|
|
273
|
+
``mixed`` preserves the legacy behavior exactly. ``changed`` keeps only
|
|
274
|
+
10/01 pairs and attempts to top up to ``target_n`` by scanning the train
|
|
275
|
+
split once. ``unchanged`` keeps only 00/11 pairs and does not top up.
|
|
276
|
+
"""
|
|
277
|
+
all_pairs = build_comparison_pairs(
|
|
278
|
+
initial_prev_results,
|
|
279
|
+
initial_curr_results,
|
|
280
|
+
initial_items,
|
|
281
|
+
prev_rollout_dir=prev_rollout_dir,
|
|
282
|
+
curr_rollout_dir=curr_rollout_dir,
|
|
283
|
+
)
|
|
284
|
+
selected_pairs = _filter_longitudinal_pairs(all_pairs, policy)
|
|
285
|
+
if policy != "changed" or len(selected_pairs) >= target_n or dataloader is None:
|
|
286
|
+
return selected_pairs, all_pairs
|
|
287
|
+
|
|
288
|
+
train_items = list(getattr(dataloader, "train_items", []) or [])
|
|
289
|
+
if not train_items:
|
|
290
|
+
return selected_pairs, all_pairs
|
|
291
|
+
|
|
292
|
+
seen_ids = {str(p.get("id", "")) for p in all_pairs}
|
|
293
|
+
rng = random.Random(seed)
|
|
294
|
+
candidates = list(train_items)
|
|
295
|
+
rng.shuffle(candidates)
|
|
296
|
+
candidates = [item for item in candidates if str(item.get("id", "")) not in seen_ids]
|
|
297
|
+
|
|
298
|
+
for idx, item in enumerate(candidates):
|
|
299
|
+
if len(selected_pairs) >= target_n:
|
|
300
|
+
break
|
|
301
|
+
item_id = _safe_pair_id(str(item.get("id", f"item_{idx}")))
|
|
302
|
+
batch = BatchSpec(
|
|
303
|
+
phase="train",
|
|
304
|
+
split="train",
|
|
305
|
+
seed=seed + idx + 1,
|
|
306
|
+
batch_size=1,
|
|
307
|
+
payload=[item],
|
|
308
|
+
)
|
|
309
|
+
env = adapter.build_env_from_batch(batch, out_root=out_root)
|
|
310
|
+
prev_dir = os.path.join(prev_rollout_dir, "topup", item_id)
|
|
311
|
+
curr_dir = os.path.join(curr_rollout_dir, "topup", item_id)
|
|
312
|
+
prev_results = adapter.rollout(env, prev_skill, prev_dir)
|
|
313
|
+
curr_results = adapter.rollout(env, curr_skill, curr_dir)
|
|
314
|
+
pair = build_comparison_pairs(
|
|
315
|
+
prev_results,
|
|
316
|
+
curr_results,
|
|
317
|
+
[item],
|
|
318
|
+
prev_rollout_dir=prev_dir,
|
|
319
|
+
curr_rollout_dir=curr_dir,
|
|
320
|
+
)
|
|
321
|
+
all_pairs.extend(pair)
|
|
322
|
+
selected_pairs.extend(_filter_longitudinal_pairs(pair, policy))
|
|
323
|
+
|
|
324
|
+
return selected_pairs[:target_n], all_pairs
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
# ── History / persistence helpers ─────────────────────────────────────────────
|
|
328
|
+
|
|
329
|
+
_SECRET_KEYS = {
|
|
330
|
+
"azure_api_key",
|
|
331
|
+
"api_key",
|
|
332
|
+
"openai_api_key",
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _redact_value(val: str) -> str:
|
|
337
|
+
if len(val) <= 8:
|
|
338
|
+
return "*" * len(val)
|
|
339
|
+
return f"{val[:4]}...{val[-4:]}"
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _redact_cfg(cfg: dict) -> dict:
|
|
343
|
+
redacted = dict(cfg)
|
|
344
|
+
for key in list(redacted):
|
|
345
|
+
if key.lower() in _SECRET_KEYS and redacted.get(key):
|
|
346
|
+
redacted[key] = _redact_value(str(redacted[key]))
|
|
347
|
+
return redacted
|
|
348
|
+
|
|
349
|
+
def _load_history(out_root: str) -> list[dict]:
|
|
350
|
+
path = os.path.join(out_root, "history.json")
|
|
351
|
+
if os.path.exists(path):
|
|
352
|
+
with open(path) as f:
|
|
353
|
+
return json.load(f)
|
|
354
|
+
return []
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _save_history(out_root: str, history: list[dict]) -> None:
|
|
358
|
+
path = os.path.join(out_root, "history.json")
|
|
359
|
+
with open(path, "w") as f:
|
|
360
|
+
json.dump(history, f, ensure_ascii=False, indent=2)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _save_skill(out_root: str, step: int, content: str) -> None:
|
|
364
|
+
skills_dir = os.path.join(out_root, "skills")
|
|
365
|
+
os.makedirs(skills_dir, exist_ok=True)
|
|
366
|
+
with open(os.path.join(skills_dir, f"skill_v{step:04d}.md"), "w") as f:
|
|
367
|
+
f.write(content)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _load_skill(out_root: str, step: int) -> str:
|
|
371
|
+
path = os.path.join(out_root, "skills", f"skill_v{step:04d}.md")
|
|
372
|
+
with open(path) as f:
|
|
373
|
+
return f.read()
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _load_meta_skill_content(out_root: str, epoch: int) -> str:
|
|
377
|
+
if epoch <= 0:
|
|
378
|
+
return ""
|
|
379
|
+
path = os.path.join(
|
|
380
|
+
out_root, "meta_skill", f"epoch_{epoch:02d}", "meta_skill_result.json",
|
|
381
|
+
)
|
|
382
|
+
if not os.path.exists(path):
|
|
383
|
+
return ""
|
|
384
|
+
try:
|
|
385
|
+
with open(path) as f:
|
|
386
|
+
result = json.load(f)
|
|
387
|
+
return str(result.get("meta_skill_content", "")).strip()
|
|
388
|
+
except Exception:
|
|
389
|
+
return ""
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def _load_runtime_state(out_root: str) -> dict | None:
|
|
393
|
+
path = os.path.join(out_root, "runtime_state.json")
|
|
394
|
+
if not os.path.exists(path):
|
|
395
|
+
return None
|
|
396
|
+
try:
|
|
397
|
+
with open(path) as f:
|
|
398
|
+
state = json.load(f)
|
|
399
|
+
return state if isinstance(state, dict) else None
|
|
400
|
+
except Exception:
|
|
401
|
+
return None
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _save_runtime_state(out_root: str, state: dict) -> None:
|
|
405
|
+
path = os.path.join(out_root, "runtime_state.json")
|
|
406
|
+
with open(path, "w") as f:
|
|
407
|
+
json.dump(state, f, ensure_ascii=False, indent=2)
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _resolve_train_size(cfg: dict, dataloader) -> int:
|
|
411
|
+
configured = int(cfg.get("train_size", 0) or 0)
|
|
412
|
+
inferred: int | None = None
|
|
413
|
+
|
|
414
|
+
if dataloader is not None:
|
|
415
|
+
getter = getattr(dataloader, "get_train_size", None)
|
|
416
|
+
if callable(getter):
|
|
417
|
+
try:
|
|
418
|
+
value = getter()
|
|
419
|
+
except Exception:
|
|
420
|
+
value = None
|
|
421
|
+
if value is not None:
|
|
422
|
+
inferred = int(value)
|
|
423
|
+
elif hasattr(dataloader, "train_items"):
|
|
424
|
+
try:
|
|
425
|
+
inferred = len(getattr(dataloader, "train_items"))
|
|
426
|
+
except Exception:
|
|
427
|
+
inferred = None
|
|
428
|
+
|
|
429
|
+
if inferred is not None and inferred <= 0:
|
|
430
|
+
inferred = None
|
|
431
|
+
|
|
432
|
+
if configured > 0 and inferred is not None and configured != inferred:
|
|
433
|
+
raise ValueError(
|
|
434
|
+
f"Configured train_size={configured} does not match loaded train split "
|
|
435
|
+
f"size={inferred}. Fix the config or the dataset split."
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
train_size = configured if configured > 0 else inferred
|
|
439
|
+
if train_size is None or train_size <= 0:
|
|
440
|
+
raise ValueError(
|
|
441
|
+
"Unable to determine train_size automatically. "
|
|
442
|
+
"Provide train.train_size in the config for this environment."
|
|
443
|
+
)
|
|
444
|
+
return int(train_size)
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def _compute_task_type_buckets(results: list[dict], task_types: list[str]) -> dict[str, dict]:
|
|
448
|
+
"""Compute per-task-type success rates."""
|
|
449
|
+
buckets: dict[str, dict] = {}
|
|
450
|
+
for task in task_types + ["overall"]:
|
|
451
|
+
buckets[task] = {"total": 0, "hard": 0, "soft": 0.0}
|
|
452
|
+
for r in results:
|
|
453
|
+
tt = r.get("task_type", "other")
|
|
454
|
+
for key in [tt, "overall"]:
|
|
455
|
+
if key not in buckets:
|
|
456
|
+
buckets[key] = {"total": 0, "hard": 0, "soft": 0.0}
|
|
457
|
+
buckets[key]["total"] += 1
|
|
458
|
+
buckets[key]["hard"] += float(r.get("hard", 0))
|
|
459
|
+
buckets[key]["soft"] += float(r.get("soft", 0.0))
|
|
460
|
+
return buckets
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def _format_rejection_buffer(buffer: list[dict]) -> str:
|
|
464
|
+
"""**DEPRECATED** — kept for backward compat; use _format_step_buffer."""
|
|
465
|
+
return _format_step_buffer(buffer)
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def _extract_failure_patterns(
|
|
469
|
+
rollout_results: list[dict],
|
|
470
|
+
step_dir: str,
|
|
471
|
+
) -> list[dict]:
|
|
472
|
+
"""Extract compact failure patterns from rollout results.
|
|
473
|
+
|
|
474
|
+
Uses analyst ``failure_summary`` from minibatch patches when available,
|
|
475
|
+
otherwise falls back to ``fail_reason`` prefix grouping.
|
|
476
|
+
"""
|
|
477
|
+
failures = [r for r in rollout_results if not r.get("hard") or float(r.get("hard", 0)) < 1e-9]
|
|
478
|
+
if not failures:
|
|
479
|
+
return []
|
|
480
|
+
|
|
481
|
+
# Group by fail_reason prefix
|
|
482
|
+
groups: dict[str, list[dict]] = defaultdict(list)
|
|
483
|
+
for r in failures:
|
|
484
|
+
reason = r.get("fail_reason", "unknown")
|
|
485
|
+
prefix = reason.split(":")[0].strip() if ":" in reason else reason
|
|
486
|
+
groups[prefix].append(r)
|
|
487
|
+
|
|
488
|
+
# Try richer descriptions from analyst patches
|
|
489
|
+
analyst_descs: list[str] = []
|
|
490
|
+
patch_globs = [
|
|
491
|
+
os.path.join(step_dir, "patches", "minibatch_fail_*.json"),
|
|
492
|
+
os.path.join(step_dir, "batch_*", "patches", "minibatch_fail_*.json"),
|
|
493
|
+
]
|
|
494
|
+
seen_patch_files: set[str] = set()
|
|
495
|
+
for pattern in patch_globs:
|
|
496
|
+
for fname in sorted(glob.glob(pattern)):
|
|
497
|
+
if fname in seen_patch_files:
|
|
498
|
+
continue
|
|
499
|
+
seen_patch_files.add(fname)
|
|
500
|
+
try:
|
|
501
|
+
with open(fname) as f:
|
|
502
|
+
patch = json.load(f)
|
|
503
|
+
for fs in patch.get("failure_summary", []):
|
|
504
|
+
ft = fs.get("failure_type", "")
|
|
505
|
+
sd = fs.get("description", "")
|
|
506
|
+
analyst_descs.append(f"{ft}: {sd}" if sd else ft)
|
|
507
|
+
except Exception:
|
|
508
|
+
pass
|
|
509
|
+
|
|
510
|
+
patterns = []
|
|
511
|
+
desc_iter = iter(analyst_descs)
|
|
512
|
+
for prefix, items in groups.items():
|
|
513
|
+
desc = next(desc_iter, None) or prefix
|
|
514
|
+
patterns.append({
|
|
515
|
+
"pattern": desc,
|
|
516
|
+
"count": len(items),
|
|
517
|
+
"task_ids": [str(r.get("id", "?")) for r in items],
|
|
518
|
+
})
|
|
519
|
+
return patterns
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def _format_step_buffer(buffer: list[dict]) -> str:
|
|
523
|
+
"""Format the unified step buffer into a single context block.
|
|
524
|
+
|
|
525
|
+
Each entry captures what happened at a previous step: failure patterns
|
|
526
|
+
observed during rollout, and — when the step was rejected — the specific
|
|
527
|
+
edits that were tried and the resulting score drop.
|
|
528
|
+
|
|
529
|
+
Returns empty string when *buffer* is empty.
|
|
530
|
+
"""
|
|
531
|
+
if not buffer:
|
|
532
|
+
return ""
|
|
533
|
+
|
|
534
|
+
parts = [
|
|
535
|
+
"Below is a summary of previous steps in this epoch. "
|
|
536
|
+
"Use it to avoid repeating ineffective edits and to prioritise "
|
|
537
|
+
"failure patterns that remain unsolved.\n"
|
|
538
|
+
]
|
|
539
|
+
|
|
540
|
+
for entry in buffer:
|
|
541
|
+
step = entry["step"]
|
|
542
|
+
action = entry["action"]
|
|
543
|
+
n_fail = entry.get("n_fail", 0)
|
|
544
|
+
n_total = entry.get("n_total", "?")
|
|
545
|
+
|
|
546
|
+
parts.append(f"### Step {step} — {action.upper()} ({n_fail}/{n_total} failed)")
|
|
547
|
+
|
|
548
|
+
# Failure patterns
|
|
549
|
+
for p in entry.get("failure_patterns", []):
|
|
550
|
+
ids = ", ".join(p["task_ids"])
|
|
551
|
+
parts.append(f' - "{p["pattern"]}" (×{p["count"]}, tasks: {ids})')
|
|
552
|
+
|
|
553
|
+
# Rejected edits (only present on reject)
|
|
554
|
+
rejected = entry.get("rejected_edits", [])
|
|
555
|
+
if rejected:
|
|
556
|
+
score_before = entry.get("score_before", "?")
|
|
557
|
+
score_after = entry.get("score_after", "?")
|
|
558
|
+
parts.append(
|
|
559
|
+
f" Rejected edits (score {score_before} → {score_after}):"
|
|
560
|
+
)
|
|
561
|
+
for i, e in enumerate(rejected, 1):
|
|
562
|
+
if e.get("op") is not None:
|
|
563
|
+
op = e.get("op", "?")
|
|
564
|
+
content = e.get("content", "")
|
|
565
|
+
target = e.get("target", "")
|
|
566
|
+
if target:
|
|
567
|
+
parts.append(f' {i}. [{op}] target="{target}" → "{content}"')
|
|
568
|
+
else:
|
|
569
|
+
parts.append(f' {i}. [{op}] "{content}"')
|
|
570
|
+
else:
|
|
571
|
+
kind = e.get("type", "?")
|
|
572
|
+
title = e.get("title", "")
|
|
573
|
+
instruction = e.get("instruction", "")
|
|
574
|
+
parts.append(f' {i}. [{kind}] "{title}" → "{instruction}"')
|
|
575
|
+
|
|
576
|
+
return "\n".join(parts)
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
# ── Trainer ──────────────────────────────────────────────────────────────────
|
|
580
|
+
|
|
581
|
+
class ReflACTTrainer:
|
|
582
|
+
"""Main ReflACT training loop.
|
|
583
|
+
|
|
584
|
+
Parameters
|
|
585
|
+
----------
|
|
586
|
+
cfg : dict
|
|
587
|
+
Configuration dictionary. See ``configs/alfworld_default.yaml``
|
|
588
|
+
for the full list of keys.
|
|
589
|
+
adapter : EnvAdapter
|
|
590
|
+
Environment adapter instance.
|
|
591
|
+
"""
|
|
592
|
+
|
|
593
|
+
def __init__(self, cfg: dict, adapter: EnvAdapter) -> None:
|
|
594
|
+
self.cfg = cfg
|
|
595
|
+
self.adapter = adapter
|
|
596
|
+
|
|
597
|
+
def train(self) -> dict:
|
|
598
|
+
"""Execute the full ReflACT training loop. Returns summary dict."""
|
|
599
|
+
cfg = self.cfg
|
|
600
|
+
adapter = self.adapter
|
|
601
|
+
out_root = cfg["out_root"]
|
|
602
|
+
os.makedirs(out_root, exist_ok=True)
|
|
603
|
+
|
|
604
|
+
# ── Adapter setup (one-time init) ────────────────────────────
|
|
605
|
+
adapter.setup(cfg)
|
|
606
|
+
dataloader = adapter.get_dataloader()
|
|
607
|
+
|
|
608
|
+
def _build_train_env(batch: BatchSpec):
|
|
609
|
+
env_manager = adapter.build_env_from_batch(batch, out_root=out_root)
|
|
610
|
+
return env_manager, batch.batch_size, batch.seed
|
|
611
|
+
|
|
612
|
+
def _build_eval_env(split: str, env_num: int, seed: int):
|
|
613
|
+
if dataloader is None:
|
|
614
|
+
env_manager = adapter.build_eval_env(
|
|
615
|
+
env_num=env_num,
|
|
616
|
+
split=split,
|
|
617
|
+
seed=seed,
|
|
618
|
+
out_root=out_root,
|
|
619
|
+
)
|
|
620
|
+
actual_n = len(env_manager) if hasattr(env_manager, "__len__") else env_num
|
|
621
|
+
return env_manager, actual_n
|
|
622
|
+
|
|
623
|
+
batch = dataloader.build_eval_batch(
|
|
624
|
+
env_num=env_num,
|
|
625
|
+
split=split,
|
|
626
|
+
seed=seed,
|
|
627
|
+
out_root=out_root,
|
|
628
|
+
)
|
|
629
|
+
env_manager = adapter.build_env_from_batch(batch, out_root=out_root)
|
|
630
|
+
return env_manager, batch.batch_size
|
|
631
|
+
|
|
632
|
+
# ── Configure models ─────────────────────────────────────────────
|
|
633
|
+
backend = cfg.get("model_backend", "azure_openai")
|
|
634
|
+
configure_azure_openai(
|
|
635
|
+
endpoint=(
|
|
636
|
+
cfg.get("azure_openai_endpoint")
|
|
637
|
+
or cfg.get("azure_endpoint")
|
|
638
|
+
or None
|
|
639
|
+
),
|
|
640
|
+
api_version=(
|
|
641
|
+
cfg.get("azure_openai_api_version")
|
|
642
|
+
or cfg.get("azure_api_version")
|
|
643
|
+
or None
|
|
644
|
+
),
|
|
645
|
+
api_key=(
|
|
646
|
+
cfg.get("azure_openai_api_key")
|
|
647
|
+
or cfg.get("azure_api_key")
|
|
648
|
+
or None
|
|
649
|
+
),
|
|
650
|
+
auth_mode=cfg.get("azure_openai_auth_mode") or None,
|
|
651
|
+
ad_scope=cfg.get("azure_openai_ad_scope") or None,
|
|
652
|
+
managed_identity_client_id=cfg.get("azure_openai_managed_identity_client_id") or None,
|
|
653
|
+
optimizer_endpoint=cfg.get("optimizer_azure_openai_endpoint") or None,
|
|
654
|
+
optimizer_api_version=cfg.get("optimizer_azure_openai_api_version") or None,
|
|
655
|
+
optimizer_api_key=cfg.get("optimizer_azure_openai_api_key") or None,
|
|
656
|
+
optimizer_auth_mode=cfg.get("optimizer_azure_openai_auth_mode") or None,
|
|
657
|
+
optimizer_ad_scope=cfg.get("optimizer_azure_openai_ad_scope") or None,
|
|
658
|
+
optimizer_managed_identity_client_id=(
|
|
659
|
+
cfg.get("optimizer_azure_openai_managed_identity_client_id") or None
|
|
660
|
+
),
|
|
661
|
+
target_endpoint=cfg.get("target_azure_openai_endpoint") or None,
|
|
662
|
+
target_api_version=cfg.get("target_azure_openai_api_version") or None,
|
|
663
|
+
target_api_key=cfg.get("target_azure_openai_api_key") or None,
|
|
664
|
+
target_auth_mode=cfg.get("target_azure_openai_auth_mode") or None,
|
|
665
|
+
target_ad_scope=cfg.get("target_azure_openai_ad_scope") or None,
|
|
666
|
+
target_managed_identity_client_id=(
|
|
667
|
+
cfg.get("target_azure_openai_managed_identity_client_id") or None
|
|
668
|
+
),
|
|
669
|
+
)
|
|
670
|
+
optimizer_backend = cfg.get("optimizer_backend")
|
|
671
|
+
target_backend = cfg.get("target_backend")
|
|
672
|
+
if not optimizer_backend or not target_backend:
|
|
673
|
+
if backend in {"claude", "claude_chat"}:
|
|
674
|
+
optimizer_backend = optimizer_backend or "claude_chat"
|
|
675
|
+
target_backend = target_backend or "claude_chat"
|
|
676
|
+
elif backend in {"codex", "codex_exec"}:
|
|
677
|
+
optimizer_backend = optimizer_backend or "openai_chat"
|
|
678
|
+
target_backend = target_backend or "codex_exec"
|
|
679
|
+
elif backend == "claude_code_exec":
|
|
680
|
+
optimizer_backend = optimizer_backend or "openai_chat"
|
|
681
|
+
target_backend = target_backend or "claude_code_exec"
|
|
682
|
+
elif backend in {"qwen", "qwen_chat"}:
|
|
683
|
+
optimizer_backend = optimizer_backend or "openai_chat"
|
|
684
|
+
target_backend = target_backend or "qwen_chat"
|
|
685
|
+
else:
|
|
686
|
+
optimizer_backend = optimizer_backend or "openai_chat"
|
|
687
|
+
target_backend = target_backend or "openai_chat"
|
|
688
|
+
cfg["optimizer_backend"] = optimizer_backend
|
|
689
|
+
cfg["target_backend"] = target_backend
|
|
690
|
+
set_optimizer_backend(optimizer_backend)
|
|
691
|
+
set_target_backend(target_backend)
|
|
692
|
+
set_optimizer_deployment(cfg["optimizer_model"])
|
|
693
|
+
set_target_deployment(cfg["target_model"])
|
|
694
|
+
configure_codex_exec(
|
|
695
|
+
path=cfg.get("codex_exec_path", "codex"),
|
|
696
|
+
sandbox=cfg.get("codex_exec_sandbox", "workspace-write"),
|
|
697
|
+
profile=cfg.get("codex_exec_profile", ""),
|
|
698
|
+
full_auto=cfg.get("codex_exec_full_auto", False),
|
|
699
|
+
reasoning_effort=cfg.get("codex_exec_reasoning_effort", "none"),
|
|
700
|
+
use_sdk=cfg.get("codex_exec_use_sdk", None),
|
|
701
|
+
network_access=cfg.get("codex_exec_network_access", False),
|
|
702
|
+
web_search=cfg.get("codex_exec_web_search", False),
|
|
703
|
+
approval_policy=cfg.get("codex_exec_approval_policy", "never"),
|
|
704
|
+
)
|
|
705
|
+
configure_claude_code_exec(
|
|
706
|
+
path=cfg.get("claude_code_exec_path", "claude"),
|
|
707
|
+
profile=cfg.get("claude_code_exec_profile", ""),
|
|
708
|
+
use_sdk=cfg.get("claude_code_exec_use_sdk", None),
|
|
709
|
+
effort=cfg.get("claude_code_exec_effort", cfg.get("reasoning_effort", "medium")),
|
|
710
|
+
max_thinking_tokens=cfg.get("claude_code_exec_max_thinking_tokens", 16384),
|
|
711
|
+
)
|
|
712
|
+
configure_qwen_chat(
|
|
713
|
+
base_url=cfg.get("qwen_chat_base_url") or None,
|
|
714
|
+
api_key=cfg.get("qwen_chat_api_key") or None,
|
|
715
|
+
temperature=cfg.get("qwen_chat_temperature"),
|
|
716
|
+
timeout_seconds=cfg.get("qwen_chat_timeout_seconds"),
|
|
717
|
+
max_tokens=cfg.get("qwen_chat_max_tokens"),
|
|
718
|
+
enable_thinking=cfg.get("qwen_chat_enable_thinking"),
|
|
719
|
+
optimizer_base_url=cfg.get("optimizer_qwen_chat_base_url") or None,
|
|
720
|
+
optimizer_api_key=cfg.get("optimizer_qwen_chat_api_key") or None,
|
|
721
|
+
optimizer_temperature=cfg.get("optimizer_qwen_chat_temperature"),
|
|
722
|
+
optimizer_timeout_seconds=cfg.get("optimizer_qwen_chat_timeout_seconds"),
|
|
723
|
+
optimizer_max_tokens=cfg.get("optimizer_qwen_chat_max_tokens"),
|
|
724
|
+
optimizer_enable_thinking=cfg.get("optimizer_qwen_chat_enable_thinking"),
|
|
725
|
+
target_base_url=cfg.get("target_qwen_chat_base_url") or None,
|
|
726
|
+
target_api_key=cfg.get("target_qwen_chat_api_key") or None,
|
|
727
|
+
target_temperature=cfg.get("target_qwen_chat_temperature"),
|
|
728
|
+
target_timeout_seconds=cfg.get("target_qwen_chat_timeout_seconds"),
|
|
729
|
+
target_max_tokens=cfg.get("target_qwen_chat_max_tokens"),
|
|
730
|
+
target_enable_thinking=cfg.get("target_qwen_chat_enable_thinking"),
|
|
731
|
+
)
|
|
732
|
+
configure_minimax_chat(
|
|
733
|
+
base_url=cfg.get("minimax_base_url") or None,
|
|
734
|
+
api_key=cfg.get("minimax_api_key") or None,
|
|
735
|
+
temperature=cfg.get("minimax_temperature"),
|
|
736
|
+
max_tokens=cfg.get("minimax_max_tokens"),
|
|
737
|
+
enable_thinking=cfg.get("minimax_enable_thinking"),
|
|
738
|
+
)
|
|
739
|
+
minimax_model_cfg = cfg.get("minimax_model")
|
|
740
|
+
if minimax_model_cfg and cfg.get("target_backend") == "minimax_chat":
|
|
741
|
+
set_target_deployment(str(minimax_model_cfg))
|
|
742
|
+
os.environ["REFLACT_CODEX_TRACE_TO_OPTIMIZER"] = (
|
|
743
|
+
"1"
|
|
744
|
+
if target_backend == "codex_exec" and cfg.get("codex_trace_to_optimizer", False)
|
|
745
|
+
else "0"
|
|
746
|
+
)
|
|
747
|
+
reasoning = cfg.get("reasoning_effort", "") or None
|
|
748
|
+
set_reasoning_effort(reasoning)
|
|
749
|
+
print(
|
|
750
|
+
f" [model config] backend={backend} "
|
|
751
|
+
f"optimizer={cfg['optimizer_model']} ({optimizer_backend}) "
|
|
752
|
+
f"target={cfg['target_model']} ({target_backend}) "
|
|
753
|
+
f"reasoning={reasoning or 'off'}"
|
|
754
|
+
)
|
|
755
|
+
|
|
756
|
+
# ── Initialize Ray ───────────────────────────────────────────────
|
|
757
|
+
if adapter.requires_ray():
|
|
758
|
+
try:
|
|
759
|
+
import ray
|
|
760
|
+
except ImportError as e:
|
|
761
|
+
raise ImportError(
|
|
762
|
+
"This environment requires ray, but ray is not installed."
|
|
763
|
+
) from e
|
|
764
|
+
|
|
765
|
+
if not ray.is_initialized():
|
|
766
|
+
ray.init(num_gpus=0)
|
|
767
|
+
|
|
768
|
+
# ── Load initial skill ───────────────────────────────────────────
|
|
769
|
+
skill_init_path = os.path.abspath(cfg["skill_init"])
|
|
770
|
+
if os.path.exists(skill_init_path):
|
|
771
|
+
with open(skill_init_path) as f:
|
|
772
|
+
skill_init = f.read()
|
|
773
|
+
print(f" [initial skill] {skill_init_path} ({len(skill_init)} chars)")
|
|
774
|
+
else:
|
|
775
|
+
skill_init = ""
|
|
776
|
+
print(" [initial skill] no initial skill file — starting from blank")
|
|
777
|
+
|
|
778
|
+
# ── Training parameters ──────────────────────────────────────────
|
|
779
|
+
batch_size = cfg["batch_size"]
|
|
780
|
+
num_epochs = cfg["num_epochs"]
|
|
781
|
+
accumulation = cfg["accumulation"]
|
|
782
|
+
seed = cfg["seed"]
|
|
783
|
+
merge_bs = cfg["merge_batch_size"]
|
|
784
|
+
max_analyst_rounds = int(cfg.get("max_analyst_rounds", 3) or 3)
|
|
785
|
+
update_mode = normalize_update_mode(cfg.get("skill_update_mode", "patch"))
|
|
786
|
+
lr_control_mode = _normalise_lr_control_mode(cfg.get("lr_control_mode", "fixed"))
|
|
787
|
+
if is_full_rewrite_minibatch_mode(update_mode):
|
|
788
|
+
lr_control_mode = "none"
|
|
789
|
+
longitudinal_pair_policy = _normalise_longitudinal_pair_policy(
|
|
790
|
+
cfg.get("longitudinal_pair_policy", "mixed")
|
|
791
|
+
)
|
|
792
|
+
rewrite_reasoning_effort = cfg.get("rewrite_reasoning_effort", "high")
|
|
793
|
+
if rewrite_reasoning_effort == "":
|
|
794
|
+
rewrite_reasoning_effort = None
|
|
795
|
+
rewrite_max_completion_tokens = int(cfg.get("rewrite_max_completion_tokens", 64000))
|
|
796
|
+
if batch_size <= 0:
|
|
797
|
+
raise ValueError(f"batch_size must be positive, got {batch_size}")
|
|
798
|
+
if accumulation <= 0:
|
|
799
|
+
raise ValueError(f"accumulation must be positive, got {accumulation}")
|
|
800
|
+
|
|
801
|
+
train_size = _resolve_train_size(cfg, dataloader)
|
|
802
|
+
steps_per_epoch = math.ceil(train_size / (batch_size * accumulation))
|
|
803
|
+
batches_per_epoch = steps_per_epoch * accumulation
|
|
804
|
+
total_steps = num_epochs * steps_per_epoch
|
|
805
|
+
|
|
806
|
+
# Persist resolved derived fields so config.json / summary.json match
|
|
807
|
+
# the actual runtime recipe.
|
|
808
|
+
cfg["train_size"] = train_size
|
|
809
|
+
cfg["steps_per_epoch"] = steps_per_epoch
|
|
810
|
+
cfg["batches_per_epoch"] = batches_per_epoch
|
|
811
|
+
cfg["samples_per_epoch"] = train_size
|
|
812
|
+
cfg["skill_update_mode"] = update_mode
|
|
813
|
+
cfg["lr_control_mode"] = lr_control_mode
|
|
814
|
+
|
|
815
|
+
# Save config after deriving runtime values.
|
|
816
|
+
with open(os.path.join(out_root, "config.json"), "w") as f:
|
|
817
|
+
json.dump(_redact_cfg(cfg), f, indent=2, ensure_ascii=False)
|
|
818
|
+
|
|
819
|
+
train_pool_size = train_size
|
|
820
|
+
|
|
821
|
+
scheduler = build_scheduler(
|
|
822
|
+
mode=cfg.get("lr_scheduler", "constant"),
|
|
823
|
+
max_lr=cfg["edit_budget"],
|
|
824
|
+
min_lr=cfg.get("min_edit_budget", 2),
|
|
825
|
+
total_steps=total_steps,
|
|
826
|
+
)
|
|
827
|
+
|
|
828
|
+
# Fixed training pool: base seeds (each seed = one deterministic batch)
|
|
829
|
+
if dataloader is not None:
|
|
830
|
+
base_seeds = dataloader.make_base_seeds(
|
|
831
|
+
steps_per_epoch=steps_per_epoch,
|
|
832
|
+
accumulation=accumulation,
|
|
833
|
+
seed=seed,
|
|
834
|
+
)
|
|
835
|
+
else:
|
|
836
|
+
base_seeds = [seed + i + 1 for i in range(batches_per_epoch)]
|
|
837
|
+
|
|
838
|
+
print(f"\n [config] epochs={num_epochs} steps/epoch={steps_per_epoch} "
|
|
839
|
+
f"(auto) accum={accumulation} batch_size={batch_size}")
|
|
840
|
+
print(f" [config] train_size={train_size}")
|
|
841
|
+
print(f" [config] batches/epoch={batches_per_epoch} "
|
|
842
|
+
f"total_steps={total_steps} "
|
|
843
|
+
f"games/epoch={train_pool_size}")
|
|
844
|
+
print(f" [config] lr_scheduler={cfg.get('lr_scheduler', 'constant')} "
|
|
845
|
+
f"edit_budget={cfg['edit_budget']} "
|
|
846
|
+
f"min_edit_budget={cfg.get('min_edit_budget', 2)}")
|
|
847
|
+
print(f" [config] skill_update_mode={update_mode} "
|
|
848
|
+
f"lr_control_mode={lr_control_mode} "
|
|
849
|
+
f"rewrite_reasoning_effort={rewrite_reasoning_effort or 'off'} "
|
|
850
|
+
f"rewrite_max_completion_tokens={rewrite_max_completion_tokens} "
|
|
851
|
+
f"max_analyst_rounds={max_analyst_rounds}")
|
|
852
|
+
print(f" [config] longitudinal_pair_policy={longitudinal_pair_policy}")
|
|
853
|
+
print(f" [config] base_seeds={base_seeds}")
|
|
854
|
+
|
|
855
|
+
# ── Resume check ─────────────────────────────────────────────────
|
|
856
|
+
history = _load_history(out_root)
|
|
857
|
+
runtime_state = _load_runtime_state(out_root)
|
|
858
|
+
if runtime_state:
|
|
859
|
+
last_step = int(runtime_state.get("last_completed_step", 0) or 0)
|
|
860
|
+
current_skill_path = runtime_state.get("current_skill_path") or os.path.join(
|
|
861
|
+
out_root, "skills", f"skill_v{last_step:04d}.md",
|
|
862
|
+
)
|
|
863
|
+
with open(current_skill_path) as f:
|
|
864
|
+
current_skill = f.read()
|
|
865
|
+
best_skill_path = runtime_state.get("best_skill_path") or os.path.join(
|
|
866
|
+
out_root, "best_skill.md",
|
|
867
|
+
)
|
|
868
|
+
if os.path.exists(best_skill_path):
|
|
869
|
+
with open(best_skill_path) as f:
|
|
870
|
+
best_skill = f.read()
|
|
871
|
+
else:
|
|
872
|
+
best_skill = current_skill
|
|
873
|
+
current_score = float(runtime_state.get("current_score", -1.0) or -1.0)
|
|
874
|
+
best_score = float(runtime_state.get("best_score", current_score) or current_score)
|
|
875
|
+
best_step = runtime_state.get("best_step", last_step)
|
|
876
|
+
current_origin = str(
|
|
877
|
+
runtime_state.get("current_origin")
|
|
878
|
+
or (f"step_{last_step:04d}" if last_step > 0 else "initial_skill")
|
|
879
|
+
)
|
|
880
|
+
best_origin = str(runtime_state.get("best_origin") or current_origin)
|
|
881
|
+
resume_from = last_step + 1
|
|
882
|
+
scheduler.load_state_dict({"current_step": last_step})
|
|
883
|
+
print(
|
|
884
|
+
f" [resume] from step {resume_from} "
|
|
885
|
+
f"current={current_score:.4f} best={best_score:.4f} "
|
|
886
|
+
f"(origin={current_origin})"
|
|
887
|
+
)
|
|
888
|
+
elif history:
|
|
889
|
+
last_step = history[-1]["step"]
|
|
890
|
+
current_skill = _load_skill(out_root, last_step)
|
|
891
|
+
best_rec = max(history, key=lambda h: h.get("best_score", 0.0))
|
|
892
|
+
best_score = best_rec["best_score"]
|
|
893
|
+
best_step = best_rec["best_step"]
|
|
894
|
+
best_skill_path = os.path.join(out_root, "best_skill.md")
|
|
895
|
+
if os.path.exists(best_skill_path):
|
|
896
|
+
with open(best_skill_path) as f:
|
|
897
|
+
best_skill = f.read()
|
|
898
|
+
else:
|
|
899
|
+
best_skill = _load_skill(out_root, best_step)
|
|
900
|
+
current_score = history[-1].get("current_score", best_score)
|
|
901
|
+
current_origin = f"step_{last_step:04d}"
|
|
902
|
+
best_origin = f"step_{int(best_step):04d}" if isinstance(best_step, int) else str(best_step)
|
|
903
|
+
resume_from = last_step + 1
|
|
904
|
+
scheduler.load_state_dict({"current_step": last_step})
|
|
905
|
+
print(
|
|
906
|
+
f" [resume] from step {resume_from} "
|
|
907
|
+
f"current={current_score:.4f} best={best_score:.4f}"
|
|
908
|
+
)
|
|
909
|
+
else:
|
|
910
|
+
current_skill = skill_init
|
|
911
|
+
best_skill = skill_init
|
|
912
|
+
best_score = -1.0
|
|
913
|
+
current_score = -1.0
|
|
914
|
+
best_step = 0
|
|
915
|
+
current_origin = "initial_skill"
|
|
916
|
+
best_origin = "initial_skill"
|
|
917
|
+
resume_from = 1
|
|
918
|
+
|
|
919
|
+
_save_skill(out_root, 0, skill_init)
|
|
920
|
+
|
|
921
|
+
use_skill_aware = cfg.get("use_skill_aware_reflection", False)
|
|
922
|
+
# Publish the toggle process-wide so run_minibatch_reflect resolves it
|
|
923
|
+
# from config for EVERY env adapter — no per-benchmark wiring needed.
|
|
924
|
+
configure_skill_aware_reflection(
|
|
925
|
+
use_skill_aware,
|
|
926
|
+
cfg.get("skill_aware_appendix_source", "both"),
|
|
927
|
+
)
|
|
928
|
+
if use_skill_aware:
|
|
929
|
+
current_skill = inject_empty_appendix_field(current_skill)
|
|
930
|
+
|
|
931
|
+
def _persist_runtime_state(last_completed_step: int) -> None:
|
|
932
|
+
_save_runtime_state(
|
|
933
|
+
out_root,
|
|
934
|
+
{
|
|
935
|
+
"last_completed_step": last_completed_step,
|
|
936
|
+
"current_skill_path": os.path.join(
|
|
937
|
+
out_root, "skills", f"skill_v{last_completed_step:04d}.md",
|
|
938
|
+
),
|
|
939
|
+
"current_score": current_score,
|
|
940
|
+
"current_origin": current_origin,
|
|
941
|
+
"best_skill_path": os.path.join(out_root, "best_skill.md"),
|
|
942
|
+
"best_score": best_score,
|
|
943
|
+
"best_step": best_step,
|
|
944
|
+
"best_origin": best_origin,
|
|
945
|
+
},
|
|
946
|
+
)
|
|
947
|
+
|
|
948
|
+
# ── Selection cache ──────────────────────────────────────────────
|
|
949
|
+
sel_cache: dict[str, tuple[float, float]] = {}
|
|
950
|
+
for rec in history:
|
|
951
|
+
sh = rec.get("candidate_hash", "")
|
|
952
|
+
if sh and rec.get("selection_hard") is not None:
|
|
953
|
+
sel_cache[sh] = (rec["selection_hard"], rec["selection_soft"])
|
|
954
|
+
|
|
955
|
+
# ── Baseline evaluation on selection set ─────────────────────────
|
|
956
|
+
# `use_gate=False` keeps validation running (selection rollout +
|
|
957
|
+
# scoring are unconditional below) but force-accepts every candidate
|
|
958
|
+
# instead of gating it; final skill is chosen manually afterwards.
|
|
959
|
+
use_gate = cfg.get("use_gate", True) is not False
|
|
960
|
+
gate_metric = str(cfg.get("gate_metric", "hard")).strip().lower()
|
|
961
|
+
if gate_metric not in {"hard", "soft", "mixed"}:
|
|
962
|
+
raise ValueError(
|
|
963
|
+
f"evaluation.gate_metric must be 'hard' | 'soft' | 'mixed', "
|
|
964
|
+
f"got {gate_metric!r}"
|
|
965
|
+
)
|
|
966
|
+
gate_mixed_weight = float(cfg.get("gate_mixed_weight", 0.5))
|
|
967
|
+
if not 0.0 <= gate_mixed_weight <= 1.0:
|
|
968
|
+
raise ValueError(
|
|
969
|
+
f"evaluation.gate_mixed_weight must be in [0, 1], "
|
|
970
|
+
f"got {gate_mixed_weight}"
|
|
971
|
+
)
|
|
972
|
+
print(
|
|
973
|
+
f" [gate] metric={gate_metric}"
|
|
974
|
+
+ (
|
|
975
|
+
f" mixed_weight={gate_mixed_weight}"
|
|
976
|
+
if gate_metric == "mixed"
|
|
977
|
+
else ""
|
|
978
|
+
)
|
|
979
|
+
+ ("" if use_gate
|
|
980
|
+
else " (DISABLED → validation runs, candidates force-accepted)")
|
|
981
|
+
)
|
|
982
|
+
slow_gate_with_selection = bool(
|
|
983
|
+
cfg.get("slow_update_gate_with_selection", False)
|
|
984
|
+
)
|
|
985
|
+
print(
|
|
986
|
+
" [slow update] acceptance="
|
|
987
|
+
+ ("gated (selection-set validation)"
|
|
988
|
+
if slow_gate_with_selection
|
|
989
|
+
else "force-accept (unconditional)")
|
|
990
|
+
)
|
|
991
|
+
if current_score < 0:
|
|
992
|
+
print(f"\n{'='*60}")
|
|
993
|
+
print(" BASELINE — evaluate initial skill on Selection set (valid_seen)")
|
|
994
|
+
print(f"{'='*60}")
|
|
995
|
+
sel_env, sel_n = _build_eval_env(
|
|
996
|
+
split="valid_seen",
|
|
997
|
+
env_num=cfg["sel_env_num"],
|
|
998
|
+
seed=seed,
|
|
999
|
+
)
|
|
1000
|
+
print(f" Selection items: {sel_n}")
|
|
1001
|
+
baseline_dir = os.path.join(out_root, "selection_eval_baseline")
|
|
1002
|
+
baseline_results = adapter.rollout(sel_env, skill_init, baseline_dir)
|
|
1003
|
+
baseline_hard, baseline_soft = compute_score(baseline_results)
|
|
1004
|
+
current_score = select_gate_score(
|
|
1005
|
+
baseline_hard, baseline_soft, gate_metric, gate_mixed_weight,
|
|
1006
|
+
)
|
|
1007
|
+
best_score = current_score
|
|
1008
|
+
sh = skill_hash(skill_init)
|
|
1009
|
+
sel_cache[sh] = (baseline_hard, baseline_soft)
|
|
1010
|
+
current_origin = "initial_skill"
|
|
1011
|
+
best_origin = "initial_skill"
|
|
1012
|
+
_persist_runtime_state(0)
|
|
1013
|
+
print(
|
|
1014
|
+
f" [baseline result] selection hard={baseline_hard:.4f} "
|
|
1015
|
+
f"soft={baseline_soft:.4f} "
|
|
1016
|
+
f"gate[{gate_metric}]={current_score:.4f}"
|
|
1017
|
+
)
|
|
1018
|
+
|
|
1019
|
+
# ── Training loop ────────────────────────────────────────────────
|
|
1020
|
+
t_loop_start = time.time()
|
|
1021
|
+
|
|
1022
|
+
if resume_from > total_steps:
|
|
1023
|
+
print(f"\n [skip] all {total_steps} steps complete — jumping to evaluation")
|
|
1024
|
+
|
|
1025
|
+
global_step = 0
|
|
1026
|
+
for epoch in range(1, num_epochs + 1):
|
|
1027
|
+
if dataloader is not None:
|
|
1028
|
+
epoch_batches = dataloader.plan_train_epoch(
|
|
1029
|
+
epoch=epoch,
|
|
1030
|
+
steps_per_epoch=steps_per_epoch,
|
|
1031
|
+
accumulation=accumulation,
|
|
1032
|
+
batch_size=batch_size,
|
|
1033
|
+
seed=seed,
|
|
1034
|
+
out_root=out_root,
|
|
1035
|
+
)
|
|
1036
|
+
shuffled_seeds = [batch.seed for batch in epoch_batches]
|
|
1037
|
+
else:
|
|
1038
|
+
epoch_batches = []
|
|
1039
|
+
epoch_rng = random.Random(seed + epoch * 1000)
|
|
1040
|
+
shuffled_seeds = base_seeds.copy()
|
|
1041
|
+
epoch_rng.shuffle(shuffled_seeds)
|
|
1042
|
+
|
|
1043
|
+
# Step buffer: accumulates per-step context (failure patterns +
|
|
1044
|
+
# rejected edits) within this epoch so optimizers see full history.
|
|
1045
|
+
step_buffer: list[dict] = []
|
|
1046
|
+
active_meta_skill = (
|
|
1047
|
+
_load_meta_skill_content(out_root, epoch - 1)
|
|
1048
|
+
if cfg.get("use_meta_skill", False)
|
|
1049
|
+
else ""
|
|
1050
|
+
)
|
|
1051
|
+
|
|
1052
|
+
print(
|
|
1053
|
+
f"\n [EPOCH {epoch}/{num_epochs}] "
|
|
1054
|
+
f"shuffled_seeds={shuffled_seeds}"
|
|
1055
|
+
)
|
|
1056
|
+
if active_meta_skill:
|
|
1057
|
+
print(
|
|
1058
|
+
f" [meta skill] loaded from epoch {epoch - 1} "
|
|
1059
|
+
f"({len(active_meta_skill)} chars)"
|
|
1060
|
+
)
|
|
1061
|
+
|
|
1062
|
+
for step_in_epoch in range(steps_per_epoch):
|
|
1063
|
+
global_step += 1
|
|
1064
|
+
if global_step < resume_from:
|
|
1065
|
+
continue
|
|
1066
|
+
|
|
1067
|
+
step_t0 = time.time()
|
|
1068
|
+
step_dir = os.path.join(out_root, "steps", f"step_{global_step:04d}")
|
|
1069
|
+
os.makedirs(step_dir, exist_ok=True)
|
|
1070
|
+
|
|
1071
|
+
tokens_before = get_token_summary()
|
|
1072
|
+
|
|
1073
|
+
print(
|
|
1074
|
+
f"\n [STEP {global_step}/{total_steps}] "
|
|
1075
|
+
f"epoch={epoch} step_in_epoch={step_in_epoch} "
|
|
1076
|
+
f"{'='*30}"
|
|
1077
|
+
)
|
|
1078
|
+
|
|
1079
|
+
step_rec: dict = {
|
|
1080
|
+
"step": global_step,
|
|
1081
|
+
"epoch": epoch,
|
|
1082
|
+
"step_in_epoch": step_in_epoch,
|
|
1083
|
+
"timing": {},
|
|
1084
|
+
"tokens": {},
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
# ── Accumulation: Rollout + Reflect ──────────────────────
|
|
1088
|
+
all_failure_patches: list[dict] = []
|
|
1089
|
+
all_success_patches: list[dict] = []
|
|
1090
|
+
all_raw_patches: list[dict | None] = []
|
|
1091
|
+
all_rollout_results: list[dict] = []
|
|
1092
|
+
accum_rollout_stats: list[dict] = []
|
|
1093
|
+
total_rollout_time = 0.0
|
|
1094
|
+
total_reflect_time = 0.0
|
|
1095
|
+
|
|
1096
|
+
for a in range(accumulation):
|
|
1097
|
+
batch_idx = step_in_epoch * accumulation + a
|
|
1098
|
+
if dataloader is not None:
|
|
1099
|
+
batch_spec = epoch_batches[batch_idx]
|
|
1100
|
+
train_env, train_n, batch_seed = _build_train_env(batch_spec)
|
|
1101
|
+
else:
|
|
1102
|
+
batch_seed = shuffled_seeds[batch_idx]
|
|
1103
|
+
train_env = adapter.build_train_env(
|
|
1104
|
+
batch_size=batch_size,
|
|
1105
|
+
seed=batch_seed,
|
|
1106
|
+
out_root=out_root,
|
|
1107
|
+
)
|
|
1108
|
+
train_n = len(train_env) if hasattr(train_env, "__len__") else batch_size
|
|
1109
|
+
|
|
1110
|
+
# Directory routing
|
|
1111
|
+
if accumulation > 1:
|
|
1112
|
+
batch_dir = os.path.join(step_dir, f"batch_{a}")
|
|
1113
|
+
else:
|
|
1114
|
+
batch_dir = step_dir
|
|
1115
|
+
|
|
1116
|
+
rollout_dir = os.path.join(batch_dir, "rollout")
|
|
1117
|
+
patches_dir = os.path.join(batch_dir, "patches")
|
|
1118
|
+
|
|
1119
|
+
# ① ROLLOUT ────────────────────────────────────────────
|
|
1120
|
+
t_phase = time.time()
|
|
1121
|
+
print(f" [1/6 ROLLOUT] train items={train_n} (from pool, batch_seed={batch_seed})")
|
|
1122
|
+
rollout_results = adapter.rollout(
|
|
1123
|
+
train_env, current_skill, rollout_dir,
|
|
1124
|
+
use_eval_feedback=True,
|
|
1125
|
+
)
|
|
1126
|
+
r_hard, r_soft = compute_score(rollout_results)
|
|
1127
|
+
total_rollout_time += time.time() - t_phase
|
|
1128
|
+
all_rollout_results.extend(rollout_results)
|
|
1129
|
+
print(f" [1/6 done] hard={r_hard:.4f} soft={r_soft:.4f}")
|
|
1130
|
+
|
|
1131
|
+
# ② REFLECT ────────────────────────────────────────────
|
|
1132
|
+
t_phase = time.time()
|
|
1133
|
+
pred_dir = os.path.join(rollout_dir, "predictions")
|
|
1134
|
+
|
|
1135
|
+
# Build step context from buffer
|
|
1136
|
+
step_buffer_context = _format_step_buffer(step_buffer)
|
|
1137
|
+
|
|
1138
|
+
raw_patches = adapter.reflect(
|
|
1139
|
+
rollout_results, current_skill, batch_dir,
|
|
1140
|
+
prediction_dir=pred_dir, patches_dir=patches_dir,
|
|
1141
|
+
random_seed=batch_seed,
|
|
1142
|
+
step_buffer_context=step_buffer_context,
|
|
1143
|
+
meta_skill_context=active_meta_skill,
|
|
1144
|
+
)
|
|
1145
|
+
failure_patches, success_patches = _normalise_patches(
|
|
1146
|
+
raw_patches,
|
|
1147
|
+
update_mode=update_mode,
|
|
1148
|
+
)
|
|
1149
|
+
all_failure_patches.extend(failure_patches)
|
|
1150
|
+
all_success_patches.extend(success_patches)
|
|
1151
|
+
all_raw_patches.extend(raw_patches)
|
|
1152
|
+
total_reflect_time += time.time() - t_phase
|
|
1153
|
+
|
|
1154
|
+
print(
|
|
1155
|
+
f" [2/6 done] failure_patches={len(failure_patches)} "
|
|
1156
|
+
f"success_patches={len(success_patches)}"
|
|
1157
|
+
)
|
|
1158
|
+
|
|
1159
|
+
# Track per-batch stats
|
|
1160
|
+
accum_rollout_stats.append({
|
|
1161
|
+
"batch_idx": a,
|
|
1162
|
+
"batch_seed": batch_seed,
|
|
1163
|
+
"n_envs": len(rollout_results),
|
|
1164
|
+
"hard": r_hard,
|
|
1165
|
+
"soft": r_soft,
|
|
1166
|
+
"n_failure_patches": len(failure_patches),
|
|
1167
|
+
"n_success_patches": len(success_patches),
|
|
1168
|
+
})
|
|
1169
|
+
|
|
1170
|
+
# ── End of accumulation loop ─────────────────────────────
|
|
1171
|
+
|
|
1172
|
+
# Aggregate rollout stats across batches
|
|
1173
|
+
total_n = sum(b["n_envs"] for b in accum_rollout_stats)
|
|
1174
|
+
agg_hard = sum(b["hard"] * b["n_envs"] for b in accum_rollout_stats) / max(total_n, 1)
|
|
1175
|
+
agg_soft = sum(b["soft"] * b["n_envs"] for b in accum_rollout_stats) / max(total_n, 1)
|
|
1176
|
+
|
|
1177
|
+
step_rec["rollout_hard"] = round(agg_hard, 6)
|
|
1178
|
+
step_rec["rollout_soft"] = round(agg_soft, 6)
|
|
1179
|
+
step_rec["rollout_n"] = total_n
|
|
1180
|
+
step_rec["accumulation_batches"] = accum_rollout_stats
|
|
1181
|
+
step_rec["timing"]["rollout_s"] = round(total_rollout_time, 1)
|
|
1182
|
+
step_rec["timing"]["reflect_s"] = round(total_reflect_time, 1)
|
|
1183
|
+
|
|
1184
|
+
n_total_patches = len(all_failure_patches) + len(all_success_patches)
|
|
1185
|
+
step_rec["n_patches"] = n_total_patches
|
|
1186
|
+
step_rec["n_failure_patches"] = len(all_failure_patches)
|
|
1187
|
+
step_rec["n_success_patches"] = len(all_success_patches)
|
|
1188
|
+
|
|
1189
|
+
if accumulation > 1:
|
|
1190
|
+
print(
|
|
1191
|
+
f" [accum done] total: failure={len(all_failure_patches)} "
|
|
1192
|
+
f"success={len(all_success_patches)} "
|
|
1193
|
+
f"from {accumulation} batches"
|
|
1194
|
+
)
|
|
1195
|
+
|
|
1196
|
+
# ── No patches? Skip ─────────────────────────────────────
|
|
1197
|
+
if not all_failure_patches and not all_success_patches:
|
|
1198
|
+
# Skill-aware: a lapse-only step has no body patches but
|
|
1199
|
+
# may still carry appendix notes — flush them BEFORE
|
|
1200
|
+
# skipping, or they would be silently dropped.
|
|
1201
|
+
if use_skill_aware:
|
|
1202
|
+
current_skill = _flush_skill_aware_appendix(
|
|
1203
|
+
current_skill, all_raw_patches, step_rec, step_dir, cfg,
|
|
1204
|
+
)
|
|
1205
|
+
step_rec["action"] = "skip_no_patches"
|
|
1206
|
+
step_rec["current_score"] = current_score
|
|
1207
|
+
step_rec["best_score"] = best_score
|
|
1208
|
+
step_rec["best_step"] = best_step
|
|
1209
|
+
step_rec["skill_len"] = len(current_skill)
|
|
1210
|
+
step_rec["wall_time_s"] = round(time.time() - step_t0, 1)
|
|
1211
|
+
history.append(step_rec)
|
|
1212
|
+
_save_history(out_root, history)
|
|
1213
|
+
_save_skill(out_root, global_step, current_skill)
|
|
1214
|
+
_persist_runtime_state(global_step)
|
|
1215
|
+
with open(os.path.join(step_dir, "step_record.json"), "w") as f:
|
|
1216
|
+
json.dump(step_rec, f, indent=2, ensure_ascii=False)
|
|
1217
|
+
print(" [skip] no usable patches — skill unchanged")
|
|
1218
|
+
continue
|
|
1219
|
+
|
|
1220
|
+
# ③ AGGREGATE ──────────────────────────────────────────────
|
|
1221
|
+
t_phase = time.time()
|
|
1222
|
+
merged_patch = merge_patches(
|
|
1223
|
+
current_skill, all_failure_patches, all_success_patches,
|
|
1224
|
+
batch_size=merge_bs, verbose=True,
|
|
1225
|
+
workers=cfg["analyst_workers"],
|
|
1226
|
+
update_mode=update_mode,
|
|
1227
|
+
meta_skill_context=active_meta_skill,
|
|
1228
|
+
)
|
|
1229
|
+
with open(os.path.join(step_dir, "merged_patch.json"), "w") as f:
|
|
1230
|
+
json.dump(merged_patch, f, ensure_ascii=False, indent=2)
|
|
1231
|
+
|
|
1232
|
+
merged_items = get_payload_items(merged_patch, update_mode)
|
|
1233
|
+
n_edits_merged = len(merged_items)
|
|
1234
|
+
step_rec["n_edits_merged"] = n_edits_merged
|
|
1235
|
+
step_rec["timing"]["aggregate_s"] = round(time.time() - t_phase, 1)
|
|
1236
|
+
print(f" [3/6 done] merged {n_edits_merged} {payload_label(update_mode)}")
|
|
1237
|
+
|
|
1238
|
+
# ④ SELECT ─────────────────────────────────────────────────
|
|
1239
|
+
t_phase = time.time()
|
|
1240
|
+
lr_decision = None
|
|
1241
|
+
if is_full_rewrite_minibatch_mode(update_mode):
|
|
1242
|
+
edit_budget = None
|
|
1243
|
+
ranked_patch = merged_patch
|
|
1244
|
+
ranked_items = merged_items
|
|
1245
|
+
n_edits_ranked = len(ranked_items)
|
|
1246
|
+
step_rec["n_edits_ranked"] = n_edits_ranked
|
|
1247
|
+
step_rec["edit_budget"] = None
|
|
1248
|
+
step_rec["lr_control_mode"] = "none"
|
|
1249
|
+
with open(os.path.join(step_dir, "ranked_edits.json"), "w") as f:
|
|
1250
|
+
json.dump(ranked_patch, f, ensure_ascii=False, indent=2)
|
|
1251
|
+
else:
|
|
1252
|
+
if lr_control_mode == "autonomous":
|
|
1253
|
+
lr_decision = decide_autonomous_learning_rate(
|
|
1254
|
+
skill_content=current_skill,
|
|
1255
|
+
merged_patch=merged_patch,
|
|
1256
|
+
update_mode=update_mode,
|
|
1257
|
+
rollout_hard=agg_hard,
|
|
1258
|
+
rollout_soft=agg_soft,
|
|
1259
|
+
rollout_n=total_n,
|
|
1260
|
+
step_buffer_context=step_buffer_context,
|
|
1261
|
+
meta_skill_context=active_meta_skill,
|
|
1262
|
+
)
|
|
1263
|
+
edit_budget = int(lr_decision["learning_rate"])
|
|
1264
|
+
with open(os.path.join(step_dir, "lr_decision.json"), "w") as f:
|
|
1265
|
+
json.dump(lr_decision, f, ensure_ascii=False, indent=2)
|
|
1266
|
+
with open(os.path.join(out_root, "lr_history.jsonl"), "a") as f:
|
|
1267
|
+
f.write(json.dumps({
|
|
1268
|
+
"step": global_step,
|
|
1269
|
+
"epoch": epoch,
|
|
1270
|
+
**lr_decision,
|
|
1271
|
+
}, ensure_ascii=False) + "\n")
|
|
1272
|
+
else:
|
|
1273
|
+
edit_budget = scheduler.step()
|
|
1274
|
+
ranked_patch = rank_and_select(
|
|
1275
|
+
current_skill, merged_patch,
|
|
1276
|
+
max_edits=edit_budget,
|
|
1277
|
+
update_mode=update_mode,
|
|
1278
|
+
meta_skill_context=active_meta_skill,
|
|
1279
|
+
)
|
|
1280
|
+
with open(os.path.join(step_dir, "ranked_edits.json"), "w") as f:
|
|
1281
|
+
json.dump(ranked_patch, f, ensure_ascii=False, indent=2)
|
|
1282
|
+
|
|
1283
|
+
ranked_items = get_payload_items(ranked_patch, update_mode)
|
|
1284
|
+
n_edits_ranked = len(ranked_items)
|
|
1285
|
+
step_rec["n_edits_ranked"] = n_edits_ranked
|
|
1286
|
+
step_rec["edit_budget"] = edit_budget
|
|
1287
|
+
step_rec["lr_control_mode"] = lr_control_mode
|
|
1288
|
+
if lr_decision is not None:
|
|
1289
|
+
step_rec["lr_decision"] = lr_decision
|
|
1290
|
+
step_rec["timing"]["select_s"] = round(time.time() - t_phase, 1)
|
|
1291
|
+
|
|
1292
|
+
support_counts = [
|
|
1293
|
+
item.get("support_count", 0) for item in ranked_items if isinstance(item, dict)
|
|
1294
|
+
]
|
|
1295
|
+
step_rec["support_counts"] = support_counts
|
|
1296
|
+
if is_full_rewrite_minibatch_mode(update_mode):
|
|
1297
|
+
print(
|
|
1298
|
+
f" [4/6 SELECT] skipped LR/select; "
|
|
1299
|
+
f"using {n_edits_ranked} merged {payload_label(update_mode)}"
|
|
1300
|
+
)
|
|
1301
|
+
else:
|
|
1302
|
+
print(
|
|
1303
|
+
f" [4/6 SELECT] "
|
|
1304
|
+
f"{n_edits_merged} -> {n_edits_ranked} {payload_label(update_mode)} "
|
|
1305
|
+
f"(budget={edit_budget}, lr_control={lr_control_mode})"
|
|
1306
|
+
)
|
|
1307
|
+
|
|
1308
|
+
# ⑤ UPDATE ─────────────────────────────────────────────────
|
|
1309
|
+
t_phase = time.time()
|
|
1310
|
+
rewrite_result = None
|
|
1311
|
+
if update_mode == "rewrite_from_suggestions":
|
|
1312
|
+
rewrite_result = rewrite_skill_from_suggestions(
|
|
1313
|
+
current_skill,
|
|
1314
|
+
ranked_patch,
|
|
1315
|
+
step_buffer_context=step_buffer_context,
|
|
1316
|
+
env=cfg.get("env"),
|
|
1317
|
+
reasoning_effort=rewrite_reasoning_effort,
|
|
1318
|
+
max_completion_tokens=rewrite_max_completion_tokens,
|
|
1319
|
+
)
|
|
1320
|
+
if rewrite_result and rewrite_result.get("new_skill"):
|
|
1321
|
+
candidate_skill = rewrite_result["new_skill"]
|
|
1322
|
+
apply_report = []
|
|
1323
|
+
with open(os.path.join(step_dir, "rewrite_result.json"), "w") as f:
|
|
1324
|
+
json.dump(rewrite_result, f, ensure_ascii=False, indent=2)
|
|
1325
|
+
else:
|
|
1326
|
+
candidate_skill = current_skill
|
|
1327
|
+
apply_report = []
|
|
1328
|
+
elif is_full_rewrite_minibatch_mode(update_mode):
|
|
1329
|
+
skill_candidates = get_payload_items(ranked_patch, update_mode)
|
|
1330
|
+
selected_candidate = next(
|
|
1331
|
+
(
|
|
1332
|
+
item for item in skill_candidates
|
|
1333
|
+
if isinstance(item, dict) and str(item.get("new_skill", "")).strip()
|
|
1334
|
+
),
|
|
1335
|
+
None,
|
|
1336
|
+
)
|
|
1337
|
+
if selected_candidate:
|
|
1338
|
+
candidate_skill = str(selected_candidate["new_skill"]).rstrip() + "\n"
|
|
1339
|
+
apply_report = []
|
|
1340
|
+
rewrite_result = {
|
|
1341
|
+
"reasoning": ranked_patch.get("reasoning", ""),
|
|
1342
|
+
"change_summary": selected_candidate.get("change_summary", []),
|
|
1343
|
+
"title": selected_candidate.get("title", ""),
|
|
1344
|
+
"source_type": selected_candidate.get("source_type", ""),
|
|
1345
|
+
}
|
|
1346
|
+
with open(os.path.join(step_dir, "full_rewrite_result.json"), "w") as f:
|
|
1347
|
+
json.dump(
|
|
1348
|
+
{
|
|
1349
|
+
"selected_candidate": selected_candidate,
|
|
1350
|
+
"merged_patch": ranked_patch,
|
|
1351
|
+
},
|
|
1352
|
+
f,
|
|
1353
|
+
ensure_ascii=False,
|
|
1354
|
+
indent=2,
|
|
1355
|
+
)
|
|
1356
|
+
else:
|
|
1357
|
+
candidate_skill = current_skill
|
|
1358
|
+
apply_report = []
|
|
1359
|
+
else:
|
|
1360
|
+
candidate_skill, apply_report = apply_patch_with_report(current_skill, ranked_patch)
|
|
1361
|
+
with open(os.path.join(step_dir, "candidate_skill.md"), "w") as f:
|
|
1362
|
+
f.write(candidate_skill)
|
|
1363
|
+
if apply_report:
|
|
1364
|
+
with open(os.path.join(step_dir, "edit_apply_report.json"), "w") as f:
|
|
1365
|
+
json.dump(apply_report, f, indent=2, ensure_ascii=False)
|
|
1366
|
+
|
|
1367
|
+
cand_hash = skill_hash(candidate_skill)
|
|
1368
|
+
step_rec["candidate_hash"] = cand_hash
|
|
1369
|
+
step_rec["candidate_skill_len"] = len(candidate_skill)
|
|
1370
|
+
if rewrite_result:
|
|
1371
|
+
step_rec["rewrite_change_summary"] = rewrite_result.get("change_summary", [])
|
|
1372
|
+
if apply_report:
|
|
1373
|
+
step_rec["edit_apply_summary"] = {
|
|
1374
|
+
"total": len(apply_report),
|
|
1375
|
+
"applied": sum(
|
|
1376
|
+
1 for row in apply_report if str(row.get("status", "")).startswith("applied")
|
|
1377
|
+
),
|
|
1378
|
+
"skipped": sum(
|
|
1379
|
+
1 for row in apply_report if str(row.get("status", "")).startswith("skipped")
|
|
1380
|
+
),
|
|
1381
|
+
"errors": sum(
|
|
1382
|
+
1 for row in apply_report if row.get("status") == "error"
|
|
1383
|
+
),
|
|
1384
|
+
}
|
|
1385
|
+
step_rec["timing"]["update_s"] = round(time.time() - t_phase, 1)
|
|
1386
|
+
if (
|
|
1387
|
+
update_mode == "rewrite_from_suggestions"
|
|
1388
|
+
and rewrite_result is None
|
|
1389
|
+
) or (
|
|
1390
|
+
is_full_rewrite_minibatch_mode(update_mode)
|
|
1391
|
+
and rewrite_result is None
|
|
1392
|
+
):
|
|
1393
|
+
# Skill-aware: flush appendix notes before skipping (see
|
|
1394
|
+
# the skip_no_patches branch above).
|
|
1395
|
+
if use_skill_aware:
|
|
1396
|
+
current_skill = _flush_skill_aware_appendix(
|
|
1397
|
+
current_skill, all_raw_patches, step_rec, step_dir, cfg,
|
|
1398
|
+
)
|
|
1399
|
+
step_rec["action"] = "skip_no_rewrite"
|
|
1400
|
+
step_rec["current_score"] = current_score
|
|
1401
|
+
step_rec["best_score"] = best_score
|
|
1402
|
+
step_rec["best_step"] = best_step
|
|
1403
|
+
step_rec["skill_len"] = len(current_skill)
|
|
1404
|
+
step_rec["wall_time_s"] = round(time.time() - step_t0, 1)
|
|
1405
|
+
history.append(step_rec)
|
|
1406
|
+
_save_history(out_root, history)
|
|
1407
|
+
_save_skill(out_root, global_step, current_skill)
|
|
1408
|
+
_persist_runtime_state(global_step)
|
|
1409
|
+
with open(os.path.join(step_dir, "step_record.json"), "w") as f:
|
|
1410
|
+
json.dump(step_rec, f, indent=2, ensure_ascii=False)
|
|
1411
|
+
print(" [skip] no usable rewrite generated — skill unchanged")
|
|
1412
|
+
continue
|
|
1413
|
+
print(
|
|
1414
|
+
f" [5/6 UPDATE] "
|
|
1415
|
+
f"skill_len {len(current_skill)} -> {len(candidate_skill)}"
|
|
1416
|
+
)
|
|
1417
|
+
|
|
1418
|
+
# ⑥ EVALUATE ───────────────────────────────────────────────
|
|
1419
|
+
t_phase = time.time()
|
|
1420
|
+
if cand_hash in sel_cache:
|
|
1421
|
+
cand_hard, cand_soft = sel_cache[cand_hash]
|
|
1422
|
+
print(
|
|
1423
|
+
f" [6/6 EVALUATE] "
|
|
1424
|
+
f"cache hit {cand_hash}: hard={cand_hard:.4f}"
|
|
1425
|
+
)
|
|
1426
|
+
else:
|
|
1427
|
+
sel_env, sel_n = _build_eval_env(
|
|
1428
|
+
split="valid_seen",
|
|
1429
|
+
env_num=cfg["sel_env_num"],
|
|
1430
|
+
seed=seed,
|
|
1431
|
+
)
|
|
1432
|
+
print(f" [6/6 EVALUATE] selection items={sel_n}")
|
|
1433
|
+
sel_eval_dir = os.path.join(step_dir, "selection_eval")
|
|
1434
|
+
sel_results = adapter.rollout(sel_env, candidate_skill, sel_eval_dir)
|
|
1435
|
+
cand_hard, cand_soft = compute_score(sel_results)
|
|
1436
|
+
sel_cache[cand_hash] = (cand_hard, cand_soft)
|
|
1437
|
+
|
|
1438
|
+
step_rec["selection_hard"] = cand_hard
|
|
1439
|
+
step_rec["selection_soft"] = cand_soft
|
|
1440
|
+
|
|
1441
|
+
gate = evaluate_gate(
|
|
1442
|
+
candidate_skill=candidate_skill,
|
|
1443
|
+
cand_hard=cand_hard,
|
|
1444
|
+
current_skill=current_skill,
|
|
1445
|
+
current_score=current_score,
|
|
1446
|
+
best_skill=best_skill,
|
|
1447
|
+
best_score=best_score,
|
|
1448
|
+
best_step=best_step,
|
|
1449
|
+
global_step=global_step,
|
|
1450
|
+
cand_soft=cand_soft,
|
|
1451
|
+
metric=gate_metric,
|
|
1452
|
+
mixed_weight=gate_mixed_weight,
|
|
1453
|
+
) if use_gate else None
|
|
1454
|
+
cand_gate_score = select_gate_score(
|
|
1455
|
+
cand_hard, cand_soft, gate_metric, gate_mixed_weight,
|
|
1456
|
+
)
|
|
1457
|
+
if not use_gate:
|
|
1458
|
+
# Validation ran (scores recorded above) but the gate is
|
|
1459
|
+
# disabled: force-accept the candidate as the new current
|
|
1460
|
+
# skill. Best-so-far is still tracked for convenience; the
|
|
1461
|
+
# final skill is selected manually from the trajectory.
|
|
1462
|
+
if cand_gate_score > best_score:
|
|
1463
|
+
fa_best_skill = candidate_skill
|
|
1464
|
+
fa_best_score = cand_gate_score
|
|
1465
|
+
fa_best_step = global_step
|
|
1466
|
+
else:
|
|
1467
|
+
fa_best_skill = best_skill
|
|
1468
|
+
fa_best_score = best_score
|
|
1469
|
+
fa_best_step = best_step
|
|
1470
|
+
gate = GateResult(
|
|
1471
|
+
action="force_accept",
|
|
1472
|
+
current_skill=candidate_skill,
|
|
1473
|
+
current_score=cand_gate_score,
|
|
1474
|
+
best_skill=fa_best_skill,
|
|
1475
|
+
best_score=fa_best_score,
|
|
1476
|
+
best_step=fa_best_step,
|
|
1477
|
+
)
|
|
1478
|
+
step_rec["gate_metric"] = gate_metric
|
|
1479
|
+
step_rec["candidate_gate_score"] = cand_gate_score
|
|
1480
|
+
step_rec["action"] = gate.action
|
|
1481
|
+
prev_current = current_score
|
|
1482
|
+
prev_best = best_score
|
|
1483
|
+
current_skill = gate.current_skill
|
|
1484
|
+
current_score = gate.current_score
|
|
1485
|
+
best_skill = gate.best_skill
|
|
1486
|
+
best_score = gate.best_score
|
|
1487
|
+
best_step = gate.best_step
|
|
1488
|
+
if gate.action in {"accept", "accept_new_best", "force_accept"}:
|
|
1489
|
+
current_origin = f"step_{global_step:04d}"
|
|
1490
|
+
if gate.action == "accept_new_best" or (
|
|
1491
|
+
gate.action == "force_accept" and best_step == global_step
|
|
1492
|
+
):
|
|
1493
|
+
best_origin = current_origin
|
|
1494
|
+
|
|
1495
|
+
if use_skill_aware:
|
|
1496
|
+
current_skill = _flush_skill_aware_appendix(
|
|
1497
|
+
current_skill, all_raw_patches, step_rec, step_dir, cfg,
|
|
1498
|
+
)
|
|
1499
|
+
|
|
1500
|
+
if gate_metric == "hard":
|
|
1501
|
+
score_label = f"hard={cand_hard:.4f}"
|
|
1502
|
+
elif gate_metric == "soft":
|
|
1503
|
+
score_label = f"soft={cand_soft:.4f}"
|
|
1504
|
+
else:
|
|
1505
|
+
score_label = (
|
|
1506
|
+
f"mixed[w={gate_mixed_weight}]={cand_gate_score:.4f} "
|
|
1507
|
+
f"(hard={cand_hard:.4f} soft={cand_soft:.4f})"
|
|
1508
|
+
)
|
|
1509
|
+
if gate.action == "accept_new_best":
|
|
1510
|
+
print(
|
|
1511
|
+
f" [6/6 EVALUATE] ACCEPT (new best) "
|
|
1512
|
+
f"{score_label} > prev best {prev_best:.4f}"
|
|
1513
|
+
)
|
|
1514
|
+
elif gate.action == "accept":
|
|
1515
|
+
print(
|
|
1516
|
+
f" [6/6 EVALUATE] ACCEPT "
|
|
1517
|
+
f"{score_label} > current={prev_current:.4f}"
|
|
1518
|
+
)
|
|
1519
|
+
elif gate.action == "force_accept":
|
|
1520
|
+
print(
|
|
1521
|
+
f" [6/6 EVALUATE] FORCE-ACCEPT (gate disabled) "
|
|
1522
|
+
f"{score_label}"
|
|
1523
|
+
)
|
|
1524
|
+
else:
|
|
1525
|
+
print(
|
|
1526
|
+
f" [6/6 EVALUATE] REJECT "
|
|
1527
|
+
f"{score_label} <= current={current_score:.4f}"
|
|
1528
|
+
)
|
|
1529
|
+
|
|
1530
|
+
step_rec["timing"]["evaluate_s"] = round(time.time() - t_phase, 1)
|
|
1531
|
+
|
|
1532
|
+
# ── Step buffer: unified failure patterns + rejected edits ─
|
|
1533
|
+
action = step_rec.get("action", "unknown")
|
|
1534
|
+
n_total = len(all_rollout_results) or 1
|
|
1535
|
+
n_fail = sum(1 for r in all_rollout_results if not r.get("hard") or float(r.get("hard", 0)) < 1e-9)
|
|
1536
|
+
failure_patterns = _extract_failure_patterns(
|
|
1537
|
+
all_rollout_results, step_dir,
|
|
1538
|
+
)
|
|
1539
|
+
|
|
1540
|
+
buf_entry: dict = {
|
|
1541
|
+
"step": global_step,
|
|
1542
|
+
"action": action,
|
|
1543
|
+
"n_total": n_total,
|
|
1544
|
+
"n_fail": n_fail,
|
|
1545
|
+
"failure_patterns": failure_patterns,
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
# Attach rejected edits when the step was rejected
|
|
1549
|
+
if "reject" in action and ranked_patch:
|
|
1550
|
+
rejected_edits = [
|
|
1551
|
+
short_item_summary(item, update_mode)
|
|
1552
|
+
for item in ranked_items
|
|
1553
|
+
if isinstance(item, dict)
|
|
1554
|
+
]
|
|
1555
|
+
buf_entry["score_before"] = current_score
|
|
1556
|
+
buf_entry["score_after"] = cand_gate_score
|
|
1557
|
+
buf_entry["rejected_edits"] = rejected_edits
|
|
1558
|
+
|
|
1559
|
+
step_buffer.append(buf_entry)
|
|
1560
|
+
|
|
1561
|
+
# Persist step digest for step buffer context
|
|
1562
|
+
digest_path = os.path.join(step_dir, "trajectory_digest.json")
|
|
1563
|
+
with open(digest_path, "w") as f:
|
|
1564
|
+
json.dump(buf_entry, f, indent=2, ensure_ascii=False)
|
|
1565
|
+
|
|
1566
|
+
# ── Token snapshot ───────────────────────────────────────
|
|
1567
|
+
tokens_after = get_token_summary()
|
|
1568
|
+
step_tokens: dict = {}
|
|
1569
|
+
for stage in tokens_after:
|
|
1570
|
+
if stage == "_total":
|
|
1571
|
+
continue
|
|
1572
|
+
after = tokens_after[stage]
|
|
1573
|
+
before = tokens_before.get(stage, {})
|
|
1574
|
+
step_tokens[stage] = {
|
|
1575
|
+
"calls": after.get("calls", 0) - before.get("calls", 0),
|
|
1576
|
+
"prompt_tokens": after.get("prompt_tokens", 0)
|
|
1577
|
+
- before.get("prompt_tokens", 0),
|
|
1578
|
+
"completion_tokens": after.get("completion_tokens", 0)
|
|
1579
|
+
- before.get("completion_tokens", 0),
|
|
1580
|
+
}
|
|
1581
|
+
step_rec["tokens"] = step_tokens
|
|
1582
|
+
|
|
1583
|
+
# ── Save state ───────────────────────────────────────────
|
|
1584
|
+
step_rec["current_score"] = current_score
|
|
1585
|
+
step_rec["best_score"] = best_score
|
|
1586
|
+
step_rec["best_step"] = best_step
|
|
1587
|
+
step_rec["current_origin"] = current_origin
|
|
1588
|
+
step_rec["best_origin"] = best_origin
|
|
1589
|
+
step_rec["skill_len"] = len(current_skill)
|
|
1590
|
+
step_rec["wall_time_s"] = round(time.time() - step_t0, 1)
|
|
1591
|
+
|
|
1592
|
+
_save_skill(out_root, global_step, current_skill)
|
|
1593
|
+
with open(os.path.join(out_root, "best_skill.md"), "w") as f:
|
|
1594
|
+
f.write(best_skill)
|
|
1595
|
+
history.append(step_rec)
|
|
1596
|
+
_save_history(out_root, history)
|
|
1597
|
+
_persist_runtime_state(global_step)
|
|
1598
|
+
with open(os.path.join(step_dir, "step_record.json"), "w") as f:
|
|
1599
|
+
json.dump(step_rec, f, indent=2, ensure_ascii=False)
|
|
1600
|
+
|
|
1601
|
+
timing = step_rec["timing"]
|
|
1602
|
+
print(
|
|
1603
|
+
f"\n [STEP {global_step} done] "
|
|
1604
|
+
f"epoch={epoch} action={step_rec['action']} "
|
|
1605
|
+
f"current={current_score:.4f} best={best_score:.4f} "
|
|
1606
|
+
f"dt={step_rec['wall_time_s']}s\n"
|
|
1607
|
+
f" timing: rollout={timing.get('rollout_s',0)}s "
|
|
1608
|
+
f"reflect={timing.get('reflect_s',0)}s "
|
|
1609
|
+
f"aggregate={timing.get('aggregate_s',0)}s "
|
|
1610
|
+
f"select={timing.get('select_s',0)}s "
|
|
1611
|
+
f"evaluate={timing.get('evaluate_s',0)}s"
|
|
1612
|
+
)
|
|
1613
|
+
|
|
1614
|
+
epoch_last_step_skill = current_skill
|
|
1615
|
+
epoch_comparison_pairs: list[dict] | None = None
|
|
1616
|
+
|
|
1617
|
+
# ── SLOW UPDATE (end of epoch) ──────────────────────────────
|
|
1618
|
+
use_slow = cfg.get("use_slow_update", False)
|
|
1619
|
+
if use_slow:
|
|
1620
|
+
slow_dir = os.path.join(out_root, "slow_update", f"epoch_{epoch:02d}")
|
|
1621
|
+
slow_done_path = os.path.join(slow_dir, "slow_result.json")
|
|
1622
|
+
|
|
1623
|
+
if os.path.exists(slow_done_path):
|
|
1624
|
+
# Resume support
|
|
1625
|
+
print(
|
|
1626
|
+
f"\n [SLOW UPDATE epoch {epoch}] "
|
|
1627
|
+
f"resumed — already done"
|
|
1628
|
+
)
|
|
1629
|
+
with open(slow_done_path) as f:
|
|
1630
|
+
slow_saved = json.load(f)
|
|
1631
|
+
comparison_path = os.path.join(slow_dir, "comparison_pairs.json")
|
|
1632
|
+
if os.path.exists(comparison_path):
|
|
1633
|
+
try:
|
|
1634
|
+
with open(comparison_path) as f:
|
|
1635
|
+
epoch_comparison_pairs = json.load(f)
|
|
1636
|
+
except Exception:
|
|
1637
|
+
epoch_comparison_pairs = None
|
|
1638
|
+
if (
|
|
1639
|
+
slow_saved.get("slow_update_content")
|
|
1640
|
+
and epoch >= 2
|
|
1641
|
+
):
|
|
1642
|
+
action = slow_saved.get("action")
|
|
1643
|
+
if slow_gate_with_selection:
|
|
1644
|
+
# Gated mode (follow SkillReflection): re-apply the
|
|
1645
|
+
# guidance to current_skill only when it was accepted.
|
|
1646
|
+
if action in {"accept", "accept_new_best"}:
|
|
1647
|
+
current_skill = replace_slow_update_field(
|
|
1648
|
+
current_skill,
|
|
1649
|
+
slow_saved["slow_update_content"],
|
|
1650
|
+
)
|
|
1651
|
+
elif action in {
|
|
1652
|
+
"accept", "accept_new_best", "force_accept",
|
|
1653
|
+
}:
|
|
1654
|
+
# Force-accept mode: re-apply guidance to
|
|
1655
|
+
# current_skill only. best_skill must remain a
|
|
1656
|
+
# faithful snapshot of the val-best step and must
|
|
1657
|
+
# NOT receive force-injected slow-update content.
|
|
1658
|
+
current_skill = replace_slow_update_field(
|
|
1659
|
+
current_skill, slow_saved["slow_update_content"],
|
|
1660
|
+
)
|
|
1661
|
+
elif epoch == 1:
|
|
1662
|
+
# Epoch 1: inject empty placeholder
|
|
1663
|
+
os.makedirs(slow_dir, exist_ok=True)
|
|
1664
|
+
current_skill = inject_empty_slow_update_field(current_skill)
|
|
1665
|
+
current_origin = f"slow_update_placeholder_epoch_{epoch:02d}"
|
|
1666
|
+
_save_skill(out_root, global_step, current_skill)
|
|
1667
|
+
with open(os.path.join(out_root, "best_skill.md"), "w") as f:
|
|
1668
|
+
f.write(best_skill)
|
|
1669
|
+
with open(slow_done_path, "w") as f:
|
|
1670
|
+
json.dump({"action": "inject_placeholder", "epoch": epoch}, f, indent=2)
|
|
1671
|
+
_persist_runtime_state(global_step)
|
|
1672
|
+
print(
|
|
1673
|
+
f"\n [SLOW UPDATE epoch {epoch}] "
|
|
1674
|
+
f"injected empty placeholder"
|
|
1675
|
+
)
|
|
1676
|
+
else:
|
|
1677
|
+
# Epoch 2+: longitudinal comparison
|
|
1678
|
+
os.makedirs(slow_dir, exist_ok=True)
|
|
1679
|
+
print(
|
|
1680
|
+
f"\n {'='*60}\n"
|
|
1681
|
+
f" SLOW UPDATE — Epoch {epoch} "
|
|
1682
|
+
f"(comparing epoch {epoch-1} vs {epoch})\n"
|
|
1683
|
+
f" {'='*60}"
|
|
1684
|
+
)
|
|
1685
|
+
|
|
1686
|
+
# 1. Get skill from last step of previous epoch
|
|
1687
|
+
prev_epoch_records = [
|
|
1688
|
+
h for h in history if h.get("epoch") == epoch - 1
|
|
1689
|
+
]
|
|
1690
|
+
prev_epoch_last_step = prev_epoch_records[-1]["step"]
|
|
1691
|
+
prev_skill = _load_skill(out_root, prev_epoch_last_step)
|
|
1692
|
+
|
|
1693
|
+
# 2. Sample items from train set
|
|
1694
|
+
slow_n = cfg.get("slow_update_samples", 20)
|
|
1695
|
+
slow_seed = seed + epoch * 2000
|
|
1696
|
+
if dataloader is not None:
|
|
1697
|
+
slow_batch = dataloader.build_train_batch(
|
|
1698
|
+
batch_size=slow_n,
|
|
1699
|
+
seed=slow_seed,
|
|
1700
|
+
out_root=out_root,
|
|
1701
|
+
)
|
|
1702
|
+
slow_env = adapter.build_env_from_batch(
|
|
1703
|
+
slow_batch, out_root=out_root,
|
|
1704
|
+
)
|
|
1705
|
+
else:
|
|
1706
|
+
slow_env = adapter.build_train_env(
|
|
1707
|
+
batch_size=slow_n,
|
|
1708
|
+
seed=slow_seed,
|
|
1709
|
+
out_root=out_root,
|
|
1710
|
+
)
|
|
1711
|
+
slow_items = list(slow_env) if hasattr(slow_env, "__iter__") else slow_env
|
|
1712
|
+
print(f" [slow update] sampled {len(slow_items)} train items (seed={slow_seed})")
|
|
1713
|
+
|
|
1714
|
+
# 3. Rollout with both skills
|
|
1715
|
+
t_slow = time.time()
|
|
1716
|
+
prev_rollout_dir = os.path.join(slow_dir, "rollout_prev")
|
|
1717
|
+
curr_rollout_dir = os.path.join(slow_dir, "rollout_curr")
|
|
1718
|
+
results_prev = adapter.rollout(slow_env, prev_skill, prev_rollout_dir)
|
|
1719
|
+
results_curr = adapter.rollout(slow_env, current_skill, curr_rollout_dir)
|
|
1720
|
+
|
|
1721
|
+
prev_hard, _ = compute_score(results_prev)
|
|
1722
|
+
curr_hard, _ = compute_score(results_curr)
|
|
1723
|
+
print(
|
|
1724
|
+
f" [slow update] prev epoch hard={prev_hard:.4f} "
|
|
1725
|
+
f"curr epoch hard={curr_hard:.4f}"
|
|
1726
|
+
)
|
|
1727
|
+
|
|
1728
|
+
# 4. Build and save structured comparison pairs
|
|
1729
|
+
comparison_pairs, all_comparison_pairs = _build_longitudinal_pairs(
|
|
1730
|
+
adapter=adapter,
|
|
1731
|
+
dataloader=dataloader,
|
|
1732
|
+
prev_skill=prev_skill,
|
|
1733
|
+
curr_skill=current_skill,
|
|
1734
|
+
initial_items=slow_items,
|
|
1735
|
+
initial_prev_results=results_prev,
|
|
1736
|
+
initial_curr_results=results_curr,
|
|
1737
|
+
prev_rollout_dir=prev_rollout_dir,
|
|
1738
|
+
curr_rollout_dir=curr_rollout_dir,
|
|
1739
|
+
policy=longitudinal_pair_policy,
|
|
1740
|
+
target_n=slow_n,
|
|
1741
|
+
seed=slow_seed,
|
|
1742
|
+
out_root=out_root,
|
|
1743
|
+
)
|
|
1744
|
+
epoch_comparison_pairs = comparison_pairs
|
|
1745
|
+
if all_comparison_pairs is not comparison_pairs:
|
|
1746
|
+
save_comparison_pairs(
|
|
1747
|
+
all_comparison_pairs,
|
|
1748
|
+
os.path.join(slow_dir, "comparison_pairs_all.json"),
|
|
1749
|
+
)
|
|
1750
|
+
save_comparison_pairs(
|
|
1751
|
+
comparison_pairs,
|
|
1752
|
+
os.path.join(slow_dir, "comparison_pairs.json"),
|
|
1753
|
+
)
|
|
1754
|
+
n_regressed = sum(1 for p in comparison_pairs if p["category"] == "regressed")
|
|
1755
|
+
n_improved = sum(1 for p in comparison_pairs if p["category"] == "improved")
|
|
1756
|
+
n_persist = sum(1 for p in comparison_pairs if p["category"] == "persistent_fail")
|
|
1757
|
+
n_stable = sum(1 for p in comparison_pairs if p["category"] == "stable_success")
|
|
1758
|
+
print(
|
|
1759
|
+
f" [slow update] comparison: "
|
|
1760
|
+
f"regressed={n_regressed} improved={n_improved} "
|
|
1761
|
+
f"persistent_fail={n_persist} stable_success={n_stable} "
|
|
1762
|
+
f"policy={longitudinal_pair_policy} "
|
|
1763
|
+
f"kept={len(comparison_pairs)}/{len(all_comparison_pairs)}"
|
|
1764
|
+
)
|
|
1765
|
+
|
|
1766
|
+
# 5. Extract previous slow update guidance for reflection
|
|
1767
|
+
existing_guidance = extract_slow_update_field(current_skill)
|
|
1768
|
+
|
|
1769
|
+
# 6. Optimizer analysis (with reflection on previous guidance)
|
|
1770
|
+
slow_result = run_slow_update(
|
|
1771
|
+
current_skill,
|
|
1772
|
+
results_prev,
|
|
1773
|
+
results_curr,
|
|
1774
|
+
slow_items,
|
|
1775
|
+
prev_skill=prev_skill,
|
|
1776
|
+
prev_slow_update_content=existing_guidance,
|
|
1777
|
+
prev_rollout_dir=prev_rollout_dir,
|
|
1778
|
+
curr_rollout_dir=curr_rollout_dir,
|
|
1779
|
+
comparison_pairs=comparison_pairs,
|
|
1780
|
+
)
|
|
1781
|
+
slow_time = round(time.time() - t_slow, 1)
|
|
1782
|
+
|
|
1783
|
+
if slow_result and slow_result.get("slow_update_content"):
|
|
1784
|
+
slow_candidate = replace_slow_update_field(
|
|
1785
|
+
current_skill, slow_result["slow_update_content"],
|
|
1786
|
+
)
|
|
1787
|
+
slow_candidate_hash = skill_hash(slow_candidate)
|
|
1788
|
+
with open(os.path.join(slow_dir, "candidate_skill.md"), "w") as f:
|
|
1789
|
+
f.write(slow_candidate)
|
|
1790
|
+
slow_result["time_s"] = slow_time
|
|
1791
|
+
slow_result["prev_hard"] = prev_hard
|
|
1792
|
+
slow_result["curr_hard"] = curr_hard
|
|
1793
|
+
slow_result["candidate_hash"] = slow_candidate_hash
|
|
1794
|
+
slow_result["update_origin"] = "slow_update_momentum"
|
|
1795
|
+
slow_result["update_target"] = (
|
|
1796
|
+
"Address longitudinal regressions and persistent failures "
|
|
1797
|
+
"observed across adjacent epochs."
|
|
1798
|
+
)
|
|
1799
|
+
|
|
1800
|
+
# Slow update acceptance — two modes selected via
|
|
1801
|
+
# `optimizer.slow_update_gate_with_selection`.
|
|
1802
|
+
if slow_gate_with_selection:
|
|
1803
|
+
# ── Gated mode (follow SkillReflection) ──────────
|
|
1804
|
+
# Evaluate the slow-update candidate on the
|
|
1805
|
+
# selection set and accept/reject via the same
|
|
1806
|
+
# validation gate used for step-level updates.
|
|
1807
|
+
if slow_candidate_hash in sel_cache:
|
|
1808
|
+
slow_sel_hard, slow_sel_soft = sel_cache[
|
|
1809
|
+
slow_candidate_hash
|
|
1810
|
+
]
|
|
1811
|
+
print(
|
|
1812
|
+
f" [slow gate] cache hit: "
|
|
1813
|
+
f"hard={slow_sel_hard:.4f}"
|
|
1814
|
+
)
|
|
1815
|
+
else:
|
|
1816
|
+
sel_env, sel_n = _build_eval_env(
|
|
1817
|
+
split="valid_seen",
|
|
1818
|
+
env_num=cfg["sel_env_num"],
|
|
1819
|
+
seed=seed,
|
|
1820
|
+
)
|
|
1821
|
+
print(f" [slow gate] selection items={sel_n}")
|
|
1822
|
+
slow_eval_dir = os.path.join(
|
|
1823
|
+
slow_dir, "selection_eval",
|
|
1824
|
+
)
|
|
1825
|
+
slow_eval_results = adapter.rollout(
|
|
1826
|
+
sel_env, slow_candidate, slow_eval_dir,
|
|
1827
|
+
)
|
|
1828
|
+
slow_sel_hard, slow_sel_soft = compute_score(
|
|
1829
|
+
slow_eval_results
|
|
1830
|
+
)
|
|
1831
|
+
sel_cache[slow_candidate_hash] = (
|
|
1832
|
+
slow_sel_hard, slow_sel_soft,
|
|
1833
|
+
)
|
|
1834
|
+
|
|
1835
|
+
slow_gate = evaluate_gate(
|
|
1836
|
+
candidate_skill=slow_candidate,
|
|
1837
|
+
cand_hard=slow_sel_hard,
|
|
1838
|
+
current_skill=current_skill,
|
|
1839
|
+
current_score=current_score,
|
|
1840
|
+
best_skill=best_skill,
|
|
1841
|
+
best_score=best_score,
|
|
1842
|
+
best_step=best_step,
|
|
1843
|
+
global_step=global_step,
|
|
1844
|
+
cand_soft=slow_sel_soft,
|
|
1845
|
+
metric=gate_metric,
|
|
1846
|
+
mixed_weight=gate_mixed_weight,
|
|
1847
|
+
)
|
|
1848
|
+
slow_result["selection_hard"] = slow_sel_hard
|
|
1849
|
+
slow_result["selection_soft"] = slow_sel_soft
|
|
1850
|
+
slow_result["action"] = slow_gate.action
|
|
1851
|
+
prev_current = current_score
|
|
1852
|
+
prev_best = best_score
|
|
1853
|
+
current_skill = slow_gate.current_skill
|
|
1854
|
+
current_score = slow_gate.current_score
|
|
1855
|
+
best_skill = slow_gate.best_skill
|
|
1856
|
+
best_score = slow_gate.best_score
|
|
1857
|
+
best_step = slow_gate.best_step
|
|
1858
|
+
if slow_gate.action in {"accept", "accept_new_best"}:
|
|
1859
|
+
current_origin = (
|
|
1860
|
+
f"slow_update_epoch_{epoch:02d}"
|
|
1861
|
+
)
|
|
1862
|
+
if slow_gate.action == "accept_new_best":
|
|
1863
|
+
best_origin = current_origin
|
|
1864
|
+
print(
|
|
1865
|
+
f" [slow gate] ACCEPT (new best) "
|
|
1866
|
+
f"hard={slow_sel_hard:.4f} > "
|
|
1867
|
+
f"prev best {prev_best:.4f}"
|
|
1868
|
+
)
|
|
1869
|
+
elif slow_gate.action == "accept":
|
|
1870
|
+
print(
|
|
1871
|
+
f" [slow gate] ACCEPT "
|
|
1872
|
+
f"hard={slow_sel_hard:.4f} > "
|
|
1873
|
+
f"current={prev_current:.4f}"
|
|
1874
|
+
)
|
|
1875
|
+
else:
|
|
1876
|
+
print(
|
|
1877
|
+
f" [slow gate] REJECT "
|
|
1878
|
+
f"hard={slow_sel_hard:.4f} <= "
|
|
1879
|
+
f"current={current_score:.4f}"
|
|
1880
|
+
)
|
|
1881
|
+
print(
|
|
1882
|
+
f" [slow update] guidance written "
|
|
1883
|
+
f"({len(slow_result['slow_update_content'])} "
|
|
1884
|
+
f"chars), {slow_time}s"
|
|
1885
|
+
)
|
|
1886
|
+
else:
|
|
1887
|
+
# ── Force-accept mode (default) ──────────────────
|
|
1888
|
+
# The epoch-level longitudinal guidance is injected
|
|
1889
|
+
# into current_skill ONLY, so training continues
|
|
1890
|
+
# with the accumulated slow memory. best_skill is
|
|
1891
|
+
# left untouched: it must remain a faithful snapshot
|
|
1892
|
+
# of the val-best step (which may be a pre-slow step
|
|
1893
|
+
# such as S_0 carrying no slow_update field at all).
|
|
1894
|
+
slow_content = slow_result["slow_update_content"]
|
|
1895
|
+
current_skill = replace_slow_update_field(
|
|
1896
|
+
current_skill, slow_content,
|
|
1897
|
+
)
|
|
1898
|
+
# Update caches so downstream steps use the
|
|
1899
|
+
# slow-update-injected skill for hashing.
|
|
1900
|
+
slow_candidate_hash = skill_hash(current_skill)
|
|
1901
|
+
sel_cache[slow_candidate_hash] = (current_score, 0.0)
|
|
1902
|
+
|
|
1903
|
+
slow_result["action"] = "force_accept"
|
|
1904
|
+
current_origin = f"slow_update_epoch_{epoch:02d}"
|
|
1905
|
+
|
|
1906
|
+
print(
|
|
1907
|
+
f" [slow update] force-injected into "
|
|
1908
|
+
f"current only "
|
|
1909
|
+
f"({len(slow_content)} chars), "
|
|
1910
|
+
f"{slow_time}s"
|
|
1911
|
+
)
|
|
1912
|
+
else:
|
|
1913
|
+
slow_result = slow_result or {}
|
|
1914
|
+
slow_result["action"] = "no_content"
|
|
1915
|
+
slow_result["time_s"] = slow_time
|
|
1916
|
+
print(
|
|
1917
|
+
f" [slow update] no guidance produced, "
|
|
1918
|
+
f"{slow_time}s"
|
|
1919
|
+
)
|
|
1920
|
+
|
|
1921
|
+
# 5. Save
|
|
1922
|
+
with open(slow_done_path, "w") as f:
|
|
1923
|
+
json.dump(slow_result, f, indent=2, ensure_ascii=False)
|
|
1924
|
+
_save_skill(out_root, global_step, current_skill)
|
|
1925
|
+
with open(os.path.join(out_root, "best_skill.md"), "w") as f:
|
|
1926
|
+
f.write(best_skill)
|
|
1927
|
+
_persist_runtime_state(global_step)
|
|
1928
|
+
|
|
1929
|
+
print(
|
|
1930
|
+
f"\n [SLOW UPDATE epoch {epoch} done] "
|
|
1931
|
+
f"current={current_score:.4f} best={best_score:.4f}"
|
|
1932
|
+
)
|
|
1933
|
+
|
|
1934
|
+
# ── META SKILL (end of epoch, optimizer-side memory) ─────────
|
|
1935
|
+
use_meta_skill = cfg.get("use_meta_skill", False)
|
|
1936
|
+
if use_meta_skill:
|
|
1937
|
+
meta_skill_dir = os.path.join(out_root, "meta_skill", f"epoch_{epoch:02d}")
|
|
1938
|
+
meta_skill_done_path = os.path.join(meta_skill_dir, "meta_skill_result.json")
|
|
1939
|
+
os.makedirs(meta_skill_dir, exist_ok=True)
|
|
1940
|
+
|
|
1941
|
+
if os.path.exists(meta_skill_done_path):
|
|
1942
|
+
print(f"\n [META SKILL epoch {epoch}] resumed — already done")
|
|
1943
|
+
elif epoch == 1:
|
|
1944
|
+
with open(meta_skill_done_path, "w") as f:
|
|
1945
|
+
json.dump(
|
|
1946
|
+
{"action": "skip_first_epoch", "epoch": epoch},
|
|
1947
|
+
f, indent=2, ensure_ascii=False,
|
|
1948
|
+
)
|
|
1949
|
+
print(f"\n [META SKILL epoch {epoch}] skipped — first epoch")
|
|
1950
|
+
else:
|
|
1951
|
+
print(
|
|
1952
|
+
f"\n {'='*60}\n"
|
|
1953
|
+
f" META SKILL — Epoch {epoch} "
|
|
1954
|
+
f"(optimizer memory from epoch {epoch-1} vs {epoch})\n"
|
|
1955
|
+
f" {'='*60}"
|
|
1956
|
+
)
|
|
1957
|
+
|
|
1958
|
+
prev_epoch_records = [h for h in history if h.get("epoch") == epoch - 1]
|
|
1959
|
+
prev_epoch_last_step = prev_epoch_records[-1]["step"]
|
|
1960
|
+
prev_skill = _load_skill(out_root, prev_epoch_last_step)
|
|
1961
|
+
prev_meta_skill = _load_meta_skill_content(out_root, epoch - 1)
|
|
1962
|
+
|
|
1963
|
+
if epoch_comparison_pairs is None:
|
|
1964
|
+
meta_n = cfg.get("slow_update_samples", 20)
|
|
1965
|
+
meta_seed = seed + epoch * 2000
|
|
1966
|
+
if dataloader is not None:
|
|
1967
|
+
meta_batch = dataloader.build_train_batch(
|
|
1968
|
+
batch_size=meta_n,
|
|
1969
|
+
seed=meta_seed,
|
|
1970
|
+
out_root=out_root,
|
|
1971
|
+
)
|
|
1972
|
+
meta_env = adapter.build_env_from_batch(
|
|
1973
|
+
meta_batch, out_root=out_root,
|
|
1974
|
+
)
|
|
1975
|
+
else:
|
|
1976
|
+
meta_env = adapter.build_train_env(
|
|
1977
|
+
batch_size=meta_n,
|
|
1978
|
+
seed=meta_seed,
|
|
1979
|
+
out_root=out_root,
|
|
1980
|
+
)
|
|
1981
|
+
meta_items = list(meta_env) if hasattr(meta_env, "__iter__") else meta_env
|
|
1982
|
+
prev_rollout_dir = os.path.join(meta_skill_dir, "rollout_prev")
|
|
1983
|
+
curr_rollout_dir = os.path.join(meta_skill_dir, "rollout_curr")
|
|
1984
|
+
results_prev = adapter.rollout(meta_env, prev_skill, prev_rollout_dir)
|
|
1985
|
+
results_curr = adapter.rollout(meta_env, epoch_last_step_skill, curr_rollout_dir)
|
|
1986
|
+
epoch_comparison_pairs, all_meta_comparison_pairs = _build_longitudinal_pairs(
|
|
1987
|
+
adapter=adapter,
|
|
1988
|
+
dataloader=dataloader,
|
|
1989
|
+
prev_skill=prev_skill,
|
|
1990
|
+
curr_skill=epoch_last_step_skill,
|
|
1991
|
+
initial_items=meta_items,
|
|
1992
|
+
initial_prev_results=results_prev,
|
|
1993
|
+
initial_curr_results=results_curr,
|
|
1994
|
+
prev_rollout_dir=prev_rollout_dir,
|
|
1995
|
+
curr_rollout_dir=curr_rollout_dir,
|
|
1996
|
+
policy=longitudinal_pair_policy,
|
|
1997
|
+
target_n=meta_n,
|
|
1998
|
+
seed=meta_seed,
|
|
1999
|
+
out_root=out_root,
|
|
2000
|
+
)
|
|
2001
|
+
if all_meta_comparison_pairs is not epoch_comparison_pairs:
|
|
2002
|
+
save_comparison_pairs(
|
|
2003
|
+
all_meta_comparison_pairs,
|
|
2004
|
+
os.path.join(meta_skill_dir, "comparison_pairs_all.json"),
|
|
2005
|
+
)
|
|
2006
|
+
save_comparison_pairs(
|
|
2007
|
+
epoch_comparison_pairs,
|
|
2008
|
+
os.path.join(meta_skill_dir, "comparison_pairs.json"),
|
|
2009
|
+
)
|
|
2010
|
+
meta_counts = _pair_category_counts(epoch_comparison_pairs)
|
|
2011
|
+
print(
|
|
2012
|
+
f" [meta skill] comparison: "
|
|
2013
|
+
f"regressed={meta_counts.get('regressed', 0)} "
|
|
2014
|
+
f"improved={meta_counts.get('improved', 0)} "
|
|
2015
|
+
f"persistent_fail={meta_counts.get('persistent_fail', 0)} "
|
|
2016
|
+
f"stable_success={meta_counts.get('stable_success', 0)} "
|
|
2017
|
+
f"policy={longitudinal_pair_policy} "
|
|
2018
|
+
f"kept={len(epoch_comparison_pairs)}/{len(all_meta_comparison_pairs)}"
|
|
2019
|
+
)
|
|
2020
|
+
|
|
2021
|
+
t_meta_skill = time.time()
|
|
2022
|
+
meta_skill_result = run_meta_skill(
|
|
2023
|
+
prev_skill=prev_skill,
|
|
2024
|
+
curr_skill=epoch_last_step_skill,
|
|
2025
|
+
comparison_pairs=epoch_comparison_pairs or [],
|
|
2026
|
+
prev_meta_skill_content=prev_meta_skill,
|
|
2027
|
+
)
|
|
2028
|
+
meta_skill_time = round(time.time() - t_meta_skill, 1)
|
|
2029
|
+
|
|
2030
|
+
if meta_skill_result and meta_skill_result.get("meta_skill_content"):
|
|
2031
|
+
meta_skill_result["time_s"] = meta_skill_time
|
|
2032
|
+
meta_skill_result["action"] = "write_meta_skill"
|
|
2033
|
+
print(
|
|
2034
|
+
f" [meta skill] memory written "
|
|
2035
|
+
f"({len(meta_skill_result['meta_skill_content'])} chars), "
|
|
2036
|
+
f"{meta_skill_time}s"
|
|
2037
|
+
)
|
|
2038
|
+
else:
|
|
2039
|
+
meta_skill_result = meta_skill_result or {}
|
|
2040
|
+
meta_skill_result["time_s"] = meta_skill_time
|
|
2041
|
+
meta_skill_result["action"] = "no_content"
|
|
2042
|
+
print(f" [meta skill] no memory produced, {meta_skill_time}s")
|
|
2043
|
+
|
|
2044
|
+
with open(meta_skill_done_path, "w") as f:
|
|
2045
|
+
json.dump(meta_skill_result, f, indent=2, ensure_ascii=False)
|
|
2046
|
+
|
|
2047
|
+
# ── Save best skill ──────────────────────────────────────────────
|
|
2048
|
+
with open(os.path.join(out_root, "best_skill.md"), "w") as f:
|
|
2049
|
+
f.write(best_skill)
|
|
2050
|
+
_persist_runtime_state(global_step)
|
|
2051
|
+
print(
|
|
2052
|
+
f"\n [done] best skill from step {best_step}, "
|
|
2053
|
+
f"score={best_score:.4f}"
|
|
2054
|
+
)
|
|
2055
|
+
|
|
2056
|
+
# ── Final test evaluation (valid_unseen) ─────────────────────────
|
|
2057
|
+
baseline_test_hard = None
|
|
2058
|
+
baseline_test_soft = None
|
|
2059
|
+
test_hard = None
|
|
2060
|
+
test_soft = None
|
|
2061
|
+
final_test_hard = None
|
|
2062
|
+
final_test_soft = None
|
|
2063
|
+
final_selection_hard = None
|
|
2064
|
+
final_selection_soft = None
|
|
2065
|
+
|
|
2066
|
+
if cfg["eval_test"]:
|
|
2067
|
+
task_types = adapter.get_task_types()
|
|
2068
|
+
|
|
2069
|
+
# ── Final skill validation (valid_seen) + best promotion ─────
|
|
2070
|
+
# The final (last) skill may carry an epoch-end slow_update that
|
|
2071
|
+
# was force-injected WITHOUT a val pass (use_gate=false or
|
|
2072
|
+
# slow_update_gate_with_selection=false), so it never competed for
|
|
2073
|
+
# best. Run one real val on the final skill; if its gate score
|
|
2074
|
+
# beats the incumbent best, PROMOTE it to best so that best is the
|
|
2075
|
+
# true val-argmax over all skills (including the final slow_update).
|
|
2076
|
+
# When final == best, reuse the existing val score (no rollout).
|
|
2077
|
+
try:
|
|
2078
|
+
if skill_hash(current_skill) == skill_hash(best_skill):
|
|
2079
|
+
final_selection_hard, final_selection_soft = best_score, None
|
|
2080
|
+
print(
|
|
2081
|
+
"\n [final skill == best skill] "
|
|
2082
|
+
f"final_selection_hard={best_score:.4f} (reused)"
|
|
2083
|
+
)
|
|
2084
|
+
else:
|
|
2085
|
+
fval_env, fval_n = _build_eval_env(
|
|
2086
|
+
split="valid_seen",
|
|
2087
|
+
env_num=cfg["sel_env_num"],
|
|
2088
|
+
seed=seed,
|
|
2089
|
+
)
|
|
2090
|
+
fval_dir = os.path.join(out_root, "final_selection_eval")
|
|
2091
|
+
fval_results = adapter.rollout(fval_env, current_skill, fval_dir)
|
|
2092
|
+
final_selection_hard, final_selection_soft = compute_score(fval_results)
|
|
2093
|
+
final_gate_score = select_gate_score(
|
|
2094
|
+
final_selection_hard, final_selection_soft,
|
|
2095
|
+
gate_metric, gate_mixed_weight,
|
|
2096
|
+
)
|
|
2097
|
+
print(
|
|
2098
|
+
f"\n [final skill val] items={fval_n} "
|
|
2099
|
+
f"final_selection_hard={final_selection_hard:.4f} "
|
|
2100
|
+
f"gate={final_gate_score:.4f} "
|
|
2101
|
+
f"(best={best_score:.4f})"
|
|
2102
|
+
)
|
|
2103
|
+
if final_gate_score > best_score:
|
|
2104
|
+
# Promote: the final (slow-updated) skill is val-better
|
|
2105
|
+
# than the incumbent best. Make it the new best so the
|
|
2106
|
+
# subsequent BEST-skill test rollout evaluates it and
|
|
2107
|
+
# best/final test scores coincide.
|
|
2108
|
+
print(
|
|
2109
|
+
f" [promote] final {final_gate_score:.4f} > "
|
|
2110
|
+
f"best {best_score:.4f} → final becomes new best "
|
|
2111
|
+
f"(step {global_step}, origin {current_origin})"
|
|
2112
|
+
)
|
|
2113
|
+
best_skill = current_skill
|
|
2114
|
+
best_score = final_gate_score
|
|
2115
|
+
best_step = global_step
|
|
2116
|
+
best_origin = current_origin
|
|
2117
|
+
with open(os.path.join(out_root, "best_skill.md"), "w") as f:
|
|
2118
|
+
f.write(best_skill)
|
|
2119
|
+
_persist_runtime_state(global_step)
|
|
2120
|
+
except Exception as _e: # noqa: BLE001
|
|
2121
|
+
final_selection_hard = None
|
|
2122
|
+
final_selection_soft = None
|
|
2123
|
+
print(f"\n [final skill val FAILED: {_e!r}]")
|
|
2124
|
+
|
|
2125
|
+
# Baseline: S_0 on test set (valid_unseen)
|
|
2126
|
+
print(f"\n{'='*60}")
|
|
2127
|
+
print(" BASELINE TEST — evaluate initial skill on Test set (valid_unseen)")
|
|
2128
|
+
print(f"{'='*60}")
|
|
2129
|
+
test_env, test_n = _build_eval_env(
|
|
2130
|
+
split="valid_unseen",
|
|
2131
|
+
env_num=cfg["test_env_num"],
|
|
2132
|
+
seed=seed,
|
|
2133
|
+
)
|
|
2134
|
+
print(f" Test items: {test_n}")
|
|
2135
|
+
baseline_test_dir = os.path.join(out_root, "test_eval_baseline")
|
|
2136
|
+
os.makedirs(baseline_test_dir, exist_ok=True)
|
|
2137
|
+
baseline_test_results = adapter.rollout(test_env, skill_init, baseline_test_dir)
|
|
2138
|
+
baseline_test_hard, baseline_test_soft = compute_score(baseline_test_results)
|
|
2139
|
+
baseline_buckets = _compute_task_type_buckets(baseline_test_results, task_types)
|
|
2140
|
+
print("\n === Baseline Test Results (S_0) ===")
|
|
2141
|
+
for task_type in task_types + ["overall"]:
|
|
2142
|
+
b = baseline_buckets.get(task_type, {"total": 0, "hard": 0})
|
|
2143
|
+
t = max(b["total"], 1)
|
|
2144
|
+
print(
|
|
2145
|
+
f" {task_type:<40s}: "
|
|
2146
|
+
f"hard={b['hard']}/{b['total']}={b['hard']/t:.4f}"
|
|
2147
|
+
)
|
|
2148
|
+
with open(os.path.join(baseline_test_dir, "summary.json"), "w") as f:
|
|
2149
|
+
json.dump(
|
|
2150
|
+
{
|
|
2151
|
+
k: {
|
|
2152
|
+
"total": b["total"],
|
|
2153
|
+
"hard_acc": b["hard"] / max(b["total"], 1),
|
|
2154
|
+
}
|
|
2155
|
+
for k, b in baseline_buckets.items()
|
|
2156
|
+
},
|
|
2157
|
+
f, indent=2, ensure_ascii=False,
|
|
2158
|
+
)
|
|
2159
|
+
|
|
2160
|
+
# Best skill on test set
|
|
2161
|
+
print(f"\n{'='*60}")
|
|
2162
|
+
print(" BEST SKILL TEST — evaluate best skill on Test set (valid_unseen)")
|
|
2163
|
+
print(f"{'='*60}")
|
|
2164
|
+
test_env2, test_n2 = _build_eval_env(
|
|
2165
|
+
split="valid_unseen",
|
|
2166
|
+
env_num=cfg["test_env_num"],
|
|
2167
|
+
seed=seed,
|
|
2168
|
+
)
|
|
2169
|
+
print(f" Test items: {test_n2}")
|
|
2170
|
+
test_dir = os.path.join(out_root, "test_eval")
|
|
2171
|
+
os.makedirs(test_dir, exist_ok=True)
|
|
2172
|
+
test_results = adapter.rollout(test_env2, best_skill, test_dir)
|
|
2173
|
+
test_hard, test_soft = compute_score(test_results)
|
|
2174
|
+
best_buckets = _compute_task_type_buckets(test_results, task_types)
|
|
2175
|
+
print("\n === Best Skill Test Results ===")
|
|
2176
|
+
for task_type in task_types + ["overall"]:
|
|
2177
|
+
b = best_buckets.get(task_type, {"total": 0, "hard": 0})
|
|
2178
|
+
t = max(b["total"], 1)
|
|
2179
|
+
print(
|
|
2180
|
+
f" {task_type:<40s}: "
|
|
2181
|
+
f"hard={b['hard']}/{b['total']}={b['hard']/t:.4f}"
|
|
2182
|
+
)
|
|
2183
|
+
with open(os.path.join(test_dir, "summary.json"), "w") as f:
|
|
2184
|
+
json.dump(
|
|
2185
|
+
{
|
|
2186
|
+
k: {
|
|
2187
|
+
"total": b["total"],
|
|
2188
|
+
"hard_acc": b["hard"] / max(b["total"], 1),
|
|
2189
|
+
}
|
|
2190
|
+
for k, b in best_buckets.items()
|
|
2191
|
+
},
|
|
2192
|
+
f, indent=2, ensure_ascii=False,
|
|
2193
|
+
)
|
|
2194
|
+
|
|
2195
|
+
# Final skill (last skill in trajectory) on test set.
|
|
2196
|
+
# Distinct from best_skill: with use_gate=False every candidate is
|
|
2197
|
+
# force-accepted so the final skill is whatever the last step
|
|
2198
|
+
# produced; with use_gate=True it is the last accepted skill, which
|
|
2199
|
+
# may differ from the best-on-val skill. We always evaluate it so
|
|
2200
|
+
# every run reports baseline / best-on-val / final on test.
|
|
2201
|
+
# Guarded so a failure here never prevents summary.json from being
|
|
2202
|
+
# written (the orchestrator's post-hoc safety net fills it in).
|
|
2203
|
+
try:
|
|
2204
|
+
if skill_hash(current_skill) == skill_hash(best_skill):
|
|
2205
|
+
# Final == best: reuse results, skip a redundant rollout.
|
|
2206
|
+
final_test_hard, final_test_soft = test_hard, test_soft
|
|
2207
|
+
final_test_dir = os.path.join(out_root, "test_eval_final")
|
|
2208
|
+
os.makedirs(final_test_dir, exist_ok=True)
|
|
2209
|
+
with open(os.path.join(final_test_dir, "summary.json"), "w") as f:
|
|
2210
|
+
json.dump(
|
|
2211
|
+
{
|
|
2212
|
+
k: {
|
|
2213
|
+
"total": b["total"],
|
|
2214
|
+
"hard_acc": b["hard"] / max(b["total"], 1),
|
|
2215
|
+
}
|
|
2216
|
+
for k, b in best_buckets.items()
|
|
2217
|
+
},
|
|
2218
|
+
f, indent=2, ensure_ascii=False,
|
|
2219
|
+
)
|
|
2220
|
+
print(
|
|
2221
|
+
"\n [final skill == best skill] "
|
|
2222
|
+
f"final_test_hard={final_test_hard:.4f} (reused)"
|
|
2223
|
+
)
|
|
2224
|
+
else:
|
|
2225
|
+
print(f"\n{'='*60}")
|
|
2226
|
+
print(" FINAL SKILL TEST — evaluate last skill on Test set (valid_unseen)")
|
|
2227
|
+
print(f"{'='*60}")
|
|
2228
|
+
test_env3, test_n3 = _build_eval_env(
|
|
2229
|
+
split="valid_unseen",
|
|
2230
|
+
env_num=cfg["test_env_num"],
|
|
2231
|
+
seed=seed,
|
|
2232
|
+
)
|
|
2233
|
+
print(f" Test items: {test_n3}")
|
|
2234
|
+
final_test_dir = os.path.join(out_root, "test_eval_final")
|
|
2235
|
+
os.makedirs(final_test_dir, exist_ok=True)
|
|
2236
|
+
final_test_results = adapter.rollout(test_env3, current_skill, final_test_dir)
|
|
2237
|
+
final_test_hard, final_test_soft = compute_score(final_test_results)
|
|
2238
|
+
final_buckets = _compute_task_type_buckets(final_test_results, task_types)
|
|
2239
|
+
print("\n === Final Skill Test Results ===")
|
|
2240
|
+
for task_type in task_types + ["overall"]:
|
|
2241
|
+
b = final_buckets.get(task_type, {"total": 0, "hard": 0})
|
|
2242
|
+
t = max(b["total"], 1)
|
|
2243
|
+
print(
|
|
2244
|
+
f" {task_type:<40s}: "
|
|
2245
|
+
f"hard={b['hard']}/{b['total']}={b['hard']/t:.4f}"
|
|
2246
|
+
)
|
|
2247
|
+
with open(os.path.join(final_test_dir, "summary.json"), "w") as f:
|
|
2248
|
+
json.dump(
|
|
2249
|
+
{
|
|
2250
|
+
k: {
|
|
2251
|
+
"total": b["total"],
|
|
2252
|
+
"hard_acc": b["hard"] / max(b["total"], 1),
|
|
2253
|
+
}
|
|
2254
|
+
for k, b in final_buckets.items()
|
|
2255
|
+
},
|
|
2256
|
+
f, indent=2, ensure_ascii=False,
|
|
2257
|
+
)
|
|
2258
|
+
except Exception as _e: # noqa: BLE001
|
|
2259
|
+
final_test_hard = None
|
|
2260
|
+
final_test_soft = None
|
|
2261
|
+
print(f"\n [final skill test FAILED: {_e!r}] "
|
|
2262
|
+
"— will be filled by post-hoc eval")
|
|
2263
|
+
|
|
2264
|
+
# Comparison
|
|
2265
|
+
delta_hard = (test_hard or 0) - (baseline_test_hard or 0)
|
|
2266
|
+
print(f"\n === Improvement vs baseline (init S_0) ===")
|
|
2267
|
+
print(
|
|
2268
|
+
f" [2] best-on-val hard: {baseline_test_hard:.4f} -> {test_hard:.4f} "
|
|
2269
|
+
f"(delta={delta_hard:+.4f})"
|
|
2270
|
+
)
|
|
2271
|
+
if final_test_hard is not None:
|
|
2272
|
+
final_delta_hard = (final_test_hard or 0) - (baseline_test_hard or 0)
|
|
2273
|
+
print(
|
|
2274
|
+
f" [3] final/last hard: {baseline_test_hard:.4f} -> {final_test_hard:.4f} "
|
|
2275
|
+
f"(delta={final_delta_hard:+.4f})"
|
|
2276
|
+
)
|
|
2277
|
+
|
|
2278
|
+
# ── Global summary ───────────────────────────────────────────────
|
|
2279
|
+
total_wall = time.time() - t_loop_start
|
|
2280
|
+
n_accept = sum(1 for h in history if "accept" in h.get("action", ""))
|
|
2281
|
+
n_reject = sum(1 for h in history if h.get("action") == "reject")
|
|
2282
|
+
n_skip = sum(1 for h in history if h.get("action") == "skip_no_patches")
|
|
2283
|
+
|
|
2284
|
+
token_summary = get_token_summary()
|
|
2285
|
+
|
|
2286
|
+
# Epoch-level statistics
|
|
2287
|
+
epoch_stats = []
|
|
2288
|
+
for e in range(1, num_epochs + 1):
|
|
2289
|
+
epoch_records = [h for h in history if h.get("epoch") == e]
|
|
2290
|
+
if epoch_records:
|
|
2291
|
+
epoch_stats.append({
|
|
2292
|
+
"epoch": e,
|
|
2293
|
+
"steps": [h["step"] for h in epoch_records],
|
|
2294
|
+
"accepts": sum(1 for h in epoch_records if "accept" in h.get("action", "")),
|
|
2295
|
+
"rejects": sum(1 for h in epoch_records if h.get("action") == "reject"),
|
|
2296
|
+
"skips": sum(1 for h in epoch_records if h.get("action") == "skip_no_patches"),
|
|
2297
|
+
"best_score_at_epoch_end": epoch_records[-1].get("best_score", 0.0),
|
|
2298
|
+
"current_score_at_epoch_end": epoch_records[-1].get("current_score", 0.0),
|
|
2299
|
+
})
|
|
2300
|
+
|
|
2301
|
+
summary = {
|
|
2302
|
+
"version": "skillopt-0.1.0",
|
|
2303
|
+
"config": _redact_cfg(cfg),
|
|
2304
|
+
"baseline_selection_hard": sel_cache.get(
|
|
2305
|
+
skill_hash(skill_init), (None, None),
|
|
2306
|
+
)[0],
|
|
2307
|
+
"best_selection_hard": best_score,
|
|
2308
|
+
"final_selection_hard": final_selection_hard,
|
|
2309
|
+
"final_selection_soft": final_selection_soft,
|
|
2310
|
+
"best_step": best_step,
|
|
2311
|
+
"current_origin": current_origin,
|
|
2312
|
+
"best_origin": best_origin,
|
|
2313
|
+
"total_steps": len(history),
|
|
2314
|
+
"total_accepts": n_accept,
|
|
2315
|
+
"total_rejects": n_reject,
|
|
2316
|
+
"total_skips": n_skip,
|
|
2317
|
+
"epoch_stats": epoch_stats,
|
|
2318
|
+
"baseline_test_hard": baseline_test_hard,
|
|
2319
|
+
"baseline_test_soft": baseline_test_soft,
|
|
2320
|
+
"test_hard": test_hard,
|
|
2321
|
+
"test_soft": test_soft,
|
|
2322
|
+
"final_test_hard": final_test_hard,
|
|
2323
|
+
"final_test_soft": final_test_soft,
|
|
2324
|
+
"test_delta_hard": (
|
|
2325
|
+
(test_hard or 0) - (baseline_test_hard or 0)
|
|
2326
|
+
if test_hard is not None
|
|
2327
|
+
else None
|
|
2328
|
+
),
|
|
2329
|
+
"final_test_delta_hard": (
|
|
2330
|
+
(final_test_hard or 0) - (baseline_test_hard or 0)
|
|
2331
|
+
if final_test_hard is not None
|
|
2332
|
+
else None
|
|
2333
|
+
),
|
|
2334
|
+
"total_wall_time_s": round(total_wall, 1),
|
|
2335
|
+
"token_summary": token_summary,
|
|
2336
|
+
}
|
|
2337
|
+
with open(os.path.join(out_root, "summary.json"), "w") as f:
|
|
2338
|
+
json.dump(summary, f, indent=2, ensure_ascii=False)
|
|
2339
|
+
|
|
2340
|
+
print(f"\n{'='*60}")
|
|
2341
|
+
print(" Final Summary")
|
|
2342
|
+
print(f"{'='*60}")
|
|
2343
|
+
print(
|
|
2344
|
+
f" steps={len(history)} accept={n_accept} "
|
|
2345
|
+
f"reject={n_reject} skip={n_skip}"
|
|
2346
|
+
)
|
|
2347
|
+
print(f" best_score={best_score:.4f} (step {best_step}) wall={total_wall:.0f}s")
|
|
2348
|
+
if epoch_stats:
|
|
2349
|
+
for es in epoch_stats:
|
|
2350
|
+
print(
|
|
2351
|
+
f" epoch {es['epoch']}: accept={es['accepts']} reject={es['rejects']} "
|
|
2352
|
+
f"best={es['best_score_at_epoch_end']:.4f}"
|
|
2353
|
+
)
|
|
2354
|
+
if baseline_test_hard is not None:
|
|
2355
|
+
print("\n === TEST scores (3 skills, split=valid_unseen) ===")
|
|
2356
|
+
print(
|
|
2357
|
+
f" [1] init/baseline (S_0) : "
|
|
2358
|
+
f"test_hard={baseline_test_hard:.4f}"
|
|
2359
|
+
)
|
|
2360
|
+
if test_hard is not None:
|
|
2361
|
+
print(
|
|
2362
|
+
f" [2] best-on-val (step {best_step})".ljust(37)
|
|
2363
|
+
+ f": test_hard={test_hard:.4f} test_soft={test_soft:.4f}"
|
|
2364
|
+
)
|
|
2365
|
+
if final_test_hard is not None:
|
|
2366
|
+
print(
|
|
2367
|
+
f" [3] final/last skill : "
|
|
2368
|
+
f"test_hard={final_test_hard:.4f} test_soft={final_test_soft:.4f}"
|
|
2369
|
+
)
|
|
2370
|
+
if token_summary.get("_total"):
|
|
2371
|
+
t = token_summary["_total"]
|
|
2372
|
+
print(
|
|
2373
|
+
f" total tokens: {t['total_tokens']:,} "
|
|
2374
|
+
f"(prompt={t['prompt_tokens']:,} "
|
|
2375
|
+
f"completion={t['completion_tokens']:,} "
|
|
2376
|
+
f"calls={t['calls']})"
|
|
2377
|
+
)
|
|
2378
|
+
|
|
2379
|
+
return summary
|