orizu 0.5.21 → 0.5.23

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 (87) hide show
  1. package/README.md +46 -15
  2. package/dist/cleanup.d.ts +6 -0
  3. package/dist/cleanup.d.ts.map +1 -0
  4. package/dist/cleanup.js +27 -0
  5. package/dist/gepa-engine-dispatch.d.ts.map +1 -1
  6. package/dist/gepa-engine-dispatch.js +20 -1
  7. package/dist/gepa-python-paths.d.ts.map +1 -1
  8. package/dist/gepa-python-paths.js +25 -6
  9. package/dist/help.d.ts.map +1 -1
  10. package/dist/help.js +37 -15
  11. package/dist/hosted-commands.d.ts +13 -0
  12. package/dist/hosted-commands.d.ts.map +1 -0
  13. package/dist/hosted-commands.js +10 -0
  14. package/dist/hosted-gepa-cli.d.ts +11 -0
  15. package/dist/hosted-gepa-cli.d.ts.map +1 -0
  16. package/dist/hosted-gepa-cli.js +110 -0
  17. package/dist/hosted-gepa-numeric-values.d.ts +3 -0
  18. package/dist/hosted-gepa-numeric-values.d.ts.map +1 -0
  19. package/dist/hosted-gepa-numeric-values.js +49 -0
  20. package/dist/hosted-provider-settings.d.ts +3 -0
  21. package/dist/hosted-provider-settings.d.ts.map +1 -0
  22. package/dist/hosted-provider-settings.js +69 -0
  23. package/dist/hosted-session-cli.d.ts.map +1 -1
  24. package/dist/hosted-session-cli.js +6 -1
  25. package/dist/http.d.ts +1 -0
  26. package/dist/http.d.ts.map +1 -1
  27. package/dist/http.js +7 -0
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +72 -74
  30. package/dist/instruction-set-loader.d.ts.map +1 -1
  31. package/dist/instruction-set-loader.js +57 -5
  32. package/dist/instruction-sets-cli.d.ts +5 -2
  33. package/dist/instruction-sets-cli.d.ts.map +1 -1
  34. package/dist/instruction-sets-cli.js +134 -24
  35. package/dist/optimizations-promote-cli.d.ts +3 -0
  36. package/dist/optimizations-promote-cli.d.ts.map +1 -1
  37. package/dist/optimizations-promote-cli.js +75 -7
  38. package/dist/runner-dir-verify.d.ts.map +1 -1
  39. package/dist/runner-dir-verify.js +4 -7
  40. package/dist/skilled-proposer-bake-report.d.ts +2 -0
  41. package/dist/skilled-proposer-bake-report.d.ts.map +1 -0
  42. package/dist/skilled-proposer-bake-report.js +1 -0
  43. package/dist/skilled-proposer-config.d.ts +2 -0
  44. package/dist/skilled-proposer-config.d.ts.map +1 -0
  45. package/dist/skilled-proposer-config.js +212 -0
  46. package/dist/skilled-proposer-launch.d.ts +26 -0
  47. package/dist/skilled-proposer-launch.d.ts.map +1 -1
  48. package/dist/skilled-proposer-launch.js +103 -6
  49. package/package.json +1 -1
  50. package/scripts/vendor-gepa-python.mjs +81 -12
  51. package/src/cleanup.ts +37 -0
  52. package/src/gepa-engine-dispatch.ts +21 -1
  53. package/src/gepa-python-paths.ts +24 -6
  54. package/src/help.ts +37 -15
  55. package/src/hosted-commands.ts +4 -0
  56. package/src/hosted-gepa-cli.ts +107 -0
  57. package/src/hosted-gepa-numeric-values.ts +60 -0
  58. package/src/hosted-provider-settings.ts +69 -0
  59. package/src/hosted-session-cli.ts +4 -1
  60. package/src/http.ts +8 -0
  61. package/src/index.ts +112 -89
  62. package/src/instruction-set-loader.ts +55 -5
  63. package/src/instruction-sets-cli.ts +173 -19
  64. package/src/optimizations-promote-cli.ts +85 -7
  65. package/src/runner-dir-verify.ts +11 -6
  66. package/src/skilled-proposer-bake-report.ts +1 -0
  67. package/src/skilled-proposer-config.ts +191 -0
  68. package/src/skilled-proposer-launch.ts +96 -7
  69. package/src/skilled-proposer-venv-manager.mjs +44 -27
  70. package/vendor/orizu-gepa/src/orizu_gepa_connector/engine.py +36 -3
  71. package/vendor/orizu-gepa/src/orizu_gepa_connector/instruction_set_loader.py +31 -3
  72. package/vendor/orizu-gepa/src/orizu_gepa_connector/runtime.py +7 -2
  73. package/vendor/orizu-gepa/src/orizu_gepa_connector/skilled_proposer_bridge.py +354 -5
  74. package/vendor/orizu-gepa-python/src/orizu_gepa/cli.py +1 -0
  75. package/vendor/orizu-gepa-python/src/orizu_gepa/client.py +15 -0
  76. package/vendor/skills/orizu-cli/SKILL.md +94 -168
  77. package/vendor/skills/orizu-cli/references/assess-and-plan.md +107 -0
  78. package/vendor/skills/orizu-cli/references/building-judges.md +442 -180
  79. package/vendor/skills/orizu-cli/references/cli-reference.md +69 -39
  80. package/vendor/skills/orizu-cli/references/dataset-design.md +100 -0
  81. package/vendor/skills/orizu-cli/references/eval-strategy.md +64 -0
  82. package/vendor/skills/orizu-cli/references/instructions-after-prompts.md +115 -0
  83. package/vendor/skills/orizu-cli/references/migrate-existing-gepa-setup.md +150 -82
  84. package/vendor/skills/orizu-cli/references/optimization-reports.md +196 -104
  85. package/vendor/skills/orizu-cli/references/optimization-with-gepa.md +57 -37
  86. package/vendor/skills/orizu-cli/references/primer.md +43 -18
  87. package/vendor/skills/orizu-cli/references/prompt-control-plane.md +199 -131
