orizu 0.5.20 → 0.5.21

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 (260) hide show
  1. package/dist/archive-artifacts-cli.d.ts +50 -0
  2. package/dist/archive-artifacts-cli.d.ts.map +1 -0
  3. package/dist/artifact-archive.d.ts +48 -0
  4. package/dist/artifact-archive.d.ts.map +1 -0
  5. package/dist/artifact-pull.d.ts +22 -0
  6. package/dist/artifact-pull.d.ts.map +1 -0
  7. package/dist/banner.d.ts +3 -0
  8. package/dist/banner.d.ts.map +1 -0
  9. package/dist/connectors-cli.d.ts +31 -0
  10. package/dist/connectors-cli.d.ts.map +1 -0
  11. package/dist/credentials.d.ts +46 -0
  12. package/dist/credentials.d.ts.map +1 -0
  13. package/dist/csv.d.ts +49 -0
  14. package/dist/csv.d.ts.map +1 -0
  15. package/dist/dataset-download.d.ts +6 -0
  16. package/dist/dataset-download.d.ts.map +1 -0
  17. package/dist/dataset-edit-rows.d.ts +10 -0
  18. package/dist/dataset-edit-rows.d.ts.map +1 -0
  19. package/dist/dataset-upload-snapshot.d.ts +13 -0
  20. package/dist/dataset-upload-snapshot.d.ts.map +1 -0
  21. package/dist/daytona-slice-rehearsal.d.ts +89 -0
  22. package/dist/daytona-slice-rehearsal.d.ts.map +1 -0
  23. package/dist/daytona-slice.d.ts +106 -0
  24. package/dist/daytona-slice.d.ts.map +1 -0
  25. package/dist/deploy-key-merge-core.d.ts +121 -0
  26. package/dist/deploy-key-merge-core.d.ts.map +1 -0
  27. package/dist/diff-comments-cli.d.ts +12 -0
  28. package/dist/diff-comments-cli.d.ts.map +1 -0
  29. package/dist/diff-comments-cli.js +3 -0
  30. package/dist/egress-allowlist-cli.d.ts +23 -0
  31. package/dist/egress-allowlist-cli.d.ts.map +1 -0
  32. package/dist/egress-policy.d.ts +108 -0
  33. package/dist/egress-policy.d.ts.map +1 -0
  34. package/dist/error-response.d.ts +9 -0
  35. package/dist/error-response.d.ts.map +1 -0
  36. package/dist/file-parser.d.ts +5 -0
  37. package/dist/file-parser.d.ts.map +1 -0
  38. package/dist/gepa-engine-dispatch.d.ts +9 -0
  39. package/dist/gepa-engine-dispatch.d.ts.map +1 -0
  40. package/dist/gepa-engine-dispatch.js +48 -2
  41. package/dist/gepa-python-command.d.ts +6 -0
  42. package/dist/gepa-python-command.d.ts.map +1 -0
  43. package/dist/gepa-python-paths.d.ts +6 -0
  44. package/dist/gepa-python-paths.d.ts.map +1 -0
  45. package/dist/gepa-python-paths.js +6 -3
  46. package/dist/git-credential.d.ts +69 -0
  47. package/dist/git-credential.d.ts.map +1 -0
  48. package/dist/git-recovery-environment.d.ts +31 -0
  49. package/dist/git-recovery-environment.d.ts.map +1 -0
  50. package/dist/github-setup.d.ts +122 -0
  51. package/dist/github-setup.d.ts.map +1 -0
  52. package/dist/global-flags.d.ts +12 -0
  53. package/dist/global-flags.d.ts.map +1 -0
  54. package/dist/help.d.ts +46 -0
  55. package/dist/help.d.ts.map +1 -0
  56. package/dist/help.js +91 -23
  57. package/dist/hosted-boot.d.ts +273 -0
  58. package/dist/hosted-boot.d.ts.map +1 -0
  59. package/dist/hosted-bootstrap-rehearsal.d.ts +86 -0
  60. package/dist/hosted-bootstrap-rehearsal.d.ts.map +1 -0
  61. package/dist/hosted-bootstrap.d.ts +188 -0
  62. package/dist/hosted-bootstrap.d.ts.map +1 -0
  63. package/dist/hosted-broker-server.d.ts +50 -0
  64. package/dist/hosted-broker-server.d.ts.map +1 -0
  65. package/dist/hosted-git-http-server.d.ts +36 -0
  66. package/dist/hosted-git-http-server.d.ts.map +1 -0
  67. package/dist/hosted-harness-claude.d.ts +195 -0
  68. package/dist/hosted-harness-claude.d.ts.map +1 -0
  69. package/dist/hosted-harness-opencode.d.ts +227 -0
  70. package/dist/hosted-harness-opencode.d.ts.map +1 -0
  71. package/dist/hosted-harness.d.ts +118 -0
  72. package/dist/hosted-harness.d.ts.map +1 -0
  73. package/dist/hosted-harvest.d.ts +68 -0
  74. package/dist/hosted-harvest.d.ts.map +1 -0
  75. package/dist/hosted-headless.d.ts +72 -0
  76. package/dist/hosted-headless.d.ts.map +1 -0
  77. package/dist/hosted-loop-lifecycle.d.ts +206 -0
  78. package/dist/hosted-loop-lifecycle.d.ts.map +1 -0
  79. package/dist/hosted-loop.d.ts +170 -0
  80. package/dist/hosted-loop.d.ts.map +1 -0
  81. package/dist/hosted-run-event-sink.d.ts +222 -0
  82. package/dist/hosted-run-event-sink.d.ts.map +1 -0
  83. package/dist/hosted-runtime-assets.d.ts +186 -0
  84. package/dist/hosted-runtime-assets.d.ts.map +1 -0
  85. package/dist/hosted-session-cli.d.ts +258 -0
  86. package/dist/hosted-session-cli.d.ts.map +1 -0
  87. package/dist/hosted-skill-staging.d.ts +73 -0
  88. package/dist/hosted-skill-staging.d.ts.map +1 -0
  89. package/dist/http.d.ts +8 -0
  90. package/dist/http.d.ts.map +1 -0
  91. package/dist/index.d.ts +22 -0
  92. package/dist/index.d.ts.map +1 -0
  93. package/dist/index.js +88 -92
  94. package/dist/instruction-set-gepa-launch.d.ts +3 -0
  95. package/dist/instruction-set-gepa-launch.d.ts.map +1 -0
  96. package/dist/instruction-set-gepa-launch.js +15 -0
  97. package/dist/instruction-set-loader.d.ts +6 -0
  98. package/dist/instruction-set-loader.d.ts.map +1 -0
  99. package/dist/instruction-set-loader.js +59 -0
  100. package/dist/instruction-set-manifest.d.ts +14 -0
  101. package/dist/instruction-set-manifest.d.ts.map +1 -0
  102. package/dist/instruction-set-manifest.js +162 -0
  103. package/dist/instruction-sets-cli.d.ts +40 -0
  104. package/dist/instruction-sets-cli.d.ts.map +1 -0
  105. package/dist/instruction-sets-cli.js +361 -0
  106. package/dist/json-response.d.ts +4 -0
  107. package/dist/json-response.d.ts.map +1 -0
  108. package/dist/jsonl-stream.d.ts +8 -0
  109. package/dist/jsonl-stream.d.ts.map +1 -0
  110. package/dist/kill-switch-cli.d.ts +30 -0
  111. package/dist/kill-switch-cli.d.ts.map +1 -0
  112. package/dist/loopback-serve.d.ts +25 -0
  113. package/dist/loopback-serve.d.ts.map +1 -0
  114. package/dist/manifests-cli.d.ts +22 -0
  115. package/dist/manifests-cli.d.ts.map +1 -0
  116. package/dist/markdown-report-input.d.ts +7 -0
  117. package/dist/markdown-report-input.d.ts.map +1 -0
  118. package/dist/merge-job.d.ts +155 -0
  119. package/dist/merge-job.d.ts.map +1 -0
  120. package/dist/model-configs-cli.d.ts +19 -0
  121. package/dist/model-configs-cli.d.ts.map +1 -0
  122. package/dist/model-configs-cli.js +104 -0
  123. package/dist/optimization-export-cli.d.ts +19 -0
  124. package/dist/optimization-export-cli.d.ts.map +1 -0
  125. package/dist/optimizations-list-cli.d.ts +20 -0
  126. package/dist/optimizations-list-cli.d.ts.map +1 -0
  127. package/dist/optimizations-promote-cli.d.ts +8 -0
  128. package/dist/optimizations-promote-cli.d.ts.map +1 -0
  129. package/dist/optimizations-promote-cli.js +34 -0
  130. package/dist/option-validation.d.ts +15 -0
  131. package/dist/option-validation.d.ts.map +1 -0
  132. package/dist/preview-runtime.d.ts +63 -0
  133. package/dist/preview-runtime.d.ts.map +1 -0
  134. package/dist/prompt-draft-push.d.ts +60 -0
  135. package/dist/prompt-draft-push.d.ts.map +1 -0
  136. package/dist/prompt-label-cli.d.ts +11 -0
  137. package/dist/prompt-label-cli.d.ts.map +1 -0
  138. package/dist/prompt-label-cli.js +29 -0
  139. package/dist/prompt-length-wire.d.ts +28 -0
  140. package/dist/prompt-length-wire.d.ts.map +1 -0
  141. package/dist/prompt-report-cli.d.ts +10 -0
  142. package/dist/prompt-report-cli.d.ts.map +1 -0
  143. package/dist/prompt-summary-table.d.ts +15 -0
  144. package/dist/prompt-summary-table.d.ts.map +1 -0
  145. package/dist/prompt-summary-table.js +6 -0
  146. package/dist/registration-tag-warning.d.ts +32 -0
  147. package/dist/registration-tag-warning.d.ts.map +1 -0
  148. package/dist/report-comments-cli.d.ts +16 -0
  149. package/dist/report-comments-cli.d.ts.map +1 -0
  150. package/dist/report-markdown-blankness.d.ts +2 -0
  151. package/dist/report-markdown-blankness.d.ts.map +1 -0
  152. package/dist/report-replacement-warning.d.ts +17 -0
  153. package/dist/report-replacement-warning.d.ts.map +1 -0
  154. package/dist/repository-backup-core.d.ts +225 -0
  155. package/dist/repository-backup-core.d.ts.map +1 -0
  156. package/dist/repository-recovery-manifest.d.ts +83 -0
  157. package/dist/repository-recovery-manifest.d.ts.map +1 -0
  158. package/dist/run-event-digest.d.ts +74 -0
  159. package/dist/run-event-digest.d.ts.map +1 -0
  160. package/dist/runner-dir-verify.d.ts +66 -0
  161. package/dist/runner-dir-verify.d.ts.map +1 -0
  162. package/dist/runner-instruction-set-contract.d.ts +24 -0
  163. package/dist/runner-instruction-set-contract.d.ts.map +1 -0
  164. package/dist/runner-instruction-set-contract.js +50 -0
  165. package/dist/sandbox-hygiene.d.ts +45 -0
  166. package/dist/sandbox-hygiene.d.ts.map +1 -0
  167. package/dist/sandbox-provider.d.ts +193 -0
  168. package/dist/sandbox-provider.d.ts.map +1 -0
  169. package/dist/scorer-draft-push.d.ts +50 -0
  170. package/dist/scorer-draft-push.d.ts.map +1 -0
  171. package/dist/scorers-cli.d.ts +20 -0
  172. package/dist/scorers-cli.d.ts.map +1 -0
  173. package/dist/scorers-cli.js +22 -0
  174. package/dist/scorers-verify-parity-bridge.d.ts +16 -0
  175. package/dist/scorers-verify-parity-bridge.d.ts.map +1 -0
  176. package/dist/scorers-verify-parity-bridge.js +90 -0
  177. package/dist/scorers-verify-parity.d.ts +81 -0
  178. package/dist/scorers-verify-parity.d.ts.map +1 -0
  179. package/dist/scorers-verify-parity.js +502 -0
  180. package/dist/scores-accept-cli.d.ts +21 -0
  181. package/dist/scores-accept-cli.d.ts.map +1 -0
  182. package/dist/secret-redaction.d.ts +47 -0
  183. package/dist/secret-redaction.d.ts.map +1 -0
  184. package/dist/session-timeline.d.ts +54 -0
  185. package/dist/session-timeline.d.ts.map +1 -0
  186. package/dist/setup-prompt.d.ts +5 -0
  187. package/dist/setup-prompt.d.ts.map +1 -0
  188. package/dist/skill-installer.d.ts +68 -0
  189. package/dist/skill-installer.d.ts.map +1 -0
  190. package/dist/skilled-proposer-launch.d.ts +7 -0
  191. package/dist/skilled-proposer-launch.d.ts.map +1 -0
  192. package/dist/skilled-proposer-launch.js +60 -0
  193. package/dist/smoke.d.ts +102 -0
  194. package/dist/smoke.d.ts.map +1 -0
  195. package/dist/start-intent.d.ts +12 -0
  196. package/dist/start-intent.d.ts.map +1 -0
  197. package/dist/task-assignment-manifest.d.ts +15 -0
  198. package/dist/task-assignment-manifest.d.ts.map +1 -0
  199. package/dist/task-create-error.d.ts +18 -0
  200. package/dist/task-create-error.d.ts.map +1 -0
  201. package/dist/team-connectors-cli.d.ts +29 -0
  202. package/dist/team-connectors-cli.d.ts.map +1 -0
  203. package/dist/types.d.ts +39 -0
  204. package/dist/types.d.ts.map +1 -0
  205. package/dist/vercel-sandbox-provider.d.ts +156 -0
  206. package/dist/vercel-sandbox-provider.d.ts.map +1 -0
  207. package/dist/workbench-cli.d.ts +233 -0
  208. package/dist/workbench-cli.d.ts.map +1 -0
  209. package/dist/workspace-sync.d.ts +207 -0
  210. package/dist/workspace-sync.d.ts.map +1 -0
  211. package/dist/workspace.d.ts +92 -0
  212. package/dist/workspace.d.ts.map +1 -0
  213. package/dist/zip-draft-push.d.ts +55 -0
  214. package/dist/zip-draft-push.d.ts.map +1 -0
  215. package/package.json +14 -1
  216. package/requirements/README.md +138 -0
  217. package/requirements/skilled-proposer.in +66 -0
  218. package/requirements/skilled-proposer.lock +265 -0
  219. package/scripts/ensure-skilled-proposer-venv.mjs +5 -0
  220. package/scripts/refresh-skilled-proposer-lock.mjs +256 -0
  221. package/src/diff-comments-cli.ts +4 -0
  222. package/src/gepa-engine-dispatch.ts +42 -2
  223. package/src/gepa-python-paths.ts +6 -3
  224. package/src/help.ts +93 -23
  225. package/src/index.ts +99 -126
  226. package/src/instruction-set-gepa-launch.ts +28 -0
  227. package/src/instruction-set-loader.ts +37 -0
  228. package/src/instruction-set-manifest.ts +120 -0
  229. package/src/instruction-sets-cli.ts +306 -0
  230. package/src/model-configs-cli.ts +119 -0
  231. package/src/optimizations-promote-cli.ts +41 -0
  232. package/src/prompt-label-cli.ts +55 -0
  233. package/src/prompt-summary-table.ts +12 -0
  234. package/src/runner-instruction-set-contract.ts +64 -0
  235. package/src/scorers-cli.ts +40 -0
  236. package/src/scorers-verify-parity-bridge.ts +90 -0
  237. package/src/scorers-verify-parity.ts +564 -0
  238. package/src/skilled-proposer-launch.ts +89 -0
  239. package/src/skilled-proposer-venv-manager.mjs +757 -0
  240. package/tsconfig.json +2 -0
  241. package/vendor/orizu-gepa/src/orizu_gepa_connector/__init__.py +2 -1
  242. package/vendor/orizu-gepa/src/orizu_gepa_connector/adapter.py +8 -2
  243. package/vendor/orizu-gepa/src/orizu_gepa_connector/callbacks.py +17 -3
  244. package/vendor/orizu-gepa/src/orizu_gepa_connector/engine.py +100 -8
  245. package/vendor/orizu-gepa/src/orizu_gepa_connector/instruction_set_loader.py +52 -0
  246. package/vendor/orizu-gepa/src/orizu_gepa_connector/reflection.py +92 -12
  247. package/vendor/orizu-gepa/src/orizu_gepa_connector/runtime.py +100 -22
  248. package/vendor/orizu-gepa/src/orizu_gepa_connector/skilled_proposer_bridge.py +381 -0
  249. package/vendor/orizu-gepa/src/orizu_gepa_connector/stop_conditions.py +17 -0
  250. package/vendor/orizu-gepa/src/orizu_gepa_connector/translator.py +2 -1
  251. package/vendor/orizu-gepa-python/src/orizu_gepa/client.py +52 -7
  252. package/vendor/orizu-gepa-python/src/orizu_gepa/local_log.py +4 -1
  253. package/vendor/orizu-gepa-python/src/orizu_gepa/optimizer.py +10 -5
  254. package/vendor/orizu-gepa-python/src/orizu_gepa/reflection.py +323 -68
  255. package/vendor/orizu-gepa-python/src/orizu_gepa/runner.py +207 -13
  256. package/vendor/skills/orizu-cli/SKILL.md +2 -0
  257. package/vendor/skills/orizu-cli/references/cli-reference.md +85 -1
  258. package/vendor/skills/orizu-cli/references/migrate-existing-gepa-setup.md +303 -0
  259. package/vendor/skills/orizu-cli/references/optimization-with-gepa.md +25 -0
  260. package/vendor/skills/orizu-cli/references/prompt-control-plane.md +108 -8
