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.
Files changed (171) hide show
  1. package/package.json +1 -1
  2. package/template/rules/ponytail.mdc +98 -23
  3. package/template/skills/skillopt-sleep/SKILL.md +42 -0
  4. package/template/skills/skillopt-sleep/configs/_base_/default.yaml +103 -0
  5. package/template/skills/skillopt-sleep/configs/alfworld/default.yaml +29 -0
  6. package/template/skills/skillopt-sleep/configs/docvqa/default.yaml +28 -0
  7. package/template/skills/skillopt-sleep/configs/features/soft_gate.yaml +47 -0
  8. package/template/skills/skillopt-sleep/configs/livemathematicianbench/default.yaml +22 -0
  9. package/template/skills/skillopt-sleep/configs/officeqa/default.yaml +34 -0
  10. package/template/skills/skillopt-sleep/configs/searchqa/default.yaml +32 -0
  11. package/template/skills/skillopt-sleep/configs/spreadsheetbench/default.yaml +34 -0
  12. package/template/skills/skillopt-sleep/scripts/framework/skillopt/__init__.py +28 -0
  13. package/template/skills/skillopt-sleep/scripts/framework/skillopt/config.py +282 -0
  14. package/template/skills/skillopt-sleep/scripts/framework/skillopt/datasets/__init__.py +7 -0
  15. package/template/skills/skillopt-sleep/scripts/framework/skillopt/datasets/base.py +512 -0
  16. package/template/skills/skillopt-sleep/scripts/framework/skillopt/engine/__init__.py +9 -0
  17. package/template/skills/skillopt-sleep/scripts/framework/skillopt/engine/trainer.py +2379 -0
  18. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/__init__.py +1 -0
  19. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/_template/README.md +43 -0
  20. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/_template/config_template.yaml +55 -0
  21. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/_template/env_template.py +151 -0
  22. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/_template/loader_template.py +87 -0
  23. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/__init__.py +5 -0
  24. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/adapter.py +428 -0
  25. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/dataloader.py +123 -0
  26. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/prompts/analyst_error.md +55 -0
  27. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/prompts/analyst_success.md +33 -0
  28. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/prompts/rollout_no_history.md +8 -0
  29. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/prompts/rollout_with_history.md +9 -0
  30. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/prompts/rollout_with_memory.md +16 -0
  31. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/reflect.py +4 -0
  32. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/rollout.py +366 -0
  33. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/skills/initial.md +45 -0
  34. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/__init__.py +9 -0
  35. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/alfworld_envs.py +221 -0
  36. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/alfworld_projection.py +60 -0
  37. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/alfworld_prompts.py +8 -0
  38. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/config_tw.yaml +145 -0
  39. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/env_base.py +84 -0
  40. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/env_manager.py +139 -0
  41. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/memory.py +87 -0
  42. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/base.py +329 -0
  43. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/__init__.py +1 -0
  44. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/adapter.py +90 -0
  45. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/dataloader.py +61 -0
  46. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/evaluator.py +113 -0
  47. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/prompts/analyst_error.md +35 -0
  48. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/prompts/analyst_success.md +24 -0
  49. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/prompts/rollout_system.md +12 -0
  50. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/rollout.py +391 -0
  51. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/skills/initial.md +11 -0
  52. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/__init__.py +1 -0
  53. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/adapter.py +129 -0
  54. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/dataloader.py +308 -0
  55. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/evaluator.py +62 -0
  56. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/prompts/analyst_error.md +37 -0
  57. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/prompts/analyst_success.md +25 -0
  58. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/prompts/rollout_system.md +12 -0
  59. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/reflect.py +4 -0
  60. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/rollout.py +434 -0
  61. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/skills/initial.md +16 -0
  62. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/__init__.py +1 -0
  63. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/adapter.py +112 -0
  64. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/dataloader.py +71 -0
  65. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/evaluator.py +46 -0
  66. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/prompts/analyst_error.md +37 -0
  67. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/prompts/analyst_success.md +25 -0
  68. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/prompts/rollout_system.md +15 -0
  69. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/rollout.py +799 -0
  70. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/skills/initial.md +15 -0
  71. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/tool_runtime.py +552 -0
  72. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/__init__.py +1 -0
  73. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/adapter.py +96 -0
  74. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/dataloader.py +42 -0
  75. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/evaluator.py +100 -0
  76. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/prompts/analyst_error.md +46 -0
  77. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/prompts/analyst_success.md +32 -0
  78. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/prompts/rollout_system.md +13 -0
  79. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/reflect.py +4 -0
  80. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/rollout.py +494 -0
  81. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/skills/initial.md +3 -0
  82. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/__init__.py +5 -0
  83. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/adapter.py +159 -0
  84. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/codegen_agent.py +731 -0
  85. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/dataloader.py +37 -0
  86. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/evaluator.py +158 -0
  87. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/executor.py +67 -0
  88. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/prompts/analyst_error.md +46 -0
  89. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/prompts/analyst_success.md +32 -0
  90. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/prompts/codegen_system.md +1 -0
  91. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/prompts/critical_rules.md +9 -0
  92. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/prompts/react_system.md +21 -0
  93. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/react_agent.py +395 -0
  94. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/reflect.py +4 -0
  95. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/rollout.py +979 -0
  96. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/skills/initial.md +56 -0
  97. package/template/skills/skillopt-sleep/scripts/framework/skillopt/evaluation/__init__.py +13 -0
  98. package/template/skills/skillopt-sleep/scripts/framework/skillopt/evaluation/gate.py +148 -0
  99. package/template/skills/skillopt-sleep/scripts/framework/skillopt/gradient/__init__.py +15 -0
  100. package/template/skills/skillopt-sleep/scripts/framework/skillopt/gradient/aggregate.py +253 -0
  101. package/template/skills/skillopt-sleep/scripts/framework/skillopt/gradient/reflect.py +635 -0
  102. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/__init__.py +514 -0
  103. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/azure_openai.py +915 -0
  104. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/backend_config.py +185 -0
  105. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/claude_backend.py +371 -0
  106. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/codex_backend.py +666 -0
  107. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/codex_harness.py +1057 -0
  108. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/common.py +229 -0
  109. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/minimax_backend.py +277 -0
  110. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/qwen_backend.py +456 -0
  111. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/router.py +236 -0
  112. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/__init__.py +15 -0
  113. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/appendix.py +156 -0
  114. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/clip.py +109 -0
  115. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/lr_autonomous.py +108 -0
  116. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/meta_skill.py +79 -0
  117. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/rewrite.py +59 -0
  118. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/scheduler.py +127 -0
  119. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/select.py +4 -0
  120. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/skill.py +201 -0
  121. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/skill_aware.py +206 -0
  122. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/slow_update.py +396 -0
  123. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/update_modes.py +135 -0
  124. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/__init__.py +63 -0
  125. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_error.md +41 -0
  126. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_error_full_rewrite.md +32 -0
  127. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_error_rewrite.md +44 -0
  128. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_success.md +36 -0
  129. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_success_full_rewrite.md +30 -0
  130. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_success_rewrite.md +33 -0
  131. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/lr_autonomous.md +20 -0
  132. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_failure.md +30 -0
  133. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_failure_full_rewrite.md +28 -0
  134. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_failure_rewrite.md +26 -0
  135. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_final.md +33 -0
  136. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_final_full_rewrite.md +28 -0
  137. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_final_rewrite.md +25 -0
  138. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_success.md +28 -0
  139. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_success_full_rewrite.md +28 -0
  140. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_success_rewrite.md +25 -0
  141. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/meta_skill.md +40 -0
  142. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/ranking.md +20 -0
  143. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/ranking_rewrite.md +15 -0
  144. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/rewrite_skill.md +25 -0
  145. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/slow_update.md +60 -0
  146. package/template/skills/skillopt-sleep/scripts/framework/skillopt/scheduler/__init__.py +8 -0
  147. package/template/skills/skillopt-sleep/scripts/framework/skillopt/types.py +306 -0
  148. package/template/skills/skillopt-sleep/scripts/framework/skillopt/utils/__init__.py +4 -0
  149. package/template/skills/skillopt-sleep/scripts/framework/skillopt/utils/json_utils.py +172 -0
  150. package/template/skills/skillopt-sleep/scripts/framework/skillopt/utils/scoring.py +28 -0
  151. package/template/skills/ponytail/SKILL.md +0 -133
  152. package/template/skills/ponytail/scripts/hooks/claude-codex-hooks.json +0 -44
  153. package/template/skills/ponytail/scripts/hooks/copilot-hooks.json +0 -21
  154. package/template/skills/ponytail/scripts/hooks/ponytail-activate.js +0 -91
  155. package/template/skills/ponytail/scripts/hooks/ponytail-config.js +0 -122
  156. package/template/skills/ponytail/scripts/hooks/ponytail-instructions.js +0 -94
  157. package/template/skills/ponytail/scripts/hooks/ponytail-mode-tracker.js +0 -55
  158. package/template/skills/ponytail/scripts/hooks/ponytail-runtime.js +0 -68
  159. package/template/skills/ponytail/scripts/hooks/ponytail-statusline.ps1 +0 -21
  160. package/template/skills/ponytail/scripts/hooks/ponytail-statusline.sh +0 -12
  161. package/template/skills/ponytail/scripts/hooks/ponytail-subagent.js +0 -22
  162. package/template/skills/ponytail/scripts/mcp/README.md +0 -46
  163. package/template/skills/ponytail/scripts/mcp/index.js +0 -48
  164. package/template/skills/ponytail/scripts/mcp/instructions.js +0 -26
  165. package/template/skills/ponytail/scripts/mcp/package.json +0 -13
  166. package/template/skills/ponytail/scripts/mcp/test/instructions.test.js +0 -22
  167. package/template/skills/ponytail-audit/SKILL.md +0 -41
  168. package/template/skills/ponytail-debt/SKILL.md +0 -44
  169. package/template/skills/ponytail-gain/SKILL.md +0 -50
  170. package/template/skills/ponytail-help/SKILL.md +0 -69
  171. package/template/skills/ponytail-review/SKILL.md +0 -57