@@ -10,6 +10,7 @@ from __future__ import annotations
10
10
 
11
11
  import atexit
12
12
  import dataclasses
13
+ import hashlib
13
14
  import json
14
15
  import os
15
16
  import shutil
@@ -35,11 +36,16 @@ from orizu_gepa.optimizer import TextGepaConfig
35
36
 
36
37
  _SELECTION_ENV = "ORIZU_CANDIDATE_PROPOSER"
37
38
  _SELECTION_VALUE = "skilled-proposer"
39
+ _CONFIG_ENV = "ORIZU_SKILLED_PROPOSER_CONFIG"
38
40
  _SUPPORTED_ROLES = {"system", "user", "assistant"}
39
41
  _ALLOWED_DSPY_REQUEST_FIELDS = frozenset({"temperature", "max_tokens", "cache"})
40
42
  _DspyBaseLM = dspy.BaseLM if dspy is not None else object
41
43
 
42
44
 
45
+ class _ProposalObservabilityEventFailure(RuntimeError):
46
+ pass
47
+
48
+
43
49
  def _require_dspy() -> Any:
44
50
  if dspy is None:
45
51
  raise RuntimeError("ALI_1505_DSPY_UNAVAILABLE")
@@ -90,6 +96,8 @@ class ProposalObservability:
90
96
  self.durable_failure_root = durable_failure_root
91
97
  self.events: list[dict[str, Any]] = []
92
98
  self.budget = budget or ProposalCallBudget()
99
+ self._event_failure_count = 0
100
+ self._provider_failure_count = 0
93
101
  self._temporary_root: Path | None = None
94
102
 
95
103
  @classmethod
@@ -151,15 +159,50 @@ class ProposalObservability:
151
159
  detail: str,
152
160
  usage: dict[str, int] | None = None,
153
161
  ) -> None:
162
+ self._provider_failure_count += 1
154
163
  self.budget.record(usage)
155
164
  self._write_durable_failure(code=code, detail=detail, correlation_id=event.get("correlation_id"))
156
165
  self._append_event(event)
157
166
 
158
167
  def record_event_failure(self, error: Exception, *, correlation_id: str) -> None:
168
+ self._event_failure_count += 1
159
169
  self._write_durable_failure(
160
170
  code="proposal_observability_event_failed", detail=str(error), correlation_id=correlation_id,
161
171
  )
162
172
 