package/tsconfig.json CHANGED
@@ -4,6 +4,8 @@
4
4
  "module": "NodeNext",
5
5
  "moduleResolution": "NodeNext",
6
6
  "outDir": "dist",
7
+ "declaration": true,
8
+ "declarationMap": true,
7
9
  "rootDir": "src",
8
10
  "strict": true,
9
11
  "esModuleInterop": true,
@@ -4,5 +4,6 @@ from .adapter import RunnerEvaluationAdapter
4
4
  from .callbacks import LifecycleHooks, OrizuCallback
5
5
  from .engine import SeedValidationRefused, run_official_gepa
6
6
  from .cli import main
7
+ from .instruction_set_loader import load_instruction_set
7
8
 
8
- __all__ = ["LifecycleHooks", "OrizuCallback", "RunnerEvaluationAdapter", "SeedValidationRefused", "main", "run_official_gepa"]
9
+ __all__ = ["LifecycleHooks", "OrizuCallback", "RunnerEvaluationAdapter", "SeedValidationRefused", "load_instruction_set", "main", "run_official_gepa"]
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import json
5
6
  from typing import Any
6
7
 
7
8
  from gepa.core.adapter import EvaluationBatch
@@ -59,8 +60,13 @@ class RunnerEvaluationAdapter:
59
60
  self.last_candidate: dict[str, str] | None = None