@@ -0,0 +1,396 @@
1
+ """ReflACT Slow Update — epoch-level longitudinal skill refinement.
2
+
3
+ At the end of each epoch, the slow update compares rollout performance of the
4
+ same sample set under the previous epoch's skill vs. the current epoch's skill
5
+ (Markov: only adjacent epochs). A optimizer analyzes regressions, improvements,
6
+ and persistent failures, then writes a free-form guidance block into a
7
+ **protected** section of the skill document. This section cannot be modified by
8
+ step-level analyst edits — only the slow update process overwrites it.
9
+
10
+ Public API
11
+ ----------
12
+ - :func:`inject_empty_slow_update_field` — add empty placeholder (epoch 1)
13
+ - :func:`extract_slow_update_field` — read current content
14
+ - :func:`replace_slow_update_field` — overwrite content
15
+ - :func:`has_slow_update_field` — check if markers are present
16
+ - :func:`build_comparison_text` — format side-by-side rollout results
17
+ - :func:`run_slow_update` — optimizer call to produce guidance
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ import os
23
+ import traceback
24
+
25
+ from skillopt.model import chat_optimizer
26
+ from skillopt.prompts import load_prompt
27
+ from skillopt.utils import extract_json
28
+
29
+ # ── Protected field markers ─────────────────────────────────────────────────
30
+
31
+ SLOW_UPDATE_START = "<!-- SLOW_UPDATE_START -->"
32
+ SLOW_UPDATE_END = "<!-- SLOW_UPDATE_END -->"
33
+
34
+ # ── Field manipulation helpers ──────────────────────────────────────────────
35
+
36
+
37
+ def has_slow_update_field(skill: str) -> bool:
38
+ return SLOW_UPDATE_START in skill and SLOW_UPDATE_END in skill
39
+
40
+
41
+ def inject_empty_slow_update_field(skill: str) -> str:
42
+ if has_slow_update_field(skill):
43
+ return skill
44
+ block = (
45
+ f"\n\n{SLOW_UPDATE_START}\n"
46
+ f"{SLOW_UPDATE_END}\n"
47
+ )
48
+ return skill.rstrip() + block
49
+
50
+
51
+ def extract_slow_update_field(skill: str) -> str:
52
+ start = skill.find(SLOW_UPDATE_START)
53
+ end = skill.find(SLOW_UPDATE_END)
54
+ if start == -1 or end == -1:
55
+ return ""
56
+ inner_start = start + len(SLOW_UPDATE_START)
57
+ return skill[inner_start:end].strip()
58
+
59
+
60
+ def _strip_all_slow_update_fields(skill: str) -> str:
61
+ """Remove every SLOW_UPDATE_START/END pair (and content between) from *skill*."""
62
+ while True:
63
+ start = skill.find(SLOW_UPDATE_START)
64
+ if start == -1:
65
+ break
66
+ end = skill.find(SLOW_UPDATE_END, start)
67
+ if end == -1:
68
+ # Orphan start marker — remove it
69
+ skill = skill[:start] + skill[start + len(SLOW_UPDATE_START):]
70
+ break
71
+ skill = skill[:start] + skill[end + len(SLOW_UPDATE_END):]
72
+ # Clean up stray end markers
73
+ skill = skill.replace(SLOW_UPDATE_END, "")
74
+ # Collapse excess blank lines left behind
75
+ while "\n\n\n" in skill:
76
+ skill = skill.replace("\n\n\n", "\n\n")
77
+ return skill.rstrip()
78
+
79
+
80
+ def replace_slow_update_field(skill: str, new_content: str) -> str:
81
+ # Remove all existing slow update regions first to guarantee exactly one.
82
+ skill = _strip_all_slow_update_fields(skill)
83
+ block = (
84
+ f"\n\n{SLOW_UPDATE_START}\n"
85
+ f"{new_content.strip()}\n"
86
+ f"{SLOW_UPDATE_END}\n"
87
+ )
88
+ return skill + block
89
+
90
+
91
+ # ── Comparison text builder ─────────────────────────────────────────────────
92
+
93
+
94
+ # NOTE: Character-length limits on the comparison samples fed to the slow-update /
95
+ # meta-skill optimizer have been REMOVED. Previously a whole-trajectory cap plus
96
+ # per-field caps (cmd/obs/reasoning/etc.) and comparison-metadata caps
97
+ # (task/answer/fail_reason) trimmed this context to save optimizer tokens and
98
+ # speed up the call. They never affected what gets written into the skill — only
99
+ # how much longitudinal context the optimizer sees. We now pass everything through
100
+ # at full length: the comparison input is as long as the source data is.
101
+
102
+
103
+ def _clip_text(value, limit: int | None = None) -> str:
104
+ # Truncation disabled: return the full text. The `limit` argument is kept only
105
+ # for call-site compatibility and is intentionally ignored (see NOTE above).
106
+ if value is None:
107
+ return ""
108
+ return str(value)
109
+
110
+
111
+ def _read_trajectory(rollout_dir: str, task_id: str) -> str:
112
+ """Read and format a single trajectory from a rollout directory."""
113
+ conv_path = os.path.join(rollout_dir, "predictions", task_id, "conversation.json")
114
+ if not os.path.exists(conv_path):
115
+ return "(trajectory not available)"
116
+ try:
117
+ with open(conv_path) as f:
118
+ conversation = json.load(f)
119
+ except Exception:
120
+ return "(trajectory read error)"
121
+ if not conversation:
122
+ return "(empty trajectory)"
123
+
124
+ lines: list[str] = []
125
+ for entry in conversation:
126
+ if not isinstance(entry, dict):
127
+ continue
128
+ # Per-field truncation removed: feed each step's full cmd/obs/reasoning/
129
+ # action/feedback/content (see NOTE above).
130
+ if entry.get("type") == "tool_call":
131
+ cmd = _clip_text(entry.get("cmd"))
132
+ obs = _clip_text(entry.get("obs"))
133
+ lines.append(f"[action] {cmd}")
134
+ lines.append(f"[obs] {obs}")
135
+ elif "action" in entry and "env_feedback" in entry:
136
+ step = entry.get("step", "?")
137
+ reasoning = _clip_text(entry.get("reasoning"))
138
+ action = _clip_text(entry.get("action"))
139
+ feedback = _clip_text(entry.get("env_feedback"))
140
+ if reasoning:
141
+ lines.append(f"[step {step} think] {reasoning}")
142
+ lines.append(f"[step {step} action] {action}")
143
+ lines.append(f"[step {step} obs] {feedback}")
144
+ elif entry.get("role") == "system":
145
+ msg = _clip_text(entry.get("content"))
146
+ lines.append(f"[verification] {msg}")
147
+ else:
148
+ msg = _clip_text(entry.get("content"))
149
+ role = entry.get("role", "agent")
150
+ lines.append(f"[{role}] {msg}")
151
+
152
+ # Whole-trajectory truncation removed: return the full formatted trajectory.
153
+ return "\n".join(lines)
154
+
155
+
156
+ # ── Structured comparison pairs ─────────────────────────────────────────────
157
+
158
+
159
+ def build_comparison_pairs(
160
+ results_prev: list[dict],
161
+ results_curr: list[dict],
162
+ items: list[dict],
163
+ prev_rollout_dir: str = "",
164
+ curr_rollout_dir: str = "",
165
+ ) -> list[dict]:
166
+ """Build a structured list of per-sample comparison entries.
167
+
168
+ Each entry bundles the original item, both rollout results, the change
169
+ category, and both trajectories into one dict — the single source of
170
+ truth for this sample's longitudinal comparison.
171
+
172
+ Returns
173
+ -------
174
+ list[dict]
175
+ One dict per sample with keys:
176
+ ``id, task, category, prev, curr, prev_trajectory, curr_trajectory``
177
+ """
178
+ prev_by_id = {str(r["id"]): r for r in results_prev}
179
+ curr_by_id = {str(r["id"]): r for r in results_curr}
180
+
181
+ pairs: list[dict] = []
182
+ for item in items:
183
+ tid = str(item.get("id", ""))
184
+ prev = prev_by_id.get(tid, {})
185
+ curr = curr_by_id.get(tid, {})
186
+ prev_ok = bool(prev.get("hard", 0))
187
+ curr_ok = bool(curr.get("hard", 0))
188
+
189
+ if not prev_ok and curr_ok:
190
+ category = "improved"
191
+ elif prev_ok and not curr_ok:
192
+ category = "regressed"
193
+ elif not prev_ok and not curr_ok:
194
+ category = "persistent_fail"
195
+ else:
196
+ category = "stable_success"
197
+
198
+ pairs.append({
199
+ "id": tid,
200
+ "task": item.get("question", item.get("task_description", item.get("instruction", tid))),
201
+ "category": category,
202
+ "prev": {
203
+ "hard": int(prev_ok),
204
+ "soft": float(prev.get("soft", 0.0)),
205
+ "predicted_answer": prev.get("predicted_answer", prev.get("answer", "N/A")),
206
+ "fail_reason": prev.get("fail_reason", ""),
207
+ },
208
+ "curr": {
209
+ "hard": int(curr_ok),
210
+ "soft": float(curr.get("soft", 0.0)),
211
+ "predicted_answer": curr.get("predicted_answer", curr.get("answer", "N/A")),
212
+ "fail_reason": curr.get("fail_reason", ""),
213
+ },
214
+ "prev_trajectory": (
215
+ _read_trajectory(prev_rollout_dir, tid) if prev_rollout_dir else ""
216
+ ),
217
+ "curr_trajectory": (
218
+ _read_trajectory(curr_rollout_dir, tid) if curr_rollout_dir else ""
219
+ ),
220
+ })
221
+
222
+ return pairs
223
+
224
+
225
+ def save_comparison_pairs(pairs: list[dict], out_path: str) -> None:
226
+ """Persist comparison pairs to JSON (without trajectory text to save space)."""
227
+ slim = []
228
+ for p in pairs:
229
+ slim.append({
230
+ "id": p["id"],
231
+ "task": p["task"],
232
+ "category": p["category"],
233
+ "prev": p["prev"],
234
+ "curr": p["curr"],
235
+ })
236
+ with open(out_path, "w") as f:
237
+ json.dump(slim, f, ensure_ascii=False, indent=2)
238
+
239
+
240
+ def format_comparison_text(pairs: list[dict]) -> str:
241
+ """Format structured comparison pairs into optimizer-readable text."""
242
+ by_cat: dict[str, list[dict]] = {
243
+ "regressed": [],
244
+ "persistent_fail": [],
245
+ "improved": [],
246
+ "stable_success": [],
247
+ }
248
+ for p in pairs:
249
+ by_cat.setdefault(p["category"], []).append(p)
250
+
251
+ total = len(pairs)
252
+ parts = [
253
+ f"## Longitudinal Comparison Summary\n"
254
+ f"Total samples: {total}\n"
255
+ f"- Improved (wrong→right): {len(by_cat['improved'])}\n"
256
+ f"- Regressed (right→wrong): {len(by_cat['regressed'])}\n"
257
+ f"- Persistent failures (wrong→wrong): {len(by_cat['persistent_fail'])}\n"
258
+ f"- Stable successes (right→right): {len(by_cat['stable_success'])}\n"
259
+ ]
260
+
261
+ categories = [
262
+ ("regressed", "Regressions (right→wrong) — HIGHEST PRIORITY", True),
263
+ ("persistent_fail", "Persistent Failures (wrong→wrong)", True),
264
+ ("improved", "Improvements (wrong→right)", True),
265
+ ("stable_success", "Stable Successes (right→right)", False),
266
+ ]
267
+
268
+ for cat_key, label, show_traj in categories:
269
+ entries = by_cat[cat_key]
270
+ if not entries:
271
+ parts.append(f"### {label}\n(none)\n")
272
+ continue
273
+
274
+ lines = [f"### {label}"]
275
+ for e in entries:
276
+ prev = e["prev"]
277
+ curr = e["curr"]
278
+ lines.append(
279
+ f"\n#### Task {e['id']}: {e['task']}\n"
280
+ f"- Prev epoch: {'PASS' if prev['hard'] else 'FAIL'} "
281
+ f"(soft={prev['soft']:.2f}) — answer: {str(prev['predicted_answer'])}\n"
282
+ f"- Curr epoch: {'PASS' if curr['hard'] else 'FAIL'} "
283
+ f"(soft={curr['soft']:.2f}) — answer: {str(curr['predicted_answer'])}"
284
+ )
285
+ if curr.get("fail_reason"):
286
+ lines.append(f"- Curr fail reason: {curr['fail_reason']}")
287
+ if prev.get("fail_reason") and not prev["hard"]:
288
+ lines.append(f"- Prev fail reason: {prev['fail_reason']}")
289
+
290
+ if show_traj:
291
+ if e.get("prev_trajectory"):
292
+ lines.append(
293
+ f"\n**Previous epoch trajectory:**\n```\n{e['prev_trajectory']}\n```"
294
+ )
295
+ if e.get("curr_trajectory"):
296
+ lines.append(
297
+ f"\n**Current epoch trajectory:**\n```\n{e['curr_trajectory']}\n```"
298
+ )
299
+
300
+ parts.append("\n".join(lines))
301
+
302
+ return "\n\n".join(parts)
303
+
304
+
305
+
306
+ # ── Optimizer call ────────────────────────────────────────────────────────────
307
+
308
+
309
+ def run_slow_update(
310
+ skill_content: str,
311
+ results_prev: list[dict],
312
+ results_curr: list[dict],
313
+ items: list[dict],
314
+ *,
315
+ prev_skill: str = "",
316
+ prev_slow_update_content: str = "",
317
+ prev_rollout_dir: str = "",
318
+ curr_rollout_dir: str = "",
319
+ comparison_pairs: list[dict] | None = None,
320
+ system_prompt: str | None = None,
321
+ ) -> dict | None:
322
+ """Run the slow update optimizer call for one epoch boundary.
323
+
324
+ Parameters
325
+ ----------
326
+ skill_content : str
327
+ Current epoch's skill (after fast updates).
328
+ results_prev : list[dict]
329
+ Rollout results of the 20 samples under previous epoch's skill.
330
+ results_curr : list[dict]
331
+ Rollout results of the 20 samples under current epoch's skill.
332
+ items : list[dict]
333
+ The 20 sample items used for comparison.
334
+ prev_skill : str
335
+ Previous epoch's skill content.
336
+ prev_slow_update_content : str
337
+ The slow update guidance from the previous epoch (to reflect on).
338
+ prev_rollout_dir : str
339
+ Path to previous epoch rollout output (contains predictions/).
340
+ curr_rollout_dir : str
341
+ Path to current epoch rollout output (contains predictions/).
342
+ system_prompt : str | None
343
+ Custom system prompt override.
344
+
345
+ Returns
346
+ -------
347
+ dict | None
348
+ Conforms to :class:`~skillopt.types.SlowUpdateResult`:
349
+ ``{"reasoning": str, "slow_update_content": str}`` or ``None``.
350
+ """
351
+ actual_system = system_prompt if system_prompt is not None else load_prompt("slow_update")
352
+
353
+ pairs = comparison_pairs
354
+ if pairs is None:
355
+ pairs = build_comparison_pairs(
356
+ results_prev, results_curr, items,
357
+ prev_rollout_dir=prev_rollout_dir,
358
+ curr_rollout_dir=curr_rollout_dir,
359
+ )
360
+ comparison_text = format_comparison_text(pairs)
361
+
362
+ prev_guidance_section = (
363
+ prev_slow_update_content.strip()
364
+ if prev_slow_update_content and prev_slow_update_content.strip()
365
+ else "(No previous guidance — this is the first slow update.)"
366
+ )
367
+
368
+ user = (
369
+ f"## Previous Epoch's Skill\n{prev_skill}\n\n"
370
+ f"## Current Epoch's Skill\n{skill_content}\n\n"
371
+ f"## Previous Slow Update Guidance\n"
372
+ f"The following guidance was active during the current epoch. "
373
+ f"Reflect on its effectiveness before writing the new version.\n\n"
374
+ f"{prev_guidance_section}\n\n"
375
+ f"## Longitudinal Comparison (same 20 tasks, two skill versions)\n"
376
+ f"{comparison_text}"
377
+ )
378
+
379
+ try:
380
+ response, _ = chat_optimizer(
381
+ system=actual_system,
382
+ user=user,
383
+ max_completion_tokens=16384,
384
+ retries=3,
385
+ stage="slow_update",
386
+ )
387
+ result = extract_json(response)
388
+ if result and result.get("slow_update_content"):
389
+ return {
390
+ "reasoning": str(result.get("reasoning", "")).strip(),
391
+ "slow_update_content": str(result["slow_update_content"]).strip(),
392
+ }
393
+ except Exception: # noqa: BLE001
394
+ traceback.print_exc()
395
+
396
+ return None
@@ -0,0 +1,135 @@
1
+ """Helpers for switching between patch edits and rewrite-from-suggestions."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Any
5
+
6
+ PATCH_MODE = "patch"
7
+ REWRITE_MODE = "rewrite_from_suggestions"
8
+ FULL_REWRITE_MINIBATCH_MODE = "full_rewrite_minibatch"
9
+
10
+
11
+ def normalize_update_mode(mode: str | None) -> str:
12
+ raw = str(mode or PATCH_MODE).strip().lower()
13
+ aliases = {
14
+ "patch": PATCH_MODE,
15
+ "edits": PATCH_MODE,
16
+ "rewrite": REWRITE_MODE,
17
+ "rewrite_from_suggestions": REWRITE_MODE,
18
+ "suggestions": REWRITE_MODE,
19
+ "rewrite_suggestions": REWRITE_MODE,
20
+ "full_rewrite": FULL_REWRITE_MINIBATCH_MODE,
21
+ "full_rewrite_minibatch": FULL_REWRITE_MINIBATCH_MODE,
22
+ "minibatch_full_rewrite": FULL_REWRITE_MINIBATCH_MODE,
23
+ "skill_rewrite_minibatch": FULL_REWRITE_MINIBATCH_MODE,
24
+ }
25
+ return aliases.get(raw, PATCH_MODE)
26
+
27
+
28
+ def is_rewrite_mode(mode: str | None) -> bool:
29
+ return normalize_update_mode(mode) == REWRITE_MODE
30
+
31
+
32
+ def is_full_rewrite_minibatch_mode(mode: str | None) -> bool:
33
+ return normalize_update_mode(mode) == FULL_REWRITE_MINIBATCH_MODE
34
+
35
+
36
+ def payload_key(mode: str | None) -> str:
37
+ if is_full_rewrite_minibatch_mode(mode):
38
+ return "skill_candidates"
39
+ return "revise_suggestions" if is_rewrite_mode(mode) else "edits"
40
+
41
+
42
+ def payload_label(mode: str | None, *, singular: bool = False, title: bool = False) -> str:
43
+ if is_full_rewrite_minibatch_mode(mode):
44
+ word = "skill candidate" if singular else "skill candidates"
45
+ elif is_rewrite_mode(mode):
46
+ word = "suggestion" if singular else "suggestions"
47
+ else:
48
+ word = "edit" if singular else "edits"
49
+ return word.title() if title else word
50
+
51
+
52
+ def get_payload_items(container: dict | None, mode: str | None) -> list[dict]:
53
+ if not isinstance(container, dict):
54
+ return []
55
+ items = container.get(payload_key(mode), [])
56
+ return items if isinstance(items, list) else []
57
+
58
+
59
+ def set_payload_items(container: dict, items: list[dict], mode: str | None) -> dict:
60
+ container[payload_key(mode)] = items
61
+ return container
62
+
63
+
64
+ def truncate_payload(container: dict, max_items: int, mode: str | None) -> dict:
65
+ if max_items < 0:
66
+ return container
67
+ items = get_payload_items(container, mode)
68
+ if len(items) > max_items:
69
+ set_payload_items(container, items[:max_items], mode)
70
+ return container
71
+
72
+
73
+ def describe_item(item: dict, mode: str | None, *, max_chars: int | None = None) -> str:
74
+ if not isinstance(item, dict):
75
+ return ""
76
+ if is_full_rewrite_minibatch_mode(mode):
77
+ parts = [
78
+ f"title={item.get('title', '')!r}",
79
+ f"change_summary={item.get('change_summary', [])!r}",
80
+ ]
81
+ if item.get("source_type"):
82
+ parts.append(f"source={item.get('source_type')}")
83
+ if item.get("support_count") is not None:
84
+ parts.append(f"support={item.get('support_count')}")
85
+ new_skill = str(item.get("new_skill", "")).strip()
86
+ if new_skill:
87
+ parts.append(f"new_skill_preview={new_skill!r}")
88
+ text = " ".join(parts)
89
+ elif is_rewrite_mode(mode):
90
+ parts = [
91
+ f"type={item.get('type', '?')}",
92
+ f"title={item.get('title', '')!r}",
93
+ f"instruction={item.get('instruction', '')!r}",
94
+ ]
95
+ if item.get("priority_hint"):
96
+ parts.append(f"priority={item.get('priority_hint')}")
97
+ if item.get("support_count") is not None:
98
+ parts.append(f"support={item.get('support_count')}")
99
+ text = " ".join(parts)
100
+ else:
101
+ op = item.get("op", "?")
102
+ target = item.get("target", "")
103
+ content = item.get("content", "")
104
+ parts = [f"op={op}"]
105
+ if target:
106
+ parts.append(f"target={target!r}")
107
+ if content:
108
+ parts.append(f"content={content!r}")
109
+ if item.get("support_count") is not None:
110
+ parts.append(f"support={item.get('support_count')}")
111
+ text = " ".join(parts)
112
+ # Truncation disabled: the optimizer is given the full item description.
113
+ return text
114
+
115
+
116
+ def short_item_summary(item: dict, mode: str | None, *, max_chars: int | None = None) -> dict[str, Any]:
117
+ if is_full_rewrite_minibatch_mode(mode):
118
+ return {
119
+ "title": str(item.get("title", "")),
120
+ "change_summary": [
121
+ str(x) for x in item.get("change_summary", [])
122
+ ] if isinstance(item.get("change_summary"), list) else [],
123
+ "source_type": item.get("source_type", ""),
124
+ }
125
+ if is_rewrite_mode(mode):
126
+ return {
127
+ "type": item.get("type", "?"),
128
+ "title": str(item.get("title", "")),
129
+ "instruction": str(item.get("instruction", "")),
130
+ }
131
+ return {
132
+ "op": item.get("op", "?"),
133
+ "content": str(item.get("content", "")),
134
+ "target": item.get("target", ""),
135
+ }
@@ -0,0 +1,63 @@
1
+ """Prompt loading utilities for ReflACT.
2
+
3
+ Prompts are stored as ``.md`` files and loaded at runtime:
4
+
5
+ - **Generic** prompts live in ``skillopt/prompts/*.md``
6
+ - **Env-specific** prompts live in ``skillopt/envs/<env>/prompts/*.md``
7
+
8
+ ``load_prompt(name, env)`` tries the env-specific path first, then falls
9
+ back to the generic default.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import os
14
+
15
+ _PROMPTS_DIR = os.path.dirname(os.path.abspath(__file__))
16
+ _REFLACT_DIR = os.path.dirname(_PROMPTS_DIR)
17
+
18
+ _cache: dict[str, str] = {}
19
+
20
+
21
+ def _read_file(path: str) -> str | None:
22
+ if path in _cache:
23
+ return _cache[path]
24
+ if not os.path.isfile(path):
25
+ return None
26
+ with open(path, encoding="utf-8") as f:
27
+ content = f.read()
28
+ _cache[path] = content
29
+ return content
30
+
31
+
32
+ def load_prompt(name: str, env: str | None = None) -> str:
33
+ """Load a prompt by name with env-specific override and generic fallback.
34
+
35
+ Lookup order:
36
+ 1. ``skillopt/envs/{env}/prompts/{name}.md`` (if *env* given)
37
+ 2. ``skillopt/prompts/{name}.md`` (generic default)
38
+
39
+ Raises ``FileNotFoundError`` if neither path exists.
40
+ """
41
+ if env is not None:
42
+ env_path = os.path.join(_REFLACT_DIR, "envs", env, "prompts", f"{name}.md")
43
+ content = _read_file(env_path)
44
+ if content is not None:
45
+ return content
46
+
47
+ generic_path = os.path.join(_PROMPTS_DIR, f"{name}.md")
48
+ content = _read_file(generic_path)
49
+ if content is not None:
50
+ return content
51
+
52
+ searched = []
53
+ if env is not None:
54
+ searched.append(os.path.join("skillopt/envs", env, "prompts", f"{name}.md"))
55
+ searched.append(f"skillopt/prompts/{name}.md")
56
+ raise FileNotFoundError(
57
+ f"Prompt '{name}' not found. Searched: {', '.join(searched)}"
58
+ )
59
+
60
+
61
+ def clear_cache() -> None:
62
+ """Clear the prompt file cache (useful for testing)."""
63
+ _cache.clear()
@@ -0,0 +1,41 @@
1
+ You are an expert failure-analysis agent for AI agent tasks.
2
+
3
+ You will be given MULTIPLE failed agent trajectories from a single minibatch
4
+ and the current skill document.
5
+ Your job is to identify the most important COMMON failure patterns across
6
+ the batch and propose a concise set of skill edits.
7
+
8
+ ## Analysis Process
9
+ 1. Read ALL trajectories in the minibatch.
10
+ 2. Identify the most prevalent, systematic failure patterns across them.
11
+ 3. For each pattern, classify its failure type.
12
+ 4. Propose skill edits that address the COMMON patterns — not individual edge cases.
13
+ 5. Edits must be generalizable; do not hardcode task-specific values.
14
+ 6. Only patch gaps in the skill — do not duplicate existing content.
15
+
16
+ You will be told the maximum number of edits (the budget L). Produce AT MOST L edits,
17
+ focusing on the highest-impact patterns. You may produce fewer if warranted.
18
+
19
+ Respond ONLY with a valid JSON object (no markdown fences, no extra text):
20
+ {
21
+ "batch_size": <number of trajectories analysed>,
22
+ "failure_summary": [
23
+ {"failure_type": "<type>", "count": <int>, "description": "<one-line>"}
24
+ ],
25
+ "patch": {
26
+ "reasoning": "<why these edits address the batch's common failures>",
27
+ "edits": [
28
+ {"op": "append", "content": "<markdown to add at end of skill>"},
29
+ {"op": "insert_after", "target": "<exact heading/text to insert after>", "content": "<markdown>"},
30
+ {"op": "replace", "target": "<exact text to replace>", "content": "<replacement>"},
31
+ {"op": "delete", "target": "<exact text to remove>"}
32
+ ]
33
+ }
34
+ }
35
+ Only include edits that are needed. "edits" can be an empty list if no patch is warranted.
36
+
37
+ IMPORTANT: The skill document may contain a section between
38
+ <!-- SLOW_UPDATE_START --> and <!-- SLOW_UPDATE_END --> markers.
39
+ This is a PROTECTED section managed by a separate slow-update process.
40
+ Do NOT propose any edits that target, modify, or delete content within
41
+ these markers.
@@ -0,0 +1,32 @@
1
+ You will be given several failed agent trajectories from one minibatch and the current skill document.
2
+
3
+ Summarize the lessons from these trajectories into one complete replacement skill document.
4
+
5
+ When rewriting from a minibatch, use the current trajectories as the primary
6
+ evidence for updates. Preserve essential task-format instructions, but avoid mechanically carrying over
7
+ stale, redundant, or conflicting rules. Prefer a concise, coherent replacement
8
+ skill over a long document with weakly supported guidance.
9
+
10
+ Do not include task-specific answers, IDs, file paths, gold values, or entity names.
11
+ If the skill contains a protected block between <!-- SLOW_UPDATE_START --> and
12
+ <!-- SLOW_UPDATE_END -->, keep that block unchanged.
13
+
14
+ Respond ONLY with a valid JSON object:
15
+ {
16
+ "batch_size": <number of trajectories analysed>,
17
+ "failure_summary": [
18
+ {"failure_type": "<type>", "count": <int>, "description": "<one-line>"}
19
+ ],
20
+ "patch": {
21
+ "reasoning": "<brief summary of the rewrite>",
22
+ "skill_candidates": [
23
+ {
24
+ "title": "<short title>",
25
+ "change_summary": ["<short change 1>", "<short change 2>"],
26
+ "new_skill": "<complete rewritten skill document>"
27
+ }
28
+ ]
29
+ }
30
+ }
31
+
32
+ Return exactly one item in "skill_candidates".