173
+ def record_proposer_failure(self, error: Exception) -> None:
174
+ self._write_durable_failure(code="proposal_generation_failed", detail=str(error))
175
+
176
+ def record_component_fallback(self, *, component: str, error: Exception) -> None:
177
+ event = {
178
+ "type": "proposal_component_fallback",
179
+ "component": component,
180
+ "policy": "keep",
181
+ "errorType": type(error).__name__,
182
+ "errorMessage": str(error),
183
+ }
184
+ try:
185
+ self._append_event(event)
186
+ except OSError as event_error:
187
+ self._write_durable_failure(
188
+ code="proposal_observability_event_failed", detail=str(event_error),
189
+ )
190
+ raise _ProposalObservabilityEventFailure(
191
+ "proposal_observability_event_failed",
192
+ ) from event_error
193
+
194
+ def write_proposer_identity(self, identity: dict[str, Any]) -> str:
195
+ root = self.event_log_root.parent
196
+ path = root / "proposer.json"
197
+ try:
198
+ root.mkdir(parents=True, exist_ok=True)
199
+ path.write_text(json.dumps(identity, sort_keys=True) + "\n", encoding="utf-8")
200
+ except OSError as error:
201
+ code = "skilled_proposer_identity_write_failed"
202
+ self._write_durable_failure(code=code, detail=str(error))
203
+ raise RuntimeError(code) from error
204
+ return str(path)
205
+
163
206
  def terminal_stats(self) -> dict[str, Any]:
164
207
  return {"call_count": self.budget.call_count, "usage": dict(self.budget.usage)}
165
208
 
@@ -182,6 +225,14 @@ class ProposalObservability:
182
225
  def has_safe_boundary_stop(self) -> bool:
183
226
  return self.budget.has_safe_boundary_stop()
184
227
 
228
+ @property
229
+ def provider_failure_count(self) -> int:
230
+ return self._provider_failure_count
231
+
232
+ @property
233
+ def event_failure_count(self) -> int:
234
+ return self._event_failure_count
235
+
185
236
 
186
237
  def _reject(message: str, *, code: str = "proposal_dspy_request_rejected") -> Any:
187
238
  return _require_dspy().LMError(message, code=code)
@@ -311,9 +362,11 @@ class OrizuSkilledProposerLM(_DspyBaseLM):
311
362
  )
312
363
  except Exception as event_error:
313
364
  self._observability.record_event_failure(event_error, correlation_id=correlation_id)