60
61
 
61
62
  def evaluate(self, batch: list[Any], candidate: dict[str, str], capture_traces: bool = False) -> EvaluationBatch:
63
+ # The exec-context can describe a multi-component wrapper for a
64
+ # legacy prompt run. The candidate is the execution contract: only a
65
+ # map with more than one value is a tuple rewrite.
66
+ is_multi_component_tuple = len(candidate) > 1
62
67
  component = next(iter(candidate))
63
- candidate_text = candidate[component]
68
+ candidate_text: str | dict[str, str] = dict(candidate) if is_multi_component_tuple else candidate[component]
69
+ candidate_id = json.dumps(candidate, sort_keys=True, separators=(",", ":")) if is_multi_component_tuple else component
64
70
  outputs: list[dict[str, Any]] = []
65
71
  scores: list[float] = []
66
72
  trajectories: list[dict[str, Any]] = []
@@ -74,7 +80,7 @@ class RunnerEvaluationAdapter:
74
80
  try:
75
81
  row_evaluations = evaluate_candidate(
76
82
  candidate_text=candidate_text,
77
- candidate_id=component,
83
+ candidate_id=candidate_id,
78
84
  rows=batch,
79
85
  split="official_gepa",
80
86
  prompt_context=self.prompt_context,
@@ -248,9 +248,12 @@ class OrizuCallback:
248
248
  def on_proposal_start(self, event: dict[str, Any]) -> None:
249
249
  iteration = int(event["iteration"])
250
250
  self._flush_pending_parent_minibatches(iteration)
251
+ parent_candidate = event.get("parent_candidate")
251
252
  self._reflection_contexts[iteration] = {
252
253
  "iteration": iteration,
253
254
  "parent_candidate_id": self._selected_parent_by_iteration.get(iteration),
255
+ "components_to_update": list(event.get("components") or []),
256
+ "parent_candidate": dict(parent_candidate) if isinstance(parent_candidate, dict) else None,
254
257
  }
255
258
  self._emit("on_proposal_start", event)
256
259
 
@@ -495,18 +498,29 @@ class OrizuCallback:
495
498
  def _emit_buffered_proposal(self, event: dict[str, Any], candidate_id: str) -> None:
496
499
  iteration = int(event["iteration"])
497
500
  parent_id = self._selected_parent_by_iteration.get(iteration)
498
- components = event.get("new_instructions") or {}
501
+ context = self._reflection_contexts.get(iteration, {})
502
+ parent_candidate = context.get("parent_candidate")
503
+ new_instructions = event.get("new_instructions") or {}
504
+ components = {
505
+ **(parent_candidate if isinstance(parent_candidate, dict) else {}),
506
+ **(new_instructions if isinstance(new_instructions, dict) else {}),
507
+ }
499
508
  bounded_components = {}
500
509
  truncated = False
501
510
  for key, value in components.items():
502
511
  bounded, did_truncate = _bounded(value, self.max_payload_chars)
503
512
  bounded_components[key] = bounded
504
513
  truncated = truncated or did_truncate
514
+ selected = list(context.get("components_to_update") or [])
505
515
  body, body_truncated = _bounded(next(iter(components.values()), None), self.max_payload_chars)
516
+ payload = {"components": bounded_components, "components_to_update": selected,
517
+ "payload_truncated": truncated or body_truncated}
518
+ if ((isinstance(parent_candidate, dict) and len(parent_candidate) == 1)
519
+ or (parent_candidate is None and len(components) == 1)):
520
+ payload["body"] = body
506
521
  self.sink.emit(
507
522
  "candidate_proposed",
508
- {"body": body, "components": bounded_components,
509
- "payload_truncated": truncated or body_truncated},
523
+ payload,
510
524
  iteration=iteration,
511
525
  candidate_id=candidate_id,
512
526
  parent_candidate_id=parent_id,
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from collections.abc import Callable, Sequence
6
+ import dataclasses
6
7
  import json
7
8
  import random
8
9
  from typing import Any
@@ -13,6 +14,7 @@ from orizu_gepa.optimizer import TextGepaConfig
13
14
 
14
15
  from .callbacks import LifecycleHooks, OrizuCallback
15
16
  from .preflight import validate_seed_and_scorer
17
+ from .stop_conditions import ProposalBudgetStopper
16
18
 
17
19
 
18
20
  def _row_id(row: Any, index: int) -> str:
@@ -84,8 +86,8 @@ def _preflight_row_evidence(rows: list[Any], scores: list[float], outputs: list[
84
86
  return evidence
85
87
 
86
88
 
87
- def validate_seed_before_run(*, seed_candidate: dict[str, str], valset: list[Any], adapter: Any,
88
- hooks: LifecycleHooks, allow_degenerate_seed: bool) -> dict[str, Any]:
89
+ def _validate_seed_before_run(*, seed_candidate: dict[str, str], valset: list[Any], adapter: Any,
90
+ hooks: LifecycleHooks, allow_degenerate_seed: bool) -> tuple[dict[str, Any], Any, list[Any]]:
89
91
  """Cheap scorer preflight; GEPA owns the single full seed validation."""
90
92
  # The approved cap bounds preflight spend. With a binary scorer and a seed
91
93
  # that is correct 70% of the time, all three sampled rows can still score
@@ -103,23 +105,60 @@ def validate_seed_before_run(*, seed_candidate: dict[str, str], valset: list[Any
103
105
  hooks.emit("seed_validated", verdict)
104
106
  if not verdict["allowed"]:
105
107
  raise SeedValidationRefused(verdict)
108
+ return verdict, seed_evaluation, preflight_rows
109
+
110
+
111
+ def validate_seed_before_run(*, seed_candidate: dict[str, str], valset: list[Any], adapter: Any,
112
+ hooks: LifecycleHooks, allow_degenerate_seed: bool) -> dict[str, Any]:
113
+ """Validate the seed and return the durable public preflight verdict."""
114
+ verdict, _evaluation, _rows = _validate_seed_before_run(
115
+ seed_candidate=seed_candidate,
116
+ valset=valset,
117
+ adapter=adapter,
118
+ hooks=hooks,
119
+ allow_degenerate_seed=allow_degenerate_seed,
120
+ )
106
121
  return verdict
107
122
 
108
123
 
124
+ class _PreflightReusingAdapter:
125
+ """Serve GEPA's immediate seed evaluation from the completed preflight."""
126
+
127
+ def __init__(self, adapter: Any, seed_candidate: dict[str, str], rows: list[Any], evaluation: Any):
128
+ self._adapter = adapter
129
+ self._seed_candidate = seed_candidate
130
+ self._rows = rows
131
+ self._evaluation = evaluation
132
+ self._served_seed = False
133
+
134
+ def evaluate(self, batch: list[Any], candidate: dict[str, str], capture_traces: bool = False):
135
+ if (not self._served_seed and not capture_traces and candidate == self._seed_candidate
136
+ and list(batch) == self._rows):
137
+ self._served_seed = True
138
+ return self._evaluation
139
+ return self._adapter.evaluate(batch, candidate, capture_traces=capture_traces)
140
+
141
+ def __getattr__(self, name: str) -> Any:
142
+ return getattr(self._adapter, name)
143
+
144
+
109
145
  def run_official_gepa(
110
146
  *,
111
147
  seed_candidate: dict[str, str],
112
148
  trainset: list[Any],
113
149
  valset: list[Any],
114
150
  adapter: Any,
115
- callback: OrizuCallback,
151
+ callback: Any,
116
152
  hooks: LifecycleHooks,
117
153
  max_metric_calls: int | None = None,
118
154
  stop_callbacks: Sequence[Callable[[Any], bool]] = (),
119
155
  allow_degenerate_seed: bool = False,
120
156
  reflection_lm: Callable[[str], str] | None = None,
157
+ custom_candidate_proposer: Callable[..., dict[str, str]] | None = None,
158
+ proposal_budget: Any | None = None,
121
159
  seed_already_validated: bool = False,
122
160
  config: TextGepaConfig | None = None,
161
+ module_selector: str = "round_robin",
123
162
  ) -> Any:
124
163
  """Run GEPA after the launch-time seed check and emit named lifecycle hooks.
125
164
 
@@ -135,8 +174,10 @@ def run_official_gepa(
135
174
  callback.evaluation_adapter = adapter
136
175
  callback.validation_row_ids = [_row_id(row, index) for index, row in enumerate(valset)]
137
176
 
177
+ preflight_evaluation = None
178
+ preflight_rows: list[Any] = []
138
179
  if not seed_already_validated:
139
- validate_seed_before_run(
180
+ _verdict, preflight_evaluation, preflight_rows = _validate_seed_before_run(
140
181
  seed_candidate=seed_candidate,
141
182
  valset=valset,
142
183
  adapter=adapter,
@@ -144,6 +185,27 @@ def run_official_gepa(
144
185
  allow_degenerate_seed=allow_degenerate_seed,
145
186
  )
146
187
 
188
+ # GEPA 0.1.4 reads this optional protocol attribute directly in its
189
+ # reflective proposer, even when a custom proposer owns the call. Our
190
+ # runner adapter supplies it; lightweight adapters used by the real GEPA
191
+ # boundary do not. Normalize the vendor's optional slot without replacing
192
+ # an adapter-owned proposer.
193
+ if custom_candidate_proposer is not None and not hasattr(adapter, "propose_new_texts"):
194
+ adapter.propose_new_texts = None
195
+
196
+ gepa_adapter = adapter
197
+ if custom_candidate_proposer is not None and preflight_evaluation is not None:
198
+ # A one-row low metric ceiling otherwise pays for this same seed row a
199
+ # second time inside GEPA before it reaches the custom proposal. The
200
+ # cached result is already a real evaluator result from this call's
201
+ # mandatory preflight; only the duplicate evaluator invocation is
202
+ # removed, never the separate proposal ledger.
203
+ gepa_adapter = _PreflightReusingAdapter(adapter, seed_candidate, preflight_rows, preflight_evaluation)
204
+ if custom_candidate_proposer is not None:
205
+ # GEPA refuses an adapter-owned proposer alongside its explicit custom
206
+ # proposer. The latter is the selected skilled-proposer path.
207
+ gepa_adapter.propose_new_texts = None
208
+
147
209
  config = config or TextGepaConfig()
148
210
  candidate_selection_strategy: Any = config.candidate_selection_strategy
149
211
  # Official GEPA's string shortcut fixes epsilon at 0.1. Use its public
@@ -154,24 +216,54 @@ def run_official_gepa(
154
216
  rng=random.Random(config.seed),
155
217
  )
156
218
 
219
+ owned_stop_callbacks = list(stop_callbacks)
220
+ if proposal_budget is not None:
221
+ owned_stop_callbacks.append(ProposalBudgetStopper(proposal_budget))
222
+
223
+ reflection_minibatch_size = config.minibatch_size
224
+ if custom_candidate_proposer is not None and max_metric_calls is not None:
225
+ # GEPA's metric stopper is evaluated at iteration boundaries. Do not
226
+ # let its default three-row reflection batch overshoot a deliberately
227
+ # smaller evaluator-only ceiling before that boundary is observed.
228
+ reflection_minibatch_size = min(
229
+ reflection_minibatch_size,
230
+ max(1, max_metric_calls - len(valset)),
231
+ )
232
+
157
233
  kwargs: dict[str, Any] = {
158
234
  "seed_candidate": seed_candidate,
159
235
  "trainset": trainset,
160
236
  "valset": valset,
161
- "adapter": adapter,
237
+ "adapter": gepa_adapter,
162
238
  "callbacks": [callback],
163
- "stop_callbacks": list(stop_callbacks) or None,
239
+ "stop_callbacks": owned_stop_callbacks or None,
164
240
  "max_metric_calls": max_metric_calls,
165
241
  "candidate_selection_strategy": candidate_selection_strategy,
166
242
  "skip_perfect_score": config.skip_perfect_parent_reflection,
167
- "reflection_minibatch_size": config.minibatch_size,
243
+ "reflection_minibatch_size": reflection_minibatch_size,
168
244
  "cache_evaluation": config.cache_evaluations,
169
245
  "seed": config.seed,
170
246
  "display_progress_bar": False,
171
247
  "raise_on_exception": True,
248
+ "module_selector": module_selector,
172
249
  }
173
250
  if reflection_lm is not None:
174
251
  kwargs["reflection_lm"] = reflection_lm
252
+ # The connector implements GEPA's component-aware strategy protocol;
253
+ # passing it as a bare callable would discard the selected component.
254
+ kwargs["reflection_strategy"] = reflection_lm
255
+ if custom_candidate_proposer is not None:
256
+ kwargs["custom_candidate_proposer"] = custom_candidate_proposer
175
257
  kwargs["trainset"] = _with_row_identities(trainset)
176
258
  kwargs["valset"] = _with_row_identities(valset)
177
- return optimize(**kwargs)
259
+ result = optimize(**kwargs)
260
+ if custom_candidate_proposer is not None and preflight_evaluation is not None:
261
+ # GEPA's state correctly counts its seed evaluation, but this run
262
+ # served that evaluation from the already-accounted launch preflight.
263
+ # Its public metric result must consequently retain M1's convention:
264
+ # GEPA result counts exclude the one caller preflight, while the
265
+ # adapter's physical count is result + one.
266
+ total = getattr(result, "total_metric_calls", None)
267
+ if isinstance(total, int):
268
+ result = dataclasses.replace(result, total_metric_calls=max(0, total - len(preflight_rows)))
269
+ return result
@@ -0,0 +1,52 @@
1
+ """Offline reader for the ALI-1532 synced instruction-set layout."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+
7
+ class InstructionSetLoaderError(Exception):
8
+ def __init__(self, code: str):
9
+ self.code = code
10
+ super().__init__(code)
11
+
12
+
13
+ def _inside(root: Path, candidate: Path) -> bool:
14
+ try:
15
+ candidate.relative_to(root)
16
+ return True
17
+ except ValueError:
18
+ return False
19
+
20
+
21
+ def load_instruction_set(directory: str, name: str, model_config_identity: str) -> dict[str, str]:
22
+ root = (Path(directory) / name).resolve()
23
+ if not root.is_dir():
24
+ raise InstructionSetLoaderError("instruction_set_not_synced")
25
+ manifest_path = root / "manifest.json"
26
+ if not manifest_path.is_file():
27
+ raise InstructionSetLoaderError("instruction_set_manifest_missing")
28
+ try:
29
+ manifest = json.loads(manifest_path.read_text())
30
+ except (OSError, json.JSONDecodeError) as error:
31
+ raise InstructionSetLoaderError("instruction_set_manifest_missing") from error
32
+ if manifest.get("manifestVersion") != 1:
33
+ raise InstructionSetLoaderError("instruction_set_manifest_version_unsupported")
34
+ if manifest.get("filteredTo") and model_config_identity not in manifest["filteredTo"]:
35
+ raise InstructionSetLoaderError("instruction_set_profile_not_synced")
36
+ production = next((profile.get("production") for profile in manifest.get("profiles", []) if profile.get("modelConfigIdentity") == model_config_identity), None)
37
+ material = production or manifest.get("default", {})
38
+ values: dict[str, str] = {}
39
+ for key in manifest.get("shape", []):
40
+ if key in material.get("pinnedComponents", {}):
41
+ raise InstructionSetLoaderError("instruction_set_component_unavailable")
42
+ relative = material.get("files", {}).get(key)
43
+ if not relative:
44
+ raise InstructionSetLoaderError("instruction_set_profile_key_missing")
45
+ path = (root / relative).resolve()
46
+ if not _inside(root, path):
47
+ raise InstructionSetLoaderError("instruction_set_path_unsafe")
48
+ try:
49
+ values[key] = path.read_text()
50
+ except OSError as error:
51
+ raise InstructionSetLoaderError("instruction_set_component_unreadable") from error
52
+ return values
@@ -3,9 +3,11 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from collections.abc import Callable
6
+ from dataclasses import replace
7
+ import json
6
8
  from typing import Any
7
9
 
8
- from orizu_gepa.optimizer import build_reflection_prompt
10
+ from orizu_gepa.optimizer import DEFAULT_REFLECTION_PROMPT_TEMPLATE, build_reflection_prompt
9
11
  from orizu_gepa.reflection import reflect_with_provider
10
12
 
11
13
 
@@ -26,16 +28,14 @@ class GepaReflectionLM:
26
28
  GEPA intentionally catches reflection failures per proposal. That keeps an
27
29
  optimization alive, but means the connector is the final durable boundary
28
30
  for the actual exception. This wrapper records it *before* re-raising to
29
- GEPA's safe helper and exposes the same estimated usage shape as GEPA's
30
- ``TrackingLM``.
31
+ GEPA's safe helper and exposes provider-measured token usage in GEPA's
32
+ ``TrackingLM``-shaped fields.
31
33
  """
32
34
 
33
- _CHARS_PER_TOKEN = 4
34
-
35
35
  def __init__(
36
36
  self,
37
37
  *,
38
- context_supplier: Callable[[], tuple[str, list[Any]]],
38
+ context_supplier: Callable[[], tuple[str | dict[str, str], list[Any]]],
39
39
  config: Any,
40
40
  failure_reporter: Callable[..., None] | None,
41
41
  success_reporter: Callable[[str], None] | None,
@@ -46,6 +46,7 @@ class GepaReflectionLM:
46
46
  self._success_reporter = success_reporter
47
47
  self._total_tokens_in = 0
48
48
  self._total_tokens_out = 0
49
+ self._total_tokens = 0
49
50
 
50
51
  @property
51
52
  def total_cost(self) -> float:
@@ -59,15 +60,46 @@ class GepaReflectionLM:
59
60
  def total_tokens_out(self) -> int:
60
61
  return self._total_tokens_out
61
62
 
63
+ @property
64
+ def total_tokens(self) -> int:
65
+ return self._total_tokens
66
+
67
+ def _account_usage(self, usage: Any) -> None:
68
+ if not isinstance(usage, dict):
69
+ raise RuntimeError("ALI_1505_PROVIDER_USAGE_MISSING: reflection transport returned no provider usage")
70
+ try:
71
+ input_tokens = int(usage["input_tokens"])
72
+ output_tokens = int(usage["output_tokens"])
73
+ total_tokens = int(usage["total_tokens"])
74
+ except (KeyError, TypeError, ValueError) as error:
75
+ raise RuntimeError("ALI_1505_PROVIDER_USAGE_INVALID: reflection transport returned invalid provider usage") from error
76
+ if min(input_tokens, output_tokens, total_tokens) < 0:
77
+ raise RuntimeError("ALI_1505_PROVIDER_USAGE_INVALID: reflection transport returned negative provider usage")
78
+ self._total_tokens_in += input_tokens
79
+ self._total_tokens_out += output_tokens
80
+ self._total_tokens += total_tokens
81
+
62
82
  def __call__(self, gepa_prompt: str) -> str:
63
- parent_text, parent_results = self._context_supplier()
83
+ parent_candidate, parent_results = self._context_supplier()
84
+ parent_text = (
85
+ json.dumps(parent_candidate, sort_keys=True, separators=(",", ":"))
86
+ if isinstance(parent_candidate, dict) and len(parent_candidate) > 1
87
+ else next(iter(parent_candidate.values()))
88
+ if isinstance(parent_candidate, dict)
89
+ else parent_candidate
90
+ )
64
91
  try:
65
92
  # Prompt validation is part of the provider attempt. Keep it
66
93
  # inside this boundary so GEPA cannot silently swallow its error.
67
94
  provider_prompt = build_reflection_prompt(parent_text, parent_results, self._config)
68
- self._total_tokens_in += max(1, len(provider_prompt) // self._CHARS_PER_TOKEN)
69
95
  result = reflect_with_provider(parent_text, parent_results, self._config)
96
+ response = result.response
97
+ provider_prompt = getattr(result, "prompt", provider_prompt)
98
+ self._account_usage(getattr(result, "usage", None))
70
99
  except Exception as error:
100
+ billable_usage = getattr(error, "usage", None)
101
+ if billable_usage is not None:
102
+ self._account_usage(billable_usage)
71
103
  if self._failure_reporter is not None:
72
104
  self._failure_reporter(
73
105
  error=error,
@@ -76,17 +108,65 @@ class GepaReflectionLM:
76
108
  parent_results=parent_results,
77
109
  )
78
110
  raise
79
- response = result.response
80
- provider_prompt = getattr(result, "prompt", provider_prompt)
81
- self._total_tokens_out += max(1, len(response) // self._CHARS_PER_TOKEN)
82
111
  if self._success_reporter is not None:
83
112
  self._success_reporter(str(provider_prompt))
84
113
  return response
85
114
 
115
+ def reflect(
116
+ self,
117
+ candidate: dict[str, str],
118
+ reflective_dataset: Any,
119
+ components_to_update: list[str],
120
+ ) -> tuple[Any, "GepaReflectionLM"]:
121
+ """Implement GEPA's component-aware ReflectionLM protocol.
122
+
123
+ The callable protocol has no selected-component information. GEPA's
124
+ newer protocol does, so tuples must use it rather than reverse-engineer
125
+ a key from a rendered vendor prompt.
126
+ """
127
+ from gepa.proposer.reflective_mutation.reflection_lm import ReflectionProposal
128
+
129
+ _, parent_results = self._context_supplier()
130
+ full_candidate = candidate
131
+ proposal = ReflectionProposal(new_texts={}, prompts={}, raw_lm_outputs={})
132
+ for component in components_to_update:
133
+ if component not in candidate:
134
+ continue
135
+ parent_text = candidate[component]
136
+ others = {key: value for key, value in full_candidate.items() if key != component}
137
+ config = self._config
138
+ if others:
139
+ template = config.reflection_prompt_template or DEFAULT_REFLECTION_PROMPT_TEMPLATE
140
+ config = replace(
141
+ config,
142
+ reflection_prompt_template=(
143
+ f"{template}\n\nOther instruction-set components (read-only): "
144
+ f"{json.dumps(others, sort_keys=True, ensure_ascii=False)}"
145
+ ),
146
+ )
147
+ provider_prompt = ""
148
+ try:
149
+ provider_prompt = build_reflection_prompt(parent_text, parent_results, config)
150
+ result = reflect_with_provider(parent_text, parent_results, config)
151
+ self._account_usage(getattr(result, "usage", None))
152
+ except Exception as error:
153
+ billable_usage = getattr(error, "usage", None)
154
+ if billable_usage is not None:
155
+ self._account_usage(billable_usage)
156
+ if self._failure_reporter is not None:
157
+ self._failure_reporter(error=error, gepa_prompt=provider_prompt, parent_text=parent_text, parent_results=parent_results)
158
+ raise
159
+ proposal.new_texts[component] = result.response
160
+ proposal.prompts[component] = getattr(result, "prompt", provider_prompt)
161
+ proposal.raw_lm_outputs[component] = result.response
162
+ if self._success_reporter is not None:
163
+ self._success_reporter(str(proposal.prompts[component]))
164
+ return proposal, self
165
+
86
166
 
87
167
  def make_gepa_reflection_lm(
88
168
  *,
89
- context_supplier: Callable[[], tuple[str, list[Any]]],
169
+ context_supplier: Callable[[], tuple[str | dict[str, str], list[Any]]],
90
170
  config: Any,
91
171
  failure_reporter: Callable[..., None] | None = None,
92
172
  success_reporter: Callable[[str], None] | None = None,