self-evolve-framework 1.4.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/rules/ponytail.mdc +98 -23
- 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
- package/template/skills/ponytail/SKILL.md +0 -133
- package/template/skills/ponytail/scripts/hooks/claude-codex-hooks.json +0 -44
- package/template/skills/ponytail/scripts/hooks/copilot-hooks.json +0 -21
- package/template/skills/ponytail/scripts/hooks/ponytail-activate.js +0 -91
- package/template/skills/ponytail/scripts/hooks/ponytail-config.js +0 -122
- package/template/skills/ponytail/scripts/hooks/ponytail-instructions.js +0 -94
- package/template/skills/ponytail/scripts/hooks/ponytail-mode-tracker.js +0 -55
- package/template/skills/ponytail/scripts/hooks/ponytail-runtime.js +0 -68
- package/template/skills/ponytail/scripts/hooks/ponytail-statusline.ps1 +0 -21
- package/template/skills/ponytail/scripts/hooks/ponytail-statusline.sh +0 -12
- package/template/skills/ponytail/scripts/hooks/ponytail-subagent.js +0 -22
- package/template/skills/ponytail/scripts/mcp/README.md +0 -46
- package/template/skills/ponytail/scripts/mcp/index.js +0 -48
- package/template/skills/ponytail/scripts/mcp/instructions.js +0 -26
- package/template/skills/ponytail/scripts/mcp/package.json +0 -13
- package/template/skills/ponytail/scripts/mcp/test/instructions.test.js +0 -22
- package/template/skills/ponytail-audit/SKILL.md +0 -41
- package/template/skills/ponytail-debt/SKILL.md +0 -44
- package/template/skills/ponytail-gain/SKILL.md +0 -50
- package/template/skills/ponytail-help/SKILL.md +0 -69
- package/template/skills/ponytail-review/SKILL.md +0 -57
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Skill-Aware Reflection — protected appendix field (EmbodiSkill S_app).
|
|
2
|
+
|
|
3
|
+
EmbodiSkill (paper 2605.10332v1) splits a skill into ``S = (S_body, S_app)``:
|
|
4
|
+
the body holds the main prescriptive rules; the appendix only *emphasizes*
|
|
5
|
+
existing valid rules that the executor failed to follow (EXECUTION_LAPSE), and
|
|
6
|
+
**never introduces new rules**.
|
|
7
|
+
|
|
8
|
+
This module owns the appendix region of the skill document. It mirrors the
|
|
9
|
+
protected-field pattern of :mod:`skillopt.optimizer.slow_update`, with two
|
|
10
|
+
differences:
|
|
11
|
+
|
|
12
|
+
1. **Append semantics** (not replace): execution-lapse reminders accumulate
|
|
13
|
+
across steps within a run, so new notes are merged into the existing
|
|
14
|
+
appendix rather than overwriting it.
|
|
15
|
+
2. **Lightweight dedup**: near-duplicate reminders are collapsed (inspired by
|
|
16
|
+
GMemory's ``_dedupe_preserve_order``) so the appendix stays compact.
|
|
17
|
+
|
|
18
|
+
The appendix lives **inside** the skill markdown, between dedicated markers, so
|
|
19
|
+
it is persisted by the normal ``_save_skill`` path and is resume-safe. Step-level
|
|
20
|
+
analyst edits cannot modify it (enforced by the shared protected-region check in
|
|
21
|
+
:mod:`skillopt.optimizer.skill`).
|
|
22
|
+
|
|
23
|
+
Public API
|
|
24
|
+
----------
|
|
25
|
+
- :func:`has_appendix_field` — check if markers are present
|
|
26
|
+
- :func:`inject_empty_appendix_field` — add empty placeholder (skill init)
|
|
27
|
+
- :func:`extract_appendix_notes` — read current notes as a list
|
|
28
|
+
- :func:`append_to_appendix_field` — merge new notes (dedup) into the region
|
|
29
|
+
"""
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import re
|
|
33
|
+
|
|
34
|
+
# ── Protected field markers ─────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
APPENDIX_START = "<!-- APPENDIX_START -->"
|
|
37
|
+
APPENDIX_END = "<!-- APPENDIX_END -->"
|
|
38
|
+
|
|
39
|
+
# Heading shown inside the rendered appendix block (human-readable only).
|
|
40
|
+
APPENDIX_HEADING = "## Execution Notes Appendix"
|
|
41
|
+
|
|
42
|
+
# Each note is rendered as a markdown bullet so the target model reads it as
|
|
43
|
+
# ordinary guidance.
|
|
44
|
+
_NOTE_BULLET_PREFIX = "- "
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ── Dedup helpers ───────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _canonicalize(text: str) -> str:
|
|
51
|
+
"""Normalize a note for duplicate detection (whitespace/punct/case-insensitive)."""
|
|
52
|
+
normalized = re.sub(r"\s+", " ", str(text or "").strip())
|
|
53
|
+
normalized = normalized.rstrip(" .;:,_-")
|
|
54
|
+
return normalized.casefold()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _dedupe_preserve_order(notes: list[str]) -> list[str]:
|
|
58
|
+
"""Drop blanks and near-duplicates, preserving first-seen order."""
|
|
59
|
+
seen: set[str] = set()
|
|
60
|
+
deduped: list[str] = []
|
|
61
|
+
for note in notes:
|
|
62
|
+
text = re.sub(r"\s+", " ", str(note).strip())
|
|
63
|
+
if not text:
|
|
64
|
+
continue
|
|
65
|
+
key = _canonicalize(text)
|
|
66
|
+
if not key or key in seen:
|
|
67
|
+
continue
|
|
68
|
+
seen.add(key)
|
|
69
|
+
deduped.append(text)
|
|
70
|
+
return deduped
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ── Field manipulation ──────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def has_appendix_field(skill: str) -> bool:
|
|
77
|
+
return APPENDIX_START in skill and APPENDIX_END in skill
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _render_block(notes: list[str]) -> str:
|
|
81
|
+
"""Render the full marker-delimited appendix block for *notes*."""
|
|
82
|
+
lines = [APPENDIX_START, APPENDIX_HEADING]
|
|
83
|
+
for note in notes:
|
|
84
|
+
lines.append(f"{_NOTE_BULLET_PREFIX}{note}")
|
|
85
|
+
lines.append(APPENDIX_END)
|
|
86
|
+
return "\n".join(lines)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def inject_empty_appendix_field(skill: str) -> str:
|
|
90
|
+
"""Add an empty appendix placeholder at the end of *skill* (idempotent).
|
|
91
|
+
|
|
92
|
+
Mirrors ``inject_empty_slow_update_field``: called once at skill init so the
|
|
93
|
+
protected region exists before any note is written.
|
|
94
|
+
"""
|
|
95
|
+
if has_appendix_field(skill):
|
|
96
|
+
return skill
|
|
97
|
+
block = f"\n\n{APPENDIX_START}\n{APPENDIX_HEADING}\n{APPENDIX_END}\n"
|
|
98
|
+
return skill.rstrip() + block
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def extract_appendix_notes(skill: str) -> list[str]:
|
|
102
|
+
"""Return the current appendix notes as a list of strings (no markers/heading)."""
|
|
103
|
+
start = skill.find(APPENDIX_START)
|
|
104
|
+
end = skill.find(APPENDIX_END)
|
|
105
|
+
if start == -1 or end == -1:
|
|
106
|
+
return []
|
|
107
|
+
inner = skill[start + len(APPENDIX_START):end].strip()
|
|
108
|
+
notes: list[str] = []
|
|
109
|
+
for raw_line in inner.splitlines():
|
|
110
|
+
line = raw_line.strip()
|
|
111
|
+
if not line:
|
|
112
|
+
continue
|
|
113
|
+
if line == APPENDIX_HEADING or line.lstrip("#").strip() == APPENDIX_HEADING.lstrip("#").strip():
|
|
114
|
+
continue
|
|
115
|
+
if line.startswith(_NOTE_BULLET_PREFIX):
|
|
116
|
+
line = line[len(_NOTE_BULLET_PREFIX):].strip()
|
|
117
|
+
elif line.startswith("-") or line.startswith("*"):
|
|
118
|
+
line = line[1:].strip()
|
|
119
|
+
if line:
|
|
120
|
+
notes.append(line)
|
|
121
|
+
return notes
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _strip_all_appendix_fields(skill: str) -> str:
|
|
125
|
+
"""Remove every appendix marker pair (and content between) from *skill*."""
|
|
126
|
+
while True:
|
|
127
|
+
start = skill.find(APPENDIX_START)
|
|
128
|
+
if start == -1:
|
|
129
|
+
break
|
|
130
|
+
end = skill.find(APPENDIX_END, start)
|
|
131
|
+
if end == -1:
|
|
132
|
+
skill = skill[:start] + skill[start + len(APPENDIX_START):]
|
|
133
|
+
break
|
|
134
|
+
skill = skill[:end + len(APPENDIX_END)].rsplit(APPENDIX_START, 1)[0] + skill[end + len(APPENDIX_END):]
|
|
135
|
+
skill = skill.replace(APPENDIX_END, "")
|
|
136
|
+
while "\n\n\n" in skill:
|
|
137
|
+
skill = skill.replace("\n\n\n", "\n\n")
|
|
138
|
+
return skill.rstrip()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def append_to_appendix_field(skill: str, new_notes: list[str]) -> str:
|
|
142
|
+
"""Merge *new_notes* into the appendix region (dedup), returning updated skill.
|
|
143
|
+
|
|
144
|
+
- If no appendix region exists yet, one is created.
|
|
145
|
+
- Existing notes are preserved; new ones are appended after dedup against the
|
|
146
|
+
combined set, so order is stable and duplicates are dropped.
|
|
147
|
+
- Empty / whitespace-only notes are ignored. If the merged set is empty, an
|
|
148
|
+
empty placeholder region is still ensured.
|
|
149
|
+
"""
|
|
150
|
+
incoming = _dedupe_preserve_order(list(new_notes or []))
|
|
151
|
+
existing = extract_appendix_notes(skill)
|
|
152
|
+
merged = _dedupe_preserve_order(existing + incoming)
|
|
153
|
+
|
|
154
|
+
base = _strip_all_appendix_fields(skill)
|
|
155
|
+
block = _render_block(merged)
|
|
156
|
+
return f"{base}\n\n{block}\n"
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""ReflACT gradient clipping — LLM-driven edit ranking and selection.
|
|
2
|
+
|
|
3
|
+
Analogous to gradient clipping in neural network training: ranks candidate
|
|
4
|
+
edits by importance and selects the top-L to apply, controlling the
|
|
5
|
+
effective step size. Previously core/select.py.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from skillopt.model import chat_optimizer
|
|
10
|
+
from skillopt.optimizer.meta_skill import format_meta_skill_context
|
|
11
|
+
from skillopt.optimizer.update_modes import (
|
|
12
|
+
describe_item,
|
|
13
|
+
get_payload_items,
|
|
14
|
+
is_rewrite_mode,
|
|
15
|
+
normalize_update_mode,
|
|
16
|
+
payload_key,
|
|
17
|
+
payload_label,
|
|
18
|
+
)
|
|
19
|
+
from skillopt.prompts import load_prompt
|
|
20
|
+
from skillopt.utils import extract_json
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# ── Public API ────────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
def rank_and_select(
|
|
26
|
+
skill_content: str,
|
|
27
|
+
patch: dict,
|
|
28
|
+
max_edits: int,
|
|
29
|
+
meta_skill_context: str = "",
|
|
30
|
+
update_mode: str = "patch",
|
|
31
|
+
) -> dict:
|
|
32
|
+
"""Use a optimizer LLM to rank edits by importance, then keep top-L.
|
|
33
|
+
|
|
34
|
+
If the edit pool is within budget, returns the patch unchanged.
|
|
35
|
+
Otherwise, calls the optimizer to rank and select the most impactful edits.
|
|
36
|
+
|
|
37
|
+
Parameters
|
|
38
|
+
----------
|
|
39
|
+
skill_content : str
|
|
40
|
+
Current skill document.
|
|
41
|
+
patch : dict
|
|
42
|
+
Merged :class:`~skillopt.types.Patch` dict with ``edits`` list.
|
|
43
|
+
max_edits : int
|
|
44
|
+
Maximum number of edits to keep (the "edit budget").
|
|
45
|
+
|
|
46
|
+
Returns
|
|
47
|
+
-------
|
|
48
|
+
dict
|
|
49
|
+
:class:`~skillopt.types.Patch` dict with selected edits and
|
|
50
|
+
optional ``ranking_details``.
|
|
51
|
+
"""
|
|
52
|
+
update_mode = normalize_update_mode(update_mode)
|
|
53
|
+
edits = get_payload_items(patch, update_mode)
|
|
54
|
+
if len(edits) <= max_edits:
|
|
55
|
+
return patch
|
|
56
|
+
|
|
57
|
+
# Build the edit pool description for the optimizer
|
|
58
|
+
edits_desc = []
|
|
59
|
+
for i, edit in enumerate(edits):
|
|
60
|
+
edits_desc.append(f"[{i}] {describe_item(edit, update_mode)}")
|
|
61
|
+
|
|
62
|
+
user = (
|
|
63
|
+
f"## Current Skill\n{skill_content}\n\n"
|
|
64
|
+
f"## {payload_label(update_mode, title=True)} Pool ({len(edits)} {payload_label(update_mode)}, budget={max_edits})\n"
|
|
65
|
+
+ "\n".join(edits_desc)
|
|
66
|
+
+ f"\n\nSelect the {max_edits} most important {payload_label(update_mode)}. "
|
|
67
|
+
f"Return their 0-based indices in priority order."
|
|
68
|
+
)
|
|
69
|
+
optimizer_ctx = format_meta_skill_context(meta_skill_context)
|
|
70
|
+
if optimizer_ctx:
|
|
71
|
+
user = f"{optimizer_ctx}\n\n{user}"
|
|
72
|
+
prompt_name = "ranking_rewrite" if is_rewrite_mode(update_mode) else "ranking"
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
response, _ = chat_optimizer(
|
|
76
|
+
system=load_prompt(prompt_name), user=user,
|
|
77
|
+
max_completion_tokens=16384, retries=3, stage="ranking",
|
|
78
|
+
)
|
|
79
|
+
result = extract_json(response)
|
|
80
|
+
if result and "selected_indices" in result:
|
|
81
|
+
indices = result["selected_indices"]
|
|
82
|
+
selected = []
|
|
83
|
+
seen: set[int] = set()
|
|
84
|
+
for idx in indices:
|
|
85
|
+
if (
|
|
86
|
+
isinstance(idx, int)
|
|
87
|
+
and 0 <= idx < len(edits)
|
|
88
|
+
and idx not in seen
|
|
89
|
+
):
|
|
90
|
+
selected.append(edits[idx])
|
|
91
|
+
seen.add(idx)
|
|
92
|
+
if len(selected) >= max_edits:
|
|
93
|
+
break
|
|
94
|
+
if selected:
|
|
95
|
+
return {
|
|
96
|
+
"reasoning": patch.get("reasoning", "")
|
|
97
|
+
+ f" [optimizer-ranked: selected {len(selected)}/{len(edits)} {payload_label(update_mode)}]",
|
|
98
|
+
payload_key(update_mode): selected,
|
|
99
|
+
"ranking_details": result,
|
|
100
|
+
}
|
|
101
|
+
except Exception: # noqa: BLE001
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
# Fallback: simple truncation
|
|
105
|
+
return {
|
|
106
|
+
"reasoning": patch.get("reasoning", "")
|
|
107
|
+
+ f" [fallback truncated {len(edits)}->{max_edits} {payload_label(update_mode)}]",
|
|
108
|
+
payload_key(update_mode): edits[:max_edits],
|
|
109
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Optimizer-driven autonomous update-size decisions."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import re
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from skillopt.model import chat_optimizer
|
|
9
|
+
from skillopt.optimizer.meta_skill import format_meta_skill_context
|
|
10
|
+
from skillopt.optimizer.update_modes import describe_item, get_payload_items, payload_label
|
|
11
|
+
from skillopt.prompts import load_prompt
|
|
12
|
+
from skillopt.utils import extract_json
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _coerce_nonnegative_int(value: Any) -> int | None:
|
|
16
|
+
if isinstance(value, bool):
|
|
17
|
+
return None
|
|
18
|
+
if isinstance(value, int):
|
|
19
|
+
return max(0, value)
|
|
20
|
+
if isinstance(value, float) and value.is_integer():
|
|
21
|
+
return max(0, int(value))
|
|
22
|
+
text = str(value or "").strip()
|
|
23
|
+
if not text:
|
|
24
|
+
return None
|
|
25
|
+
match = re.search(r"-?\d+", text)
|
|
26
|
+
if not match:
|
|
27
|
+
return None
|
|
28
|
+
return max(0, int(match.group(0)))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def decide_autonomous_learning_rate(
|
|
32
|
+
*,
|
|
33
|
+
skill_content: str,
|
|
34
|
+
merged_patch: dict,
|
|
35
|
+
update_mode: str,
|
|
36
|
+
rollout_hard: float,
|
|
37
|
+
rollout_soft: float,
|
|
38
|
+
rollout_n: int,
|
|
39
|
+
step_buffer_context: str = "",
|
|
40
|
+
meta_skill_context: str = "",
|
|
41
|
+
) -> dict:
|
|
42
|
+
"""Ask the optimizer to choose the number of update items for this step.
|
|
43
|
+
|
|
44
|
+
The prompt intentionally avoids default budgets, candidate budget lists, or
|
|
45
|
+
scheduler history. The only hard post-processing is validity: the returned
|
|
46
|
+
integer is clamped to the available item count.
|
|
47
|
+
"""
|
|
48
|
+
items = get_payload_items(merged_patch, update_mode)
|
|
49
|
+
available = len(items)
|
|
50
|
+
item_lines = [
|
|
51
|
+
f"[{idx}] {describe_item(item, update_mode)}"
|
|
52
|
+
for idx, item in enumerate(items)
|
|
53
|
+
]
|
|
54
|
+
user = (
|
|
55
|
+
f"## Current Skill\n{skill_content}\n\n"
|
|
56
|
+
f"## Current Step Evidence\n"
|
|
57
|
+
f"rollout_n={rollout_n}\n"
|
|
58
|
+
f"rollout_hard={rollout_hard:.6f}\n"
|
|
59
|
+
f"rollout_soft={rollout_soft:.6f}\n"
|
|
60
|
+
f"proposed_update_items={available}\n"
|
|
61
|
+
f"update_item_type={payload_label(update_mode)}\n\n"
|
|
62
|
+
f"## Proposed Update Items\n"
|
|
63
|
+
+ "\n".join(item_lines)
|
|
64
|
+
+ "\n\nDecide how many proposed update items should be applied now."
|
|
65
|
+
)
|
|
66
|
+
if step_buffer_context.strip():
|
|
67
|
+
user += f"\n\n## Previous Steps in This Epoch\n{step_buffer_context}"
|
|
68
|
+
optimizer_ctx = format_meta_skill_context(meta_skill_context)
|
|
69
|
+
if optimizer_ctx:
|
|
70
|
+
user = f"{optimizer_ctx}\n\n{user}"
|
|
71
|
+
|
|
72
|
+
response = ""
|
|
73
|
+
parsed: dict | None = None
|
|
74
|
+
decision: int | None = None
|
|
75
|
+
try:
|
|
76
|
+
response, _ = chat_optimizer(
|
|
77
|
+
system=load_prompt("lr_autonomous"),
|
|
78
|
+
user=user,
|
|
79
|
+
max_completion_tokens=16384,
|
|
80
|
+
retries=3,
|
|
81
|
+
stage="lr_autonomous",
|
|
82
|
+
)
|
|
83
|
+
parsed = extract_json(response)
|
|
84
|
+
if parsed:
|
|
85
|
+
decision = _coerce_nonnegative_int(parsed.get("learning_rate"))
|
|
86
|
+
except Exception as exc: # noqa: BLE001
|
|
87
|
+
parsed = {"error": str(exc)}
|
|
88
|
+
|
|
89
|
+
fallback = False
|
|
90
|
+
if decision is None:
|
|
91
|
+
decision = 0
|
|
92
|
+
fallback = True
|
|
93
|
+
|
|
94
|
+
chosen = min(decision, available)
|
|
95
|
+
record = {
|
|
96
|
+
"learning_rate": chosen,
|
|
97
|
+
"raw_learning_rate": decision,
|
|
98
|
+
"available_update_items": available,
|
|
99
|
+
"clamped": chosen != decision,
|
|
100
|
+
"fallback": fallback,
|
|
101
|
+
"reasoning": (parsed or {}).get("reasoning", ""),
|
|
102
|
+
"confidence": (parsed or {}).get("confidence", ""),
|
|
103
|
+
"risk_notes": (parsed or {}).get("risk_notes", []),
|
|
104
|
+
"raw_response": response,
|
|
105
|
+
}
|
|
106
|
+
if parsed and "error" in parsed:
|
|
107
|
+
record["error"] = parsed["error"]
|
|
108
|
+
return record
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Optimizer-side meta skill memory for cross-epoch optimization guidance.
|
|
2
|
+
|
|
3
|
+
This module maintains a compact optimizer-facing memory distilled from
|
|
4
|
+
adjacent-epoch skill comparisons. Unlike ``slow_update``, it does not
|
|
5
|
+
modify the target skill document. Instead, it produces guidance meant to
|
|
6
|
+
improve future optimizer behavior when proposing, merging, and ranking edits.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import traceback
|
|
11
|
+
|
|
12
|
+
from skillopt.model import chat_optimizer
|
|
13
|
+
from skillopt.optimizer.slow_update import format_comparison_text
|
|
14
|
+
from skillopt.prompts import load_prompt
|
|
15
|
+
from skillopt.utils import extract_json
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def format_meta_skill_context(meta_skill_content: str) -> str:
|
|
19
|
+
"""Render optimizer memory into a prompt-ready context block."""
|
|
20
|
+
content = (meta_skill_content or "").strip()
|
|
21
|
+
if not content:
|
|
22
|
+
return ""
|
|
23
|
+
return (
|
|
24
|
+
"## Optimizer Meta Skill\n"
|
|
25
|
+
"This is optimizer-side memory distilled from prior epoch transitions in "
|
|
26
|
+
"this environment. Use it to improve how you propose, merge, and rank "
|
|
27
|
+
"skill edits. Prefer it when the current evidence is ambiguous, but do "
|
|
28
|
+
"not force it if the current trajectories clearly contradict it.\n\n"
|
|
29
|
+
f"{content}"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def run_meta_skill(
|
|
34
|
+
prev_skill: str,
|
|
35
|
+
curr_skill: str,
|
|
36
|
+
comparison_pairs: list[dict],
|
|
37
|
+
*,
|
|
38
|
+
prev_meta_skill_content: str = "",
|
|
39
|
+
system_prompt: str | None = None,
|
|
40
|
+
) -> dict | None:
|
|
41
|
+
"""Produce updated optimizer-side meta skill from adjacent epochs."""
|
|
42
|
+
actual_system = system_prompt if system_prompt is not None else load_prompt("meta_skill")
|
|
43
|
+
|
|
44
|
+
prev_meta_section = (
|
|
45
|
+
prev_meta_skill_content.strip()
|
|
46
|
+
if prev_meta_skill_content and prev_meta_skill_content.strip()
|
|
47
|
+
else "(No previous optimizer meta skill — this is the first update.)"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
comparison_text = format_comparison_text(comparison_pairs)
|
|
51
|
+
user = (
|
|
52
|
+
f"## Previous Epoch Last-Step Skill\n{prev_skill}\n\n"
|
|
53
|
+
f"## Current Epoch Last-Step Skill\n{curr_skill}\n\n"
|
|
54
|
+
f"## Previous Optimizer Meta Skill\n"
|
|
55
|
+
f"The following optimizer memory was available during the current epoch. "
|
|
56
|
+
f"Reflect on whether it improved or harmed the quality of edits.\n\n"
|
|
57
|
+
f"{prev_meta_section}\n\n"
|
|
58
|
+
f"## Longitudinal Comparison (same tasks, two last-step skills)\n"
|
|
59
|
+
f"{comparison_text}"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
response, _ = chat_optimizer(
|
|
64
|
+
system=actual_system,
|
|
65
|
+
user=user,
|
|
66
|
+
max_completion_tokens=16384,
|
|
67
|
+
retries=3,
|
|
68
|
+
stage="meta_skill",
|
|
69
|
+
)
|
|
70
|
+
result = extract_json(response)
|
|
71
|
+
if result and result.get("meta_skill_content"):
|
|
72
|
+
return {
|
|
73
|
+
"reasoning": str(result.get("reasoning", "")).strip(),
|
|
74
|
+
"meta_skill_content": str(result["meta_skill_content"]).strip(),
|
|
75
|
+
}
|
|
76
|
+
except Exception: # noqa: BLE001
|
|
77
|
+
traceback.print_exc()
|
|
78
|
+
|
|
79
|
+
return None
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Optimizer-driven full skill rewrite from selected revise_suggestions."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
from skillopt.model import chat_optimizer
|
|
7
|
+
from skillopt.prompts import load_prompt
|
|
8
|
+
from skillopt.optimizer.update_modes import get_payload_items
|
|
9
|
+
from skillopt.utils import extract_json
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def rewrite_skill_from_suggestions(
|
|
13
|
+
skill_content: str,
|
|
14
|
+
patch: dict,
|
|
15
|
+
*,
|
|
16
|
+
system_prompt: str | None = None,
|
|
17
|
+
step_buffer_context: str = "",
|
|
18
|
+
env: str | None = None,
|
|
19
|
+
reasoning_effort: str | None = "high",
|
|
20
|
+
max_completion_tokens: int = 64000,
|
|
21
|
+
) -> dict | None:
|
|
22
|
+
suggestions = get_payload_items(patch, "rewrite_from_suggestions")
|
|
23
|
+
if not suggestions:
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
user = (
|
|
27
|
+
f"## Current Skill\n{skill_content}\n\n"
|
|
28
|
+
f"## Selected Revise Suggestions ({len(suggestions)} total)\n"
|
|
29
|
+
f"{json.dumps(suggestions, ensure_ascii=False, indent=2)}\n\n"
|
|
30
|
+
)
|
|
31
|
+
if step_buffer_context.strip():
|
|
32
|
+
user += f"## Previous Steps in This Epoch\n{step_buffer_context}\n\n"
|
|
33
|
+
user += (
|
|
34
|
+
"Rewrite the full skill document so it integrates the selected suggestions. "
|
|
35
|
+
"Return the complete new skill in `new_skill`."
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
actual_system = system_prompt if system_prompt is not None else load_prompt(
|
|
39
|
+
"rewrite_skill", env=env,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
response, _ = chat_optimizer(
|
|
44
|
+
system=actual_system,
|
|
45
|
+
user=user,
|
|
46
|
+
max_completion_tokens=max_completion_tokens,
|
|
47
|
+
retries=3,
|
|
48
|
+
stage="rewrite",
|
|
49
|
+
reasoning_effort=reasoning_effort,
|
|
50
|
+
)
|
|
51
|
+
result = extract_json(response)
|
|
52
|
+
if result and str(result.get("new_skill", "")).strip():
|
|
53
|
+
result["new_skill"] = str(result["new_skill"]).rstrip() + "\n"
|
|
54
|
+
if "change_summary" not in result or not isinstance(result["change_summary"], list):
|
|
55
|
+
result["change_summary"] = []
|
|
56
|
+
return result
|
|
57
|
+
except Exception: # noqa: BLE001
|
|
58
|
+
return None
|
|
59
|
+
return None
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Learning-rate (edit budget) schedulers for ReflACT.
|
|
2
|
+
|
|
3
|
+
The "learning rate" in ReflACT is the maximum number of skill edits allowed
|
|
4
|
+
per optimization step. A scheduler controls how this budget changes over
|
|
5
|
+
the course of training.
|
|
6
|
+
|
|
7
|
+
Supported modes
|
|
8
|
+
---------------
|
|
9
|
+
- ``constant`` : Fixed budget throughout training.
|
|
10
|
+
- ``linear`` : Linear decay from ``max_lr`` to ``min_lr``.
|
|
11
|
+
- ``cosine`` : Cosine annealing from ``max_lr`` to ``min_lr``.
|
|
12
|
+
- ``autonomous`` : No limit — the model decides how many edits to make.
|
|
13
|
+
|
|
14
|
+
Usage::
|
|
15
|
+
|
|
16
|
+
scheduler = build_scheduler(cfg)
|
|
17
|
+
for step in range(1, total_steps + 1):
|
|
18
|
+
lr = scheduler.step() # returns edit budget for this step
|
|
19
|
+
# ... use lr as max_edits ...
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import math
|
|
24
|
+
from abc import ABC, abstractmethod
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class LRScheduler(ABC):
|
|
28
|
+
"""Base class for edit-budget schedulers."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, max_lr: int, min_lr: int, total_steps: int) -> None:
|
|
31
|
+
self.max_lr = max_lr
|
|
32
|
+
self.min_lr = min_lr
|
|
33
|
+
self.total_steps = total_steps
|
|
34
|
+
self._current_step = 0
|
|
35
|
+
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def _compute_lr(self, step: int) -> int:
|
|
38
|
+
"""Return the edit budget for the given 1-indexed step."""
|
|
39
|
+
|
|
40
|
+
def step(self) -> int:
|
|
41
|
+
"""Advance one step and return the edit budget."""
|
|
42
|
+
self._current_step += 1
|
|
43
|
+
return self._compute_lr(self._current_step)
|
|
44
|
+
|
|
45
|
+
def get_lr(self, step: int) -> int:
|
|
46
|
+
"""Return the edit budget for an arbitrary step (1-indexed)."""
|
|
47
|
+
return self._compute_lr(step)
|
|
48
|
+
|
|
49
|
+
def state_dict(self) -> dict:
|
|
50
|
+
return {"current_step": self._current_step}
|
|
51
|
+
|
|
52
|
+
def load_state_dict(self, state: dict) -> None:
|
|
53
|
+
self._current_step = state.get("current_step", 0)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class ConstantScheduler(LRScheduler):
|
|
57
|
+
"""Fixed edit budget throughout training."""
|
|
58
|
+
|
|
59
|
+
def _compute_lr(self, step: int) -> int:
|
|
60
|
+
return self.max_lr
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class LinearScheduler(LRScheduler):
|
|
64
|
+
"""Linear decay from ``max_lr`` to ``min_lr`` over ``total_steps``."""
|
|
65
|
+
|
|
66
|
+
def _compute_lr(self, step: int) -> int:
|
|
67
|
+
if self.total_steps <= 1:
|
|
68
|
+
return self.max_lr
|
|
69
|
+
t = min(step, self.total_steps) / self.total_steps
|
|
70
|
+
lr = self.max_lr + (self.min_lr - self.max_lr) * t
|
|
71
|
+
return max(self.min_lr, round(lr))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class CosineScheduler(LRScheduler):
|
|
75
|
+
"""Cosine annealing from ``max_lr`` to ``min_lr`` over ``total_steps``."""
|
|
76
|
+
|
|
77
|
+
def _compute_lr(self, step: int) -> int:
|
|
78
|
+
if self.total_steps <= 1:
|
|
79
|
+
return self.max_lr
|
|
80
|
+
t = min(step, self.total_steps) / self.total_steps
|
|
81
|
+
lr = self.min_lr + 0.5 * (self.max_lr - self.min_lr) * (1 + math.cos(math.pi * t))
|
|
82
|
+
return max(self.min_lr, round(lr))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class AutonomousScheduler(LRScheduler):
|
|
86
|
+
"""No edit limit — the model decides freely."""
|
|
87
|
+
|
|
88
|
+
NO_LIMIT = 999
|
|
89
|
+
|
|
90
|
+
def _compute_lr(self, step: int) -> int:
|
|
91
|
+
return self.NO_LIMIT
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# ── Factory ──────────────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
_REGISTRY: dict[str, type[LRScheduler]] = {
|
|
97
|
+
"constant": ConstantScheduler,
|
|
98
|
+
"linear": LinearScheduler,
|
|
99
|
+
"cosine": CosineScheduler,
|
|
100
|
+
"autonomous": AutonomousScheduler,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def build_scheduler(
|
|
105
|
+
mode: str = "constant",
|
|
106
|
+
max_lr: int = 8,
|
|
107
|
+
min_lr: int = 2,
|
|
108
|
+
total_steps: int = 8,
|
|
109
|
+
) -> LRScheduler:
|
|
110
|
+
"""Build a scheduler from config parameters.
|
|
111
|
+
|
|
112
|
+
Parameters
|
|
113
|
+
----------
|
|
114
|
+
mode : str
|
|
115
|
+
One of ``constant``, ``linear``, ``cosine``, ``autonomous``.
|
|
116
|
+
max_lr : int
|
|
117
|
+
Initial / maximum edit budget.
|
|
118
|
+
min_lr : int
|
|
119
|
+
Minimum edit budget (for decay modes).
|
|
120
|
+
total_steps : int
|
|
121
|
+
Total number of optimization steps in training.
|
|
122
|
+
"""
|
|
123
|
+
if mode not in _REGISTRY:
|
|
124
|
+
raise ValueError(
|
|
125
|
+
f"Unknown scheduler mode '{mode}'. Available: {list(_REGISTRY.keys())}"
|
|
126
|
+
)
|
|
127
|
+
return _REGISTRY[mode](max_lr=max_lr, min_lr=min_lr, total_steps=total_steps)
|