314
- raise RuntimeError("proposal_observability_event_failed") from event_error
365
+ raise _ProposalObservabilityEventFailure(
366
+ "proposal_observability_event_failed",
367
+ ) from event_error
315
368
  raise dspy_module.LMError(
316
- f"proposal_provider_transport_failed: {error}",
369
+ "proposal_provider_transport_failed",
317
370
  code="proposal_provider_transport_failed",
318
371
  model=self.model,
319
372
  provider="orizu",
@@ -337,7 +390,9 @@ class OrizuSkilledProposerLM(_DspyBaseLM):
337
390
  self._observability.record_success(success_event, usage)
338
391
  except Exception as error:
339
392
  self._observability.record_event_failure(error, correlation_id=correlation_id)
340
- raise RuntimeError("proposal_observability_event_failed") from error
393
+ raise _ProposalObservabilityEventFailure(
394
+ "proposal_observability_event_failed",
395
+ ) from error
341
396
  return dspy_module.LMResponse.from_text(completion.text, model=self.model, usage=usage, cache_hit=False)
342
397
 
343
398
 
@@ -359,6 +414,268 @@ def make_skilled_proposer_bridge(
359
414
  )
360
415
 
361
416
 
417
+ @dataclass(frozen=True)
418
+ class _NormalizedText:
419
+ source: str
420
+ content: str
421
+ sha256: str
422
+ byte_length: int
423
+
424
+
425
+ @dataclass(frozen=True)
426
+ class _NormalizedSkill(_NormalizedText):
427
+ name: str
428
+ description: str | None
429
+
430
+
431
+ @dataclass(frozen=True)
432
+ class _SkilledProposerConfig:
433
+ config_file_sha256: str
434
+ implementation: str
435
+ package_version: str
436
+ config_sha256: str
437
+ skills: tuple[_NormalizedSkill, ...]
438
+ additional_instructions: _NormalizedText | None
439
+ base_instructions: _NormalizedText | None
440
+ max_words: int | None
441
+ max_tokens: int | None
442
+ max_examples: int | None
443
+ on_error: str
444
+
445
+ def identity(self) -> dict[str, Any]:
446
+ return {
447
+ "implementation": self.implementation,
448
+ "packageVersion": self.package_version,
449
+ "configSha256": self.config_sha256,
450
+ "skills": [{
451
+ "name": skill.name,
452
+ "source": skill.source,
453
+ "sha256": skill.sha256,
454
+ "byteLength": skill.byte_length,
455
+ } for skill in self.skills],
456
+ "additionalInstructionsSha256": (
457
+ self.additional_instructions.sha256 if self.additional_instructions else None
458
+ ),
459
+ "baseInstructionsSha256": self.base_instructions.sha256 if self.base_instructions else None,
460
+ "maxWords": self.max_words,
461
+ "maxTokens": self.max_tokens,
462
+ "maxExamples": self.max_examples,
463
+ "onError": self.on_error,
464
+ }
465
+
466
+
467
+ def _config_error(code: str, field_name: str) -> Any:
468
+ raise RuntimeError(f"{code}: {field_name}")
469
+
470
+
471
+ def _strict_object(value: Any, fields: set[str], field_name: str) -> dict[str, Any]:
472
+ if not isinstance(value, dict):
473
+ return _config_error("skilled_proposer_config_invalid_field", field_name)
474
+ unknown = sorted(set(value) - fields)
475
+ if unknown:
476
+ return _config_error(
477
+ "skilled_proposer_config_unknown_field",
478
+ f"{field_name + '.' if field_name else ''}{unknown[0]}",
479
+ )
480
+ missing = sorted(fields - set(value))
481
+ if missing:
482
+ return _config_error(
483
+ "skilled_proposer_config_missing_field",
484
+ f"{field_name + '.' if field_name else ''}{missing[0]}",
485
+ )
486
+ return value
487
+
488
+
489
+ def _required_string(value: Any, field_name: str, *, allow_empty: bool = False) -> str:
490
+ if not isinstance(value, str) or (not allow_empty and not value.strip()):
491
+ return _config_error("skilled_proposer_config_invalid_field", field_name)
492
+ try:
493
+ value.encode("utf-8")
494
+ except UnicodeEncodeError:
495
+ return _config_error("skilled_proposer_config_invalid_unicode", field_name)
496
+ return value
497
+
498
+
499
+ def _positive_int_or_none(value: Any, field_name: str) -> int | None:
500
+ if value is None:
501
+ return None
502
+ if type(value) is not int or value <= 0:
503
+ return _config_error("skilled_proposer_config_invalid_field", field_name)
504
+ return value
505
+
506
+
507
+ def _normalized_text(value: Any, field_name: str) -> _NormalizedText:
508
+ raw = _strict_object(value, {"source", "content", "sha256", "byteLength"}, field_name)
509
+ source = _required_string(raw["source"], f"{field_name}.source")
510
+ content = _required_string(raw["content"], f"{field_name}.content")
511
+ digest = _required_string(raw["sha256"], f"{field_name}.sha256")
512
+ byte_length = raw["byteLength"]
513
+ if (len(digest) != 64 or any(character not in "0123456789abcdef" for character in digest)
514
+ or type(byte_length) is not int or byte_length != len(content.encode("utf-8"))):
515
+ return _config_error("skilled_proposer_config_invalid_field", field_name)
516
+ if hashlib.sha256(content.encode()).hexdigest() != digest:
517
+ return _config_error("skilled_proposer_config_invalid_field", f"{field_name}.sha256")
518
+ return _NormalizedText(source, content, digest, byte_length)
519
+
520
+
521
+ def _optional_normalized_text(value: Any, field_name: str) -> _NormalizedText | None:
522
+ return None if value is None else _normalized_text(value, field_name)
523
+
524
+
525
+ def _normalized_text_payload(value: _NormalizedText | None) -> dict[str, Any] | None:
526
+ if value is None:
527
+ return None
528
+ return {
529
+ "source": value.source, "content": value.content,
530
+ "sha256": value.sha256, "byteLength": value.byte_length,
531
+ }
532
+
533
+
534
+ def _parse_skilled_proposer_config(raw_payload: str) -> _SkilledProposerConfig:
535
+ try:
536
+ parsed = json.loads(raw_payload)
537
+ except (json.JSONDecodeError, TypeError) as error:
538
+ raise RuntimeError("skilled_proposer_config_invalid_json") from error
539
+ fields = {
540
+ "configFileSha256", "schemaVersion", "implementation", "packageVersion", "configSha256", "skills",
541
+ "additionalInstructions", "baseInstructions", "maxWords", "maxTokens", "maxExamples", "onError",
542
+ }
543
+ raw = _strict_object(parsed, fields, "")
544
+ if (type(raw["schemaVersion"]) is not int or raw["schemaVersion"] != 1
545
+ or raw["implementation"] != "cmpnd-ai/skilled-proposer"):
546
+ return _config_error("skilled_proposer_config_invalid_field", "schemaVersion or implementation")
547
+ package_version = _required_string(raw["packageVersion"], "packageVersion")
548
+ config_file_sha256 = _required_string(raw["configFileSha256"], "configFileSha256")
549
+ config_sha256 = _required_string(raw["configSha256"], "configSha256")
550
+ for field_name, digest in (("configFileSha256", config_file_sha256), ("configSha256", config_sha256)):
551
+ if len(digest) != 64 or any(character not in "0123456789abcdef" for character in digest):
552
+ return _config_error("skilled_proposer_config_invalid_field", field_name)
553
+ if not isinstance(raw["skills"], list):
554
+ return _config_error("skilled_proposer_config_invalid_field", "skills")
555
+ skills: list[_NormalizedSkill] = []
556
+ for index, value in enumerate(raw["skills"]):
557
+ field_name = f"skills[{index}]"
558
+ skill_raw = _strict_object(
559
+ value, {"name", "description", "source", "content", "sha256", "byteLength"}, field_name,
560
+ )
561
+ text = _normalized_text({key: skill_raw[key] for key in (
562
+ "source", "content", "sha256", "byteLength",
563
+ )}, field_name)
564
+ name = _required_string(skill_raw["name"], f"{field_name}.name")
565
+ description = skill_raw["description"]
566
+ if description is not None:
567
+ description = _required_string(description, f"{field_name}.description", allow_empty=True)
568
+ skills.append(_NormalizedSkill(
569
+ source=text.source, content=text.content, sha256=text.sha256, byte_length=text.byte_length,
570
+ name=name, description=description,
571
+ ))
572
+ if len({skill.name for skill in skills}) != len(skills):
573
+ return _config_error("skilled_proposer_config_invalid_field", "skills duplicate names")
574
+ on_error = raw["onError"]
575
+ if on_error not in ("keep", "raise"):
576
+ return _config_error("skilled_proposer_config_invalid_field", "onError")
577
+ result = _SkilledProposerConfig(
578
+ config_file_sha256=config_file_sha256,
579
+ implementation=raw["implementation"], package_version=package_version,
580
+ config_sha256=config_sha256, skills=tuple(skills),
581
+ additional_instructions=_optional_normalized_text(raw["additionalInstructions"], "additionalInstructions"),
582
+ base_instructions=_optional_normalized_text(raw["baseInstructions"], "baseInstructions"),
583
+ max_words=_positive_int_or_none(raw["maxWords"], "maxWords"),
584
+ max_tokens=_positive_int_or_none(raw["maxTokens"], "maxTokens"),
585
+ max_examples=_positive_int_or_none(raw["maxExamples"], "maxExamples"),
586
+ on_error=on_error,
587
+ )
588
+ effective = {
589
+ "configFileSha256": result.config_file_sha256,
590
+ "schemaVersion": 1,
591
+ "implementation": result.implementation,
592
+ "packageVersion": result.package_version,
593
+ "skills": [{
594
+ "name": skill.name, "description": skill.description, "source": skill.source,
595
+ "content": skill.content, "sha256": skill.sha256, "byteLength": skill.byte_length,
596
+ } for skill in result.skills],
597
+ "additionalInstructions": _normalized_text_payload(result.additional_instructions),
598
+ "baseInstructions": _normalized_text_payload(result.base_instructions),
599
+ "maxWords": result.max_words, "maxTokens": result.max_tokens,
600
+ "maxExamples": result.max_examples, "onError": result.on_error,
601
+ }
602
+ canonical = json.dumps(effective, ensure_ascii=False, separators=(",", ":"))
603
+ if hashlib.sha256(canonical.encode("utf-8")).hexdigest() != result.config_sha256:
604
+ return _config_error("skilled_proposer_config_hash_mismatch", "configSha256")
605
+ return result
606
+
607
+
608
+ class _CompressionFailureObserver:
609
+ def __init__(self) -> None:
610
+ self.failure_count = 0
611
+ self.last_error: Exception | None = None
612
+
613
+ def on_module_start(self, **_unused: Any) -> None:
614
+ return
615
+
616
+ def on_module_end(self, *, exception: Exception | None = None, **_unused: Any) -> None:
617
+ if exception is not None:
618
+ self.failure_count += 1
619
+ self.last_error = exception
620
+
621
+
622
+ class _ObservableConfiguredProposer:
623
+ def __init__(self, proposer: Any, observability: ProposalObservability, on_error: str) -> None:
624
+ self.proposer = proposer
625
+ self.observability = observability
626
+ self.on_error = on_error
627
+ self.compression_observer = _CompressionFailureObserver()
628
+ proposer.module.compress.callbacks.append(self.compression_observer)
629
+
630
+ def __getattr__(self, name: str) -> Any:
631
+ return getattr(self.proposer, name)
632
+
633
+ def __call__(self, candidate: dict[str, str], reflective_dataset: Any,
634
+ components_to_update: list[str]) -> dict[str, str]:
635
+ results: dict[str, str] = {}
636
+ for component in components_to_update:
637
+ compression_failures_before = self.compression_observer.failure_count
638
+ event_failures_before = self.observability.event_failure_count
639
+ failures_before = self.observability.provider_failure_count
640
+ try:
641
+ proposed = self.proposer(candidate, reflective_dataset, [component])
642
+ except Exception as error:
643
+ if self.observability.event_failure_count > event_failures_before:
644
+ raise RuntimeError("proposal_observability_event_failed") from error
645
+ if isinstance(error, _ProposalObservabilityEventFailure):
646
+ raise RuntimeError("proposal_observability_event_failed") from error
647
+ if isinstance(error, _require_dspy().LMError) and self.on_error == "keep":
648
+ raise
649
+ if self.on_error == "raise":
650
+ code = getattr(error, "code", None)
651
+ if code != "proposal_provider_transport_failed":
652
+ code = "proposal_generation_failed"
653
+ self.observability.record_proposer_failure(error)
654
+ raise RuntimeError(code or "proposal_observability_event_failed") from error
655
+ self.observability.record_component_fallback(component=component, error=error)
656
+ results[component] = candidate[component]
657
+ continue
658
+ if self.observability.event_failure_count > event_failures_before:
659
+ raise RuntimeError("proposal_observability_event_failed")
660
+ provider_failed = self.observability.provider_failure_count > failures_before
661
+ compression_failed = self.compression_observer.failure_count > compression_failures_before
662
+ if compression_failed or provider_failed:
663
+ compression_error = self.compression_observer.last_error if compression_failed else None
664
+ if isinstance(compression_error, _ProposalObservabilityEventFailure):
665
+ raise RuntimeError("proposal_observability_event_failed") from compression_error
666
+ error = (_require_dspy().LMError(
667
+ "proposal_compression_provider_failed: compression provider failure was suppressed upstream",
668
+ code="proposal_compression_provider_failed",
669
+ ) if provider_failed else RuntimeError("proposal_compression_failed"))
670
+ if self.on_error == "raise":
671
+ raise error from compression_error
672
+ self.observability.record_component_fallback(component=component, error=error)
673
+ results[component] = candidate[component]
674
+ else:
675
+ results.update(proposed)
676
+ return results
677
+
678
+
362
679
  def make_skilled_proposer_from_environment(
363
680
  *,
364
681
  config: TextGepaConfig,
@@ -369,7 +686,7 @@ def make_skilled_proposer_from_environment(
369
686
  """Create the opt-in proposer only for the exact selected environment value."""
370
687
  if os.environ.get(_SELECTION_ENV) != _SELECTION_VALUE:
371
688
  return None
372
- from skilled_proposer import SkilledProposer
689
+ from skilled_proposer import Skill, SkilledProposer
373
690
 
374
691
  bridge = make_skilled_proposer_bridge(
375
692
  config=config,
@@ -378,4 +695,36 @@ def make_skilled_proposer_from_environment(
378
695
  endpoint_override=endpoint_override,
379
696
  ssl_cert_file=os.environ.get("SSL_CERT_FILE"),
380
697
  )
381
- return SkilledProposer(prompt_model=bridge)
698
+ raw_proposer_config = os.environ.get(_CONFIG_ENV)
699
+ if raw_proposer_config is None:
700
+ return SkilledProposer(prompt_model=bridge)
701
+ if raw_proposer_config.startswith("@"):
702
+ payload_path = Path(raw_proposer_config[1:])
703
+ try:
704
+ raw_proposer_config = payload_path.read_text(encoding="utf-8")
705
+ except (OSError, UnicodeError) as error:
706
+ raise RuntimeError("skilled_proposer_config_payload_unreadable") from error
707
+ try:
708
+ payload_path.unlink()
709
+ if payload_path.parent.name.startswith("orizu-skilled-proposer-payload-"):
710
+ payload_path.parent.rmdir()
711
+ except OSError:
712
+ pass
713
+ proposer_config = _parse_skilled_proposer_config(raw_proposer_config)
714
+ proposer = SkilledProposer(
715
+ skills=[Skill(
716
+ name=skill.name, description=skill.description, content=skill.content,
717
+ ) for skill in proposer_config.skills],
718
+ additional_instructions=(
719
+ proposer_config.additional_instructions.content
720
+ if proposer_config.additional_instructions else None
721
+ ),
722
+ base_instructions=(proposer_config.base_instructions.content if proposer_config.base_instructions else None),
723
+ max_words=proposer_config.max_words,
724
+ max_tokens=proposer_config.max_tokens,
725
+ max_examples=proposer_config.max_examples,
726
+ on_error="raise",
727
+ prompt_model=bridge,
728
+ )
729
+ observability.write_proposer_identity(proposer_config.identity())
730
+ return _ObservableConfiguredProposer(proposer, observability, proposer_config.on_error)
@@ -300,6 +300,7 @@ def main() -> None:
300
300
  split_set_id=args.split_set_id,
301
301
  train_split=args.train_split,
302
302
  validation_split=args.val_split,
303
+ runner_version_id=prompt_context.runner_version_id,
303
304
  metadata=run_metadata,
304
305
  )
305
306
  print(f"[orizu-gepa] started optimization run {run_id}", flush=True)
@@ -139,6 +139,7 @@ class OrizuClient:
139
139
  train_split: str,
140
140
  validation_split: str,
141
141
  model_config_settings_version_id: str | None = None,
142
+ runner_version_id: str | None = None,
142
143
  metadata: dict[str, Any] | None = None,
143
144
  ) -> str:
144
145
  query = urllib.parse.urlencode({"project": project})
@@ -165,9 +166,23 @@ class OrizuClient:
165
166
  "validationSplitName": validation_split,
166
167
  "metadata": metadata or {},
167
168
  **({"modelConfigSettingsVersionId": model_config_settings_version_id} if model_config_settings_version_id else {}),
169
+ **({"runnerVersionId": runner_version_id} if runner_version_id else {}),
168
170
  })
169
171
  return data["optimization_run_id"]
170
172
 
173
+ def require_hosted_optimization_run(self, *, run_id: str, project: str) -> str:
174
+ """Resolve the existing project-scoped run before any runner executes."""
175
+ query = urllib.parse.urlencode({"project": project})
176
+ encoded_run_id = urllib.parse.quote(run_id, safe="")
177
+ data = self._request(
178
+ "GET",
179
+ f"/api/cli/optimization-runs/{encoded_run_id}?{query}",
180
+ )
181
+ run = data.get("optimizationRun") if isinstance(data, dict) else None
182
+ if not isinstance(run, dict) or run.get("id") != run_id:
183
+ raise RuntimeError("hosted_optimization_run_not_found")
184
+ return run_id
185
+
171
186
  def fetch_exec_context(
172
187
  self,
173
188
  *,