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.
Files changed (149) hide show
  1. package/package.json +1 -1
  2. package/template/skills/skillopt-sleep/SKILL.md +42 -0
  3. package/template/skills/skillopt-sleep/configs/_base_/default.yaml +103 -0
  4. package/template/skills/skillopt-sleep/configs/alfworld/default.yaml +29 -0
  5. package/template/skills/skillopt-sleep/configs/docvqa/default.yaml +28 -0
  6. package/template/skills/skillopt-sleep/configs/features/soft_gate.yaml +47 -0
  7. package/template/skills/skillopt-sleep/configs/livemathematicianbench/default.yaml +22 -0
  8. package/template/skills/skillopt-sleep/configs/officeqa/default.yaml +34 -0
  9. package/template/skills/skillopt-sleep/configs/searchqa/default.yaml +32 -0
  10. package/template/skills/skillopt-sleep/configs/spreadsheetbench/default.yaml +34 -0
  11. package/template/skills/skillopt-sleep/scripts/framework/skillopt/__init__.py +28 -0
  12. package/template/skills/skillopt-sleep/scripts/framework/skillopt/config.py +282 -0
  13. package/template/skills/skillopt-sleep/scripts/framework/skillopt/datasets/__init__.py +7 -0
  14. package/template/skills/skillopt-sleep/scripts/framework/skillopt/datasets/base.py +512 -0
  15. package/template/skills/skillopt-sleep/scripts/framework/skillopt/engine/__init__.py +9 -0
  16. package/template/skills/skillopt-sleep/scripts/framework/skillopt/engine/trainer.py +2379 -0
  17. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/__init__.py +1 -0
  18. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/_template/README.md +43 -0
  19. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/_template/config_template.yaml +55 -0
  20. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/_template/env_template.py +151 -0
  21. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/_template/loader_template.py +87 -0
  22. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/__init__.py +5 -0
  23. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/adapter.py +428 -0
  24. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/dataloader.py +123 -0
  25. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/prompts/analyst_error.md +55 -0
  26. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/prompts/analyst_success.md +33 -0
  27. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/prompts/rollout_no_history.md +8 -0
  28. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/prompts/rollout_with_history.md +9 -0
  29. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/prompts/rollout_with_memory.md +16 -0
  30. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/reflect.py +4 -0
  31. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/rollout.py +366 -0
  32. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/skills/initial.md +45 -0
  33. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/__init__.py +9 -0
  34. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/alfworld_envs.py +221 -0
  35. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/alfworld_projection.py +60 -0
  36. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/alfworld_prompts.py +8 -0
  37. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/config_tw.yaml +145 -0
  38. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/env_base.py +84 -0
  39. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/env_manager.py +139 -0
  40. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/memory.py +87 -0
  41. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/base.py +329 -0
  42. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/__init__.py +1 -0
  43. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/adapter.py +90 -0
  44. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/dataloader.py +61 -0
  45. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/evaluator.py +113 -0
  46. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/prompts/analyst_error.md +35 -0
  47. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/prompts/analyst_success.md +24 -0
  48. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/prompts/rollout_system.md +12 -0
  49. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/rollout.py +391 -0
  50. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/skills/initial.md +11 -0
  51. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/__init__.py +1 -0
  52. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/adapter.py +129 -0
  53. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/dataloader.py +308 -0
  54. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/evaluator.py +62 -0
  55. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/prompts/analyst_error.md +37 -0
  56. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/prompts/analyst_success.md +25 -0
  57. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/prompts/rollout_system.md +12 -0
  58. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/reflect.py +4 -0
  59. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/rollout.py +434 -0
  60. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/skills/initial.md +16 -0
  61. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/__init__.py +1 -0
  62. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/adapter.py +112 -0
  63. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/dataloader.py +71 -0
  64. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/evaluator.py +46 -0
  65. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/prompts/analyst_error.md +37 -0
  66. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/prompts/analyst_success.md +25 -0
  67. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/prompts/rollout_system.md +15 -0
  68. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/rollout.py +799 -0
  69. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/skills/initial.md +15 -0
  70. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/tool_runtime.py +552 -0
  71. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/__init__.py +1 -0
  72. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/adapter.py +96 -0
  73. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/dataloader.py +42 -0
  74. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/evaluator.py +100 -0
  75. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/prompts/analyst_error.md +46 -0
  76. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/prompts/analyst_success.md +32 -0
  77. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/prompts/rollout_system.md +13 -0
  78. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/reflect.py +4 -0
  79. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/rollout.py +494 -0
  80. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/skills/initial.md +3 -0
  81. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/__init__.py +5 -0
  82. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/adapter.py +159 -0
  83. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/codegen_agent.py +731 -0
  84. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/dataloader.py +37 -0
  85. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/evaluator.py +158 -0
  86. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/executor.py +67 -0
  87. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/prompts/analyst_error.md +46 -0
  88. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/prompts/analyst_success.md +32 -0
  89. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/prompts/codegen_system.md +1 -0
  90. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/prompts/critical_rules.md +9 -0
  91. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/prompts/react_system.md +21 -0
  92. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/react_agent.py +395 -0
  93. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/reflect.py +4 -0
  94. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/rollout.py +979 -0
  95. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/skills/initial.md +56 -0
  96. package/template/skills/skillopt-sleep/scripts/framework/skillopt/evaluation/__init__.py +13 -0
  97. package/template/skills/skillopt-sleep/scripts/framework/skillopt/evaluation/gate.py +148 -0
  98. package/template/skills/skillopt-sleep/scripts/framework/skillopt/gradient/__init__.py +15 -0
  99. package/template/skills/skillopt-sleep/scripts/framework/skillopt/gradient/aggregate.py +253 -0
  100. package/template/skills/skillopt-sleep/scripts/framework/skillopt/gradient/reflect.py +635 -0
  101. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/__init__.py +514 -0
  102. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/azure_openai.py +915 -0
  103. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/backend_config.py +185 -0
  104. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/claude_backend.py +371 -0
  105. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/codex_backend.py +666 -0
  106. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/codex_harness.py +1057 -0
  107. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/common.py +229 -0
  108. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/minimax_backend.py +277 -0
  109. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/qwen_backend.py +456 -0
  110. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/router.py +236 -0
  111. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/__init__.py +15 -0
  112. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/appendix.py +156 -0
  113. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/clip.py +109 -0
  114. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/lr_autonomous.py +108 -0
  115. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/meta_skill.py +79 -0
  116. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/rewrite.py +59 -0
  117. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/scheduler.py +127 -0
  118. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/select.py +4 -0
  119. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/skill.py +201 -0
  120. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/skill_aware.py +206 -0
  121. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/slow_update.py +396 -0
  122. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/update_modes.py +135 -0
  123. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/__init__.py +63 -0
  124. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_error.md +41 -0
  125. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_error_full_rewrite.md +32 -0
  126. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_error_rewrite.md +44 -0
  127. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_success.md +36 -0
  128. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_success_full_rewrite.md +30 -0
  129. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_success_rewrite.md +33 -0
  130. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/lr_autonomous.md +20 -0
  131. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_failure.md +30 -0
  132. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_failure_full_rewrite.md +28 -0
  133. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_failure_rewrite.md +26 -0
  134. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_final.md +33 -0
  135. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_final_full_rewrite.md +28 -0
  136. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_final_rewrite.md +25 -0
  137. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_success.md +28 -0
  138. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_success_full_rewrite.md +28 -0
  139. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_success_rewrite.md +25 -0
  140. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/meta_skill.md +40 -0
  141. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/ranking.md +20 -0
  142. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/ranking_rewrite.md +15 -0
  143. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/rewrite_skill.md +25 -0
  144. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/slow_update.md +60 -0
  145. package/template/skills/skillopt-sleep/scripts/framework/skillopt/scheduler/__init__.py +8 -0
  146. package/template/skills/skillopt-sleep/scripts/framework/skillopt/types.py +306 -0
  147. package/template/skills/skillopt-sleep/scripts/framework/skillopt/utils/__init__.py +4 -0
  148. package/template/skills/skillopt-sleep/scripts/framework/skillopt/utils/json_utils.py +172 -0
  149. package/template/skills/skillopt-sleep/scripts/framework/skillopt/utils/scoring.py +28 -0
@@ -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)
@@ -0,0 +1,4 @@
1
+ """Backward-compat stub — moved to skillopt.optimizer.clip."""
2
+ from skillopt.optimizer.clip import rank_and_select # noqa: F401
3
+
4
+ __all__ = ["rank_and_select"]