cmpnd 0.22.0__tar.gz → 0.23.0__tar.gz

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 (311) hide show
  1. {cmpnd-0.22.0 → cmpnd-0.23.0}/PKG-INFO +1 -1
  2. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/__init__.py +47 -1
  3. cmpnd-0.23.0/cmpnd/_exporter_pool.py +120 -0
  4. cmpnd-0.23.0/cmpnd/_gepa_patch.py +216 -0
  5. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/_program_patch.py +17 -1
  6. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/_rlm_patch.py +15 -1
  7. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/callback.py +66 -11
  8. cmpnd-0.23.0/cmpnd/client.py +183 -0
  9. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/optimizers/tracker.py +21 -15
  10. cmpnd-0.23.0/docs/client.md +110 -0
  11. {cmpnd-0.22.0 → cmpnd-0.23.0}/docs/sdk-instrumentation.md +2 -0
  12. {cmpnd-0.22.0 → cmpnd-0.23.0}/pyproject.toml +1 -1
  13. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/stress/_reliability.py +35 -0
  14. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/stress/test_optimize_client_committee.py +16 -3
  15. cmpnd-0.23.0/tests/test_client.py +722 -0
  16. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_traces_read.py +7 -3
  17. {cmpnd-0.22.0 → cmpnd-0.23.0}/uv.lock +31 -31
  18. cmpnd-0.22.0/cmpnd/_gepa_patch.py +0 -185
  19. {cmpnd-0.22.0 → cmpnd-0.23.0}/.gitignore +0 -0
  20. {cmpnd-0.22.0 → cmpnd-0.23.0}/CLAUDE.md +0 -0
  21. {cmpnd-0.22.0 → cmpnd-0.23.0}/README.md +0 -0
  22. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/_http.py +0 -0
  23. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/_timespec.py +0 -0
  24. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/cli/__init__.py +0 -0
  25. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/cli/api_client.py +0 -0
  26. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/cli/commands/__init__.py +0 -0
  27. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/cli/commands/admin.py +0 -0
  28. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/cli/commands/assessments.py +0 -0
  29. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/cli/commands/auth.py +0 -0
  30. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/cli/commands/datasets.py +0 -0
  31. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/cli/commands/deployments.py +0 -0
  32. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/cli/commands/evals.py +0 -0
  33. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/cli/commands/login.py +0 -0
  34. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/cli/commands/optimizations.py +0 -0
  35. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/cli/commands/org.py +0 -0
  36. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/cli/commands/programs.py +0 -0
  37. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/cli/commands/provider_credentials.py +0 -0
  38. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/cli/commands/signatures.py +0 -0
  39. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/cli/commands/tool_servers.py +0 -0
  40. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/cli/commands/top.py +0 -0
  41. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/cli/commands/traces.py +0 -0
  42. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/cli/commands/valsets.py +0 -0
  43. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/cli/credentials.py +0 -0
  44. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/cli/deprecation.py +0 -0
  45. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/cli/pagination.py +0 -0
  46. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/cli/shell.py +0 -0
  47. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/cli/trace_render.py +0 -0
  48. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/cli/verbs.py +0 -0
  49. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/configuration.py +0 -0
  50. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/context.py +0 -0
  51. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/dataset_sync.py +0 -0
  52. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/datasets.py +0 -0
  53. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/decorators.py +0 -0
  54. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/deployment.py +0 -0
  55. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/eval_handler.py +0 -0
  56. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/execution.py +0 -0
  57. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/exporter.py +0 -0
  58. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/exporter_http.py +0 -0
  59. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/helpers.py +0 -0
  60. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/identity.py +0 -0
  61. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/imaging.py +0 -0
  62. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/ir/__init__.py +0 -0
  63. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/ir/decode.py +0 -0
  64. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/ir/encode.py +0 -0
  65. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/ir/hash.py +0 -0
  66. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/ir/program.py +0 -0
  67. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/ir/reflection.py +0 -0
  68. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/ir/types.py +0 -0
  69. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/ir/xxh64.py +0 -0
  70. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/mcp_servers.py +0 -0
  71. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/models.py +0 -0
  72. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/optimization.py +0 -0
  73. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/optimizers/__init__.py +0 -0
  74. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/optimizers/gepa.py +0 -0
  75. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/optimizers/gepa_callback.py +0 -0
  76. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/optimizers/resume.py +0 -0
  77. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/packaging.py +0 -0
  78. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/retry.py +0 -0
  79. {cmpnd-0.22.0 → cmpnd-0.23.0}/cmpnd/traces.py +0 -0
  80. {cmpnd-0.22.0 → cmpnd-0.23.0}/docs/2026-04-02-code-review.md +0 -0
  81. {cmpnd-0.22.0 → cmpnd-0.23.0}/docs/cli.md +0 -0
  82. {cmpnd-0.22.0 → cmpnd-0.23.0}/docs/custom-proposer.md +0 -0
  83. {cmpnd-0.22.0 → cmpnd-0.23.0}/docs/plans/2026-04-05-train-val-dataset-capture-design.md +0 -0
  84. {cmpnd-0.22.0 → cmpnd-0.23.0}/docs/plans/2026-04-05-train-val-dataset-capture.md +0 -0
  85. {cmpnd-0.22.0 → cmpnd-0.23.0}/docs/reading-traces.md +0 -0
  86. {cmpnd-0.22.0 → cmpnd-0.23.0}/examples/generalizing_proposer.py +0 -0
  87. {cmpnd-0.22.0 → cmpnd-0.23.0}/examples/local_ollama.py +0 -0
  88. {cmpnd-0.22.0 → cmpnd-0.23.0}/ingest/LINEAGE.md +0 -0
  89. {cmpnd-0.22.0 → cmpnd-0.23.0}/ingest/README.md +0 -0
  90. {cmpnd-0.22.0 → cmpnd-0.23.0}/ingest/cmpnd_ingest/__init__.py +0 -0
  91. {cmpnd-0.22.0 → cmpnd-0.23.0}/ingest/cmpnd_ingest/bodies.py +0 -0
  92. {cmpnd-0.22.0 → cmpnd-0.23.0}/ingest/cmpnd_ingest/exporter_http.py +0 -0
  93. {cmpnd-0.22.0 → cmpnd-0.23.0}/ingest/cmpnd_ingest/protocols.py +0 -0
  94. {cmpnd-0.22.0 → cmpnd-0.23.0}/ingest/cmpnd_ingest/py.typed +0 -0
  95. {cmpnd-0.22.0 → cmpnd-0.23.0}/ingest/cmpnd_ingest/request_log.py +0 -0
  96. {cmpnd-0.22.0 → cmpnd-0.23.0}/ingest/cmpnd_ingest/retry.py +0 -0
  97. {cmpnd-0.22.0 → cmpnd-0.23.0}/ingest/pyproject.toml +0 -0
  98. {cmpnd-0.22.0 → cmpnd-0.23.0}/scripts/audit-sdist.sh +0 -0
  99. {cmpnd-0.22.0 → cmpnd-0.23.0}/scripts/capture_review_run.py +0 -0
  100. {cmpnd-0.22.0 → cmpnd-0.23.0}/scripts/check_release_artifacts.py +0 -0
  101. {cmpnd-0.22.0 → cmpnd-0.23.0}/scripts/fixtures/review_run.json.gz +0 -0
  102. {cmpnd-0.22.0 → cmpnd-0.23.0}/scripts/gepa_resume_bug.py +0 -0
  103. {cmpnd-0.22.0 → cmpnd-0.23.0}/scripts/require_version_bump.py +0 -0
  104. {cmpnd-0.22.0 → cmpnd-0.23.0}/scripts/seed_review_run.py +0 -0
  105. {cmpnd-0.22.0 → cmpnd-0.23.0}/scripts/test-in-linux.sh +0 -0
  106. {cmpnd-0.22.0 → cmpnd-0.23.0}/stubs/dspy/__init__.pyi +0 -0
  107. {cmpnd-0.22.0 → cmpnd-0.23.0}/stubs/dspy/primitives/__init__.pyi +0 -0
  108. {cmpnd-0.22.0 → cmpnd-0.23.0}/stubs/dspy/primitives/prediction.pyi +0 -0
  109. {cmpnd-0.22.0 → cmpnd-0.23.0}/stubs/dspy/utils/__init__.pyi +0 -0
  110. {cmpnd-0.22.0 → cmpnd-0.23.0}/stubs/dspy/utils/callback.pyi +0 -0
  111. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/__init__.py +0 -0
  112. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/_exporter_helpers.py +0 -0
  113. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/_fake_clock.py +0 -0
  114. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/_identity_helpers.py +0 -0
  115. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/conftest.py +0 -0
  116. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/e2e/__init__.py +0 -0
  117. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/e2e/committee_task.py +0 -0
  118. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/e2e/conftest.py +0 -0
  119. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/e2e/data/committee_train.ndjson.gz +0 -0
  120. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/e2e/data/committee_val.ndjson.gz +0 -0
  121. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/e2e/fake_lm.py +0 -0
  122. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/e2e/helpers.py +0 -0
  123. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/e2e/target_capabilities.py +0 -0
  124. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/e2e/test_assessments_cli.py +0 -0
  125. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/e2e/test_capture_prompts.py +0 -0
  126. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/e2e/test_composite_module.py +0 -0
  127. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/e2e/test_dataset_auto_creation.py +0 -0
  128. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/e2e/test_deploy_execute.py +0 -0
  129. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/e2e/test_deploy_programs.py +0 -0
  130. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/e2e/test_edge_cases.py +0 -0
  131. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/e2e/test_example_hash.py +0 -0
  132. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/e2e/test_gepa_optimization.py +0 -0
  133. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/e2e/test_interpreter_spans.py +0 -0
  134. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/e2e/test_lm_accuracy.py +0 -0
  135. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/e2e/test_module_tracing.py +0 -0
  136. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/e2e/test_optimization_lineage.py +0 -0
  137. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/e2e/test_optimization_metric_identity.py +0 -0
  138. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/e2e/test_optimize_failures.py +0 -0
  139. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/e2e/test_optimize_loop.py +0 -0
  140. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/e2e/test_optimize_review_committee.py +0 -0
  141. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/e2e/test_optimize_server_owned_completion.py +0 -0
  142. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/e2e/test_parallel_export.py +0 -0
  143. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/e2e/test_project_attribution.py +0 -0
  144. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/e2e/test_react_tool_spans.py +0 -0
  145. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/e2e/test_save_load.py +0 -0
  146. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/e2e/test_trace_structure.py +0 -0
  147. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/e2e/test_traces_read.py +0 -0
  148. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/e2e/test_user_attribution.py +0 -0
  149. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/e2e/test_valsets_cli.py +0 -0
  150. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/identity_vectors/01_predict_qa.json +0 -0
  151. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/identity_vectors/02_predict_renamed_field.json +0 -0
  152. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/identity_vectors/03_chain_of_thought_qa.json +0 -0
  153. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/identity_vectors/04_predict_richer_types.json +0 -0
  154. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/identity_vectors/05_composite_two_predicts.json +0 -0
  155. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/identity_vectors/06_multichaincomparison.json +0 -0
  156. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/identity_vectors/07_program_of_thought.json +0 -0
  157. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/identity_vectors/08_opaque_module.json +0 -0
  158. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/identity_vectors/09_predict_pydantic_field.json +0 -0
  159. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/identity_vectors/10_react.json +0 -0
  160. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/identity_vectors/11_refine.json +0 -0
  161. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/identity_vectors/12_retrieve.json +0 -0
  162. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/identity_vectors/13_knn.json +0 -0
  163. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/identity_vectors/14_parallel.json +0 -0
  164. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/identity_vectors/README.md +0 -0
  165. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/ingest_conformance.py +0 -0
  166. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/integration/__init__.py +0 -0
  167. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/integration/conftest.py +0 -0
  168. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/integration/hosted_optimize_probe.py +0 -0
  169. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/integration/optimization_run_probe.py +0 -0
  170. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/integration/test_cli_deploy_smoketest.py +0 -0
  171. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/integration/test_logs_renders_traces.py +0 -0
  172. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/integration/test_optimization_run.py +0 -0
  173. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/integration/test_optimizing_progress.py +0 -0
  174. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/integration/test_unhappy_path.py +0 -0
  175. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/load/__init__.py +0 -0
  176. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/load/__main__.py +0 -0
  177. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/load/drain.py +0 -0
  178. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/load/ingest.py +0 -0
  179. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/load/latency_transport.py +0 -0
  180. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/load/rlm_shape.py +0 -0
  181. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/load/sweep.py +0 -0
  182. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/load/test_rlm_shape.py +0 -0
  183. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/parity/__init__.py +0 -0
  184. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/parity/conftest.py +0 -0
  185. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/parity/seed_parity.py +0 -0
  186. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/parity/test_auth.py +0 -0
  187. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/parity/test_datasets.py +0 -0
  188. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/parity/test_deployments.py +0 -0
  189. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/parity/test_evals.py +0 -0
  190. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/parity/test_health.py +0 -0
  191. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/parity/test_modules.py +0 -0
  192. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/parity/test_optimizations.py +0 -0
  193. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/parity/test_programs.py +0 -0
  194. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/parity/test_signatures.py +0 -0
  195. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/parity/test_trace_streaming.py +0 -0
  196. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/parity/test_traces.py +0 -0
  197. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/stress/__init__.py +0 -0
  198. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/stress/_provider.py +0 -0
  199. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/stress/_result.py +0 -0
  200. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/stress/mint_bedrock_token.py +0 -0
  201. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/stress/test_deploy_programs.py +0 -0
  202. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/stress/test_multi_client_stress.py +0 -0
  203. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/stress/test_optimize_loop.py +0 -0
  204. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/stress/test_optimize_loop_wasm.py +0 -0
  205. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/stress/test_optimize_stress.py +0 -0
  206. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/stress/test_provider_roles.py +0 -0
  207. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/stress/test_real_provider_twins.py +0 -0
  208. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/stress/test_reliability.py +0 -0
  209. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/stress/test_reliability_classify.py +0 -0
  210. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/stress/test_rlm_wasm.py +0 -0
  211. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/stress/test_trace_search.py +0 -0
  212. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_callback.py +0 -0
  213. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_candidate_lineage.py +0 -0
  214. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_cli/__init__.py +0 -0
  215. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_cli/conftest.py +0 -0
  216. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_cli/test_admin.py +0 -0
  217. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_cli/test_api_client.py +0 -0
  218. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_cli/test_assessments.py +0 -0
  219. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_cli/test_auth.py +0 -0
  220. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_cli/test_credentials.py +0 -0
  221. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_cli/test_datasets.py +0 -0
  222. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_cli/test_deployments.py +0 -0
  223. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_cli/test_evals.py +0 -0
  224. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_cli/test_login.py +0 -0
  225. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_cli/test_main.py +0 -0
  226. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_cli/test_optimizations.py +0 -0
  227. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_cli/test_org.py +0 -0
  228. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_cli/test_pagination.py +0 -0
  229. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_cli/test_parity.py +0 -0
  230. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_cli/test_programs.py +0 -0
  231. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_cli/test_provider_credentials.py +0 -0
  232. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_cli/test_schema_sample.py +0 -0
  233. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_cli/test_shell.py +0 -0
  234. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_cli/test_signatures.py +0 -0
  235. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_cli/test_tool_servers.py +0 -0
  236. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_cli/test_top.py +0 -0
  237. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_cli/test_traces.py +0 -0
  238. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_cli/test_traces_search_wire.py +0 -0
  239. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_cli/test_valsets.py +0 -0
  240. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_clustering/__init__.py +0 -0
  241. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_clustering/conftest.py +0 -0
  242. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_clustering/test_edge_cases.py +0 -0
  243. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_clustering/test_hash_consistency.py +0 -0
  244. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_clustering/test_type_conversion.py +0 -0
  245. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_cmp551_grouping_identity.py +0 -0
  246. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_configuration.py +0 -0
  247. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_context.py +0 -0
  248. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_context_propagation.py +0 -0
  249. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_dataset_polling.py +0 -0
  250. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_dataset_sync.py +0 -0
  251. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_datasets.py +0 -0
  252. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_decorators.py +0 -0
  253. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_deploy.py +0 -0
  254. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_deploy_mcp_tool_references.py +0 -0
  255. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_eval.py +0 -0
  256. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_eval_example_attribution.py +0 -0
  257. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_eval_start.py +0 -0
  258. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_eval_trace_grouping.py +0 -0
  259. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_exception_paths.py +0 -0
  260. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_execute.py +0 -0
  261. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_exporter.py +0 -0
  262. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_exporter_queue.py +0 -0
  263. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_exporter_real.py +0 -0
  264. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_fake_lm.py +0 -0
  265. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_gepa_callback.py +0 -0
  266. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_gepa_integration.py +0 -0
  267. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_gepa_resume.py +0 -0
  268. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_identity.py +0 -0
  269. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_imaging.py +0 -0
  270. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_import_weight.py +0 -0
  271. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_ingest_conformance.py +0 -0
  272. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_ingest_second_producer.py +0 -0
  273. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_integration.py +0 -0
  274. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_interpreter_callback.py +0 -0
  275. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_ir_encoder_properties.py +0 -0
  276. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_ir_golden_vectors.py +0 -0
  277. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_ir_roundtrip.py +0 -0
  278. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_lm_usage_shapes.py +0 -0
  279. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_mcp_tools.py +0 -0
  280. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_models.py +0 -0
  281. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_optimization_progress.py +0 -0
  282. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_optimization_start.py +0 -0
  283. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_optimize.py +0 -0
  284. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_optimize_budget.py +0 -0
  285. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_optimize_lift.py +0 -0
  286. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_optimize_lms.py +0 -0
  287. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_optimize_proposer.py +0 -0
  288. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_optimize_reflection_lm.py +0 -0
  289. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_optimize_terminal_failure.py +0 -0
  290. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_packaging.py +0 -0
  291. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_packaging_callable_source.py +0 -0
  292. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_packaging_mcp_tools.py +0 -0
  293. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_packaging_program_source.py +0 -0
  294. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_poll_until.py +0 -0
  295. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_program_lineage_persist.py +0 -0
  296. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_project_payload.py +0 -0
  297. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_reflector_properties.py +0 -0
  298. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_reflector_smoke.py +0 -0
  299. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_repro_reactv2_bugs.py +0 -0
  300. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_require_version_bump.py +0 -0
  301. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_retry.py +0 -0
  302. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_reversible_failures.py +0 -0
  303. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_rlm_patch.py +0 -0
  304. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_schemas.py +0 -0
  305. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_seed_review_run.py +0 -0
  306. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_trace_render.py +0 -0
  307. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_tracked_gepa.py +0 -0
  308. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_tracker_enrich.py +0 -0
  309. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_truncation.py +0 -0
  310. {cmpnd-0.22.0 → cmpnd-0.23.0}/tests/test_xxh64.py +0 -0
  311. {cmpnd-0.22.0 → cmpnd-0.23.0}/todo.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cmpnd
3
- Version: 0.22.0
3
+ Version: 0.23.0
4
4
  Summary: DSPy observability and deployment SDK for cmpnd
5
5
  Project-URL: Homepage, https://cmpnd.ai
6
6
  Author-email: cmpnd <hello@cmpnd.ai>
@@ -17,6 +17,7 @@ import importlib.metadata
17
17
  from . import traces
18
18
  from ._http import ApiError
19
19
  from .callback import CmpndCallback, trace_id
20
+ from .client import Client
20
21
  from .configuration import configure, get_config, require_config
21
22
  from .context import (
22
23
  clear_context,
@@ -48,6 +49,11 @@ try:
48
49
  except importlib.metadata.PackageNotFoundError:
49
50
  __version__ = "0.0.0"
50
51
 
52
+ # The exact callback auto_instrument() installed, so de_instrument() removes that
53
+ # one and leaves a manually installed CmpndCallback alone. None when this SDK owns
54
+ # no global callback.
55
+ _auto_instrument_callback: CmpndCallback | None = None
56
+
51
57
  __all__ = [
52
58
  # Configuration
53
59
  "configure",
@@ -57,6 +63,9 @@ __all__ = [
57
63
  "CmpndCallback",
58
64
  # Auto-instrumentation
59
65
  "auto_instrument",
66
+ "de_instrument",
67
+ # Scoped, membership-based tracing
68
+ "Client",
60
69
  # Manual tracing
61
70
  "trace",
62
71
  "start_span",
@@ -139,10 +148,15 @@ def auto_instrument() -> CmpndCallback:
139
148
  # Create callback
140
149
  callback = CmpndCallback()
141
150
 
142
- # Get current callbacks and avoid duplicates
151
+ # Get current callbacks and avoid duplicates. Remember the exact instance this
152
+ # call installs so de_instrument() removes only it — never a CmpndCallback an
153
+ # application installed itself. When one is already present (this call or a
154
+ # manual install), we add nothing and own nothing to remove.
155
+ global _auto_instrument_callback
143
156
  current_callbacks = dspy.settings.get("callbacks", []) or []
144
157
  if not any(isinstance(cb, CmpndCallback) for cb in current_callbacks):
145
158
  dspy.configure(callbacks=[*current_callbacks, callback])
159
+ _auto_instrument_callback = callback
146
160
 
147
161
  # Patch GEPA for automatic optimization tracking
148
162
  from . import _gepa_patch
@@ -163,3 +177,35 @@ def auto_instrument() -> CmpndCallback:
163
177
  _rlm_patch.patch_rlm_tools()
164
178
 
165
179
  return callback
180
+
181
+
182
+ def de_instrument() -> None:
183
+ """Remove global auto_instrument() instrumentation and restore patched classes.
184
+
185
+ The full teardown counterpart to :func:`auto_instrument`: removes the exact
186
+ ``CmpndCallback`` that :func:`auto_instrument` installed and restores
187
+ ``dspy.GEPA``, ``dspy.RLM`` and the program-state ``dump_state`` /
188
+ ``load_state`` to pristine. Idempotent, a no-op when nothing is instrumented,
189
+ and it touches no :class:`Client` — a client's callback is scoped through
190
+ ``dspy.context`` and never lives in the global settings this removes. It also
191
+ leaves a ``CmpndCallback`` an application installed itself in place, removing
192
+ only the one this SDK owns.
193
+ """
194
+ global _auto_instrument_callback
195
+ try:
196
+ import dspy
197
+ except ImportError:
198
+ dspy = None
199
+
200
+ if dspy is not None and _auto_instrument_callback is not None:
201
+ current = list(dspy.settings.get("callbacks", []) or [])
202
+ remaining = [cb for cb in current if cb is not _auto_instrument_callback]
203
+ if len(remaining) != len(current):
204
+ dspy.configure(callbacks=remaining)
205
+ _auto_instrument_callback = None
206
+
207
+ from . import _gepa_patch, _program_patch, _rlm_patch
208
+
209
+ _gepa_patch.unpatch_gepa()
210
+ _program_patch.unpatch_program_state()
211
+ _rlm_patch.unpatch_rlm_tools()
@@ -0,0 +1,120 @@
1
+ """Exporter pool for client-scoped tracing, keyed by credentials.
2
+
3
+ A ``Client`` owns config and a callback, never an OS resource. The one
4
+ long-lived resource in the trace-production path is the exporter's background
5
+ thread, so clients do not each hold one: exporters live here at module scope,
6
+ keyed by ``(endpoint, api_key)``, started on first use and flushed at process
7
+ exit. A client that falls out of scope or is deleted releases nothing; its
8
+ queued traces still drain through the pooled exporter's ``atexit`` flush.
9
+
10
+ The pool honors a process-injected exporter. A worker replaces
11
+ ``cmpnd.exporter._exporter`` with an in-memory collector to read traces back
12
+ with no server, and a test injects a recorder the same way; when the process has
13
+ set one, every client publishes through it rather than opening its own. Only when
14
+ no exporter is injected does a client get a pooled transport of its own.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import atexit
20
+ import threading
21
+ from typing import TYPE_CHECKING, Any
22
+
23
+ from . import configuration, exporter
24
+
25
+ if TYPE_CHECKING:
26
+ from . import exporter_http
27
+
28
+ _pool: dict[tuple[str, str], exporter_http.BatchExporter] = {}
29
+ _pool_lock = threading.Lock()
30
+ _atexit_registered = False
31
+
32
+
33
+ def _key(config: configuration.Config) -> tuple[str, str]:
34
+ return (config.endpoint, config.api_key)
35
+
36
+
37
+ def _pooled(config: configuration.Config) -> exporter_http.BatchExporter:
38
+ """Return the exporter for these credentials, starting it once."""
39
+ global _atexit_registered
40
+ with _pool_lock:
41
+ key = _key(config)
42
+ exp = _pool.get(key)
43
+ if exp is None:
44
+ from . import exporter_http
45
+
46
+ exp = exporter_http.BatchExporter(config)
47
+ exp.start()
48
+ _pool[key] = exp
49
+ if not _atexit_registered:
50
+ atexit.register(flush_all)
51
+ _atexit_registered = True
52
+ return exp
53
+
54
+
55
+ def get_exporter(config: configuration.Config) -> Any:
56
+ """The exporter a client with this config publishes through.
57
+
58
+ A genuine process injection wins; otherwise the pooled transport for these
59
+ credentials. Returns ``None`` only when nothing is injected and the pooled
60
+ transport has not been created — a caller that only reads never forces one.
61
+ """
62
+ injected = _worker_injection()
63
+ if injected is not None:
64
+ return injected
65
+ with _pool_lock:
66
+ return _pool.get(_key(config))
67
+
68
+
69
+ def ensure_exporter_started(config: configuration.Config) -> Any:
70
+ """Return this config's exporter, creating the pooled transport if needed."""
71
+ injected = _worker_injection()
72
+ if injected is not None:
73
+ return injected
74
+ return _pooled(config)
75
+
76
+
77
+ def _worker_injection() -> Any:
78
+ """A worker's directly-injected collector, or None.
79
+
80
+ A worker replaces ``cmpnd.exporter._exporter`` with an in-memory collector and
81
+ never calls ``cmpnd.configure()`` — it is the single tenant, so every trace
82
+ belongs in that collector. ``auto_instrument`` also sets a process exporter,
83
+ but through ``configure()``, and that one must NOT capture a client's traces:
84
+ a client points at its own endpoint with its own key. So an injected exporter
85
+ counts as a worker's only when no cmpnd config is set; once ``configure()``
86
+ has run, a client publishes through its own credential-keyed pool instead.
87
+ """
88
+ injected = exporter.get_exporter()
89
+ if injected is not None and configuration.get_config() is None:
90
+ return injected
91
+ return None
92
+
93
+
94
+ def flush(config: configuration.Config, timeout: float = 5.0) -> bool:
95
+ """Drain this config's exporter so a just-written trace is visible.
96
+
97
+ Returns ``True`` when the drain completed, ``False`` when it timed out or
98
+ there is nothing to drain.
99
+ """
100
+ exp = get_exporter(config)
101
+ if exp is not None and hasattr(exp, "flush"):
102
+ return bool(exp.flush(timeout))
103
+ return False
104
+
105
+
106
+ def flush_all(timeout: float = 5.0) -> None:
107
+ """Flush every pooled exporter — the ``atexit`` drain."""
108
+ with _pool_lock:
109
+ exporters = list(_pool.values())
110
+ for exp in exporters:
111
+ try:
112
+ exp.flush(timeout)
113
+ except Exception: # noqa: BLE001 — best-effort drain at shutdown
114
+ pass
115
+
116
+
117
+ def _reset_for_tests() -> None: # pyright: ignore[reportUnusedFunction] # called from tests/, outside pyright's include
118
+ """Drop pooled exporters. Test-only; the pool is otherwise process-lived."""
119
+ with _pool_lock:
120
+ _pool.clear()
@@ -0,0 +1,216 @@
1
+ """dspy.GEPA monkey-patch for automatic optimization tracking.
2
+
3
+ Extracted from ``cmpnd/__init__.py`` so its first-party imports can be
4
+ module-style (``from . import identity``); basedpyright can't resolve
5
+ ``from . import <submodule>`` inside the package's own ``__init__``.
6
+
7
+ ``patch_gepa()`` is invoked lazily from ``cmpnd.auto_instrument()`` so importing
8
+ ``cmpnd`` doesn't drag in dspy / the optimizers (and transitively litellm).
9
+ """
10
+
11
+ import contextvars
12
+
13
+ from . import context
14
+
15
+ _gepa_patched = False
16
+
17
+ # Set by a client's run_optimization while it owns the run. The class patch reads
18
+ # it at compile entry: when a client owns the optimization, the patch does not
19
+ # also track it — the client records the run on its own account, and a second
20
+ # global-account record would both misattribute and duplicate it.
21
+ _client_owns_optimization: contextvars.ContextVar[bool] = contextvars.ContextVar(
22
+ "cmpnd_client_owns_optimization", default=False
23
+ )
24
+
25
+
26
+ def _gepa_supports_callbacks() -> bool:
27
+ """Check if the installed gepa version supports callbacks in optimize()."""
28
+ try:
29
+ import inspect
30
+
31
+ from gepa import api
32
+
33
+ return "callbacks" in inspect.signature(api.optimize).parameters
34
+ except Exception:
35
+ return False
36
+
37
+
38
+ def _run_tracked_compile(
39
+ gepa, student, *, trainset, valset, run_original, has_callbacks=None, project_tags=None, exporter_override=None
40
+ ):
41
+ """Track one GEPA compile: mint a tracker, start, run the search, finalize.
42
+
43
+ Shared by the auto_instrument class patch (global account) and a client's
44
+ run_optimization (the client's tags + its own transport). ``run_original``
45
+ runs the actual search and returns the compiled program. ``project_tags`` and
46
+ ``exporter_override`` scope the run record to a client when given.
47
+ ``has_callbacks`` is passed by the patch (resolved once at patch time); a
48
+ client leaves it None and it is resolved here.
49
+ """
50
+ import dspy
51
+
52
+ from .optimizers import gepa_callback, tracker
53
+
54
+ if has_callbacks is None:
55
+ has_callbacks = _gepa_supports_callbacks()
56
+
57
+ trk = tracker.OptimizationTracker(optimizer_type="gepa", program_name="")
58
+ if project_tags is not None:
59
+ trk.project_tags = project_tags
60
+ trk._exporter_override = exporter_override
61
+ callback = gepa_callback.CmpndGEPACallback(trk, defer_finalize=True)
62
+
63
+ if has_callbacks:
64
+ old_callback = getattr(gepa, "_cmpnd_gepa_callback", None)
65
+ existing = gepa.gepa_kwargs.get("callbacks") or []
66
+ if old_callback is not None:
67
+ existing = [cb for cb in existing if cb is not old_callback]
68
+ gepa.gepa_kwargs["callbacks"] = existing + [callback]
69
+ gepa._cmpnd_gepa_callback = callback
70
+
71
+ trk = callback._tracker
72
+
73
+ # Build the config dict from the GEPA instance, then hand the live program to
74
+ # the shared enrichment helper (one definition of the optimization_run payload
75
+ # shape, reused by the deploy path).
76
+ config = {}
77
+ for key in ["auto", "max_metric_calls", "max_full_evals", "seed", "log_dir"]:
78
+ val = getattr(gepa, key, None)
79
+ if val is not None:
80
+ config[key] = val
81
+
82
+ lm = None
83
+ try:
84
+ lm = dspy.settings.lm
85
+ except Exception:
86
+ pass
87
+
88
+ # For an `auto` preset max_metric_calls is None until GEPA's first budget
89
+ # event, so estimate the up-front budget now for the initial "running" record.
90
+ max_metric_calls = getattr(gepa, "max_metric_calls", None)
91
+ if not max_metric_calls:
92
+ max_metric_calls = tracker.estimate_max_metric_calls(
93
+ student=student,
94
+ trainset=trainset,
95
+ valset=valset,
96
+ max_metric_calls=max_metric_calls,
97
+ max_full_evals=getattr(gepa, "max_full_evals", None),
98
+ auto=getattr(gepa, "auto", None),
99
+ )
100
+
101
+ tracker.enrich_from_program(
102
+ trk,
103
+ student=student,
104
+ trainset=trainset,
105
+ valset=valset,
106
+ lm=lm,
107
+ reflection_lm=getattr(gepa, "reflection_lm", None),
108
+ config=config,
109
+ metric_fn=getattr(gepa, "metric_fn", None) or getattr(gepa, "metric", None),
110
+ max_metric_calls=max_metric_calls,
111
+ )
112
+
113
+ trk.start()
114
+ context.set_current_optimization_run_id(trk.optimization_run_id)
115
+
116
+ try:
117
+ result = run_original()
118
+
119
+ if not has_callbacks:
120
+ detailed = getattr(result, "detailed_results", None)
121
+ if detailed is not None:
122
+ trk.capture_final_results(detailed)
123
+
124
+ trk.capture_compiled_program(result)
125
+ trk.finalize()
126
+
127
+ # Tag the compiled program with the run id, after finalize (the last point
128
+ # the run can acquire one), so the compiled-from link points at a run that
129
+ # exists.
130
+ if hasattr(result, "__dict__"):
131
+ result._optimization_run_id = str(trk.optimization_run_id)
132
+
133
+ return result
134
+
135
+ # BaseException: a run that ends by ANY route must stop claiming progress on
136
+ # the row start() sent, and a backend model-refusal raises outside Exception.
137
+ except BaseException as e:
138
+ trk.finalize_error(e)
139
+ raise
140
+
141
+ finally:
142
+ context.set_current_optimization_run_id(None)
143
+
144
+
145
+ def patch_gepa() -> None:
146
+ """Monkey-patch dspy.GEPA to auto-inject CmpndGEPACallback.
147
+
148
+ Patches both __init__ (to create callback/tracker shell) and compile
149
+ (to enrich tracker with student/valset info before optimization runs).
150
+
151
+ Uses a module-level flag to avoid double-patching.
152
+ """
153
+ global _gepa_patched
154
+ if _gepa_patched:
155
+ return
156
+
157
+ import dspy
158
+
159
+ if not hasattr(dspy, "GEPA"):
160
+ return
161
+
162
+ # Resolve callback support once, at patch time — not per compile — so the
163
+ # answer matches the installed gepa the patch bound to.
164
+ has_callbacks = _gepa_supports_callbacks()
165
+
166
+ GEPA = dspy.GEPA
167
+ GEPA._cmpnd_original_init = GEPA.__init__ # pyright: ignore[reportAttributeAccessIssue]
168
+ GEPA._cmpnd_original_compile = GEPA.compile # pyright: ignore[reportAttributeAccessIssue]
169
+
170
+ def _patched_init(self, *args, **kwargs):
171
+ # Force track_stats=True so detailed_results is always available
172
+ if "track_stats" not in kwargs:
173
+ kwargs["track_stats"] = True
174
+
175
+ type(self)._cmpnd_original_init(self, *args, **kwargs)
176
+
177
+ def _patched_compile(self, student, *, trainset, teacher=None, valset=None, **kwargs):
178
+ # When a client's run_optimization owns this run it records the run on its
179
+ # own account and has suppressed the global track; run the search only.
180
+ if _client_owns_optimization.get():
181
+ return type(self)._cmpnd_original_compile(
182
+ self, student, trainset=trainset, teacher=teacher, valset=valset, **kwargs
183
+ )
184
+
185
+ # Otherwise track the run on the global account, the same body a client's
186
+ # run_optimization drives with its own tags and transport.
187
+ def _search():
188
+ return type(self)._cmpnd_original_compile(
189
+ self, student, trainset=trainset, teacher=teacher, valset=valset, **kwargs
190
+ )
191
+
192
+ return _run_tracked_compile(
193
+ self, student, trainset=trainset, valset=valset, run_original=_search, has_callbacks=has_callbacks
194
+ )
195
+
196
+ GEPA.__init__ = _patched_init
197
+ GEPA.compile = _patched_compile
198
+ _gepa_patched = True
199
+
200
+
201
+ def unpatch_gepa() -> None:
202
+ """Restore ``dspy.GEPA.__init__`` / ``.compile`` to pristine. Idempotent."""
203
+ global _gepa_patched
204
+ try:
205
+ import dspy
206
+ except ImportError:
207
+ _gepa_patched = False
208
+ return
209
+
210
+ GEPA = getattr(dspy, "GEPA", None)
211
+ if GEPA is not None and hasattr(GEPA, "_cmpnd_original_init"):
212
+ GEPA.__init__ = GEPA._cmpnd_original_init
213
+ GEPA.compile = GEPA._cmpnd_original_compile
214
+ del GEPA._cmpnd_original_init
215
+ del GEPA._cmpnd_original_compile
216
+ _gepa_patched = False
@@ -40,6 +40,9 @@ _LINEAGE_KEY = "_cmpnd"
40
40
  _RUN_ID_FIELD = "optimization_run_id"
41
41
 
42
42
  _patched = False
43
+ # The original dump_state/load_state per patched class, stashed so de_instrument
44
+ # can restore each to pristine. A list of ``(class, name, method)`` or None.
45
+ _originals: list[tuple[Any, str, Any]] | None = None
43
46
 
44
47
 
45
48
  def _inject(state: Any, module: Any) -> Any:
@@ -89,7 +92,7 @@ def patch_program_state() -> None:
89
92
  Idempotent (a module-level flag guards against double-patching); a no-op
90
93
  when dspy isn't installed.
91
94
  """
92
- global _patched
95
+ global _patched, _originals
93
96
  if _patched:
94
97
  return
95
98
 
@@ -105,8 +108,21 @@ def patch_program_state() -> None:
105
108
  # assignment because dspy.*.load_state types as Unknown to basedpyright (its
106
109
  # body imports the untyped dspy.predict.predict) — reflection keeps the swap
107
110
  # off the type checker's attribute-access path across every DSPy version.
111
+ _originals = []
108
112
  for cls, strip in ((dspy.Module, False), (dspy.Predict, True)):
113
+ for name in ("dump_state", "load_state"):
114
+ _originals.append((cls, name, getattr(cls, name)))
109
115
  setattr(cls, "dump_state", _wrap_dump(getattr(cls, "dump_state")))
110
116
  setattr(cls, "load_state", _wrap_load(getattr(cls, "load_state"), strip=strip))
111
117
 
112
118
  _patched = True
119
+
120
+
121
+ def unpatch_program_state() -> None:
122
+ """Restore dump_state/load_state on the patched classes to pristine. Idempotent."""
123
+ global _patched, _originals
124
+ if _originals is not None:
125
+ for cls, name, method in _originals:
126
+ setattr(cls, name, method)
127
+ _originals = None
128
+ _patched = False
@@ -53,6 +53,9 @@ import uuid
53
53
  from typing import Any, Callable
54
54
 
55
55
  _patched = False
56
+ # The original ``RLM._prepare_execution_tools``, stashed so de_instrument can
57
+ # restore the class to pristine. ``(class, method)`` or None when unpatched.
58
+ _original: tuple[Any, Callable[..., Any]] | None = None
56
59
 
57
60
 
58
61
  def _active_cmpnd_callback() -> Any | None:
@@ -118,7 +121,7 @@ def patch_rlm_tools() -> None:
118
121
  Idempotent (a module-level flag guards double-patching); a no-op when dspy
119
122
  or ``dspy.RLM`` isn't available.
120
123
  """
121
- global _patched
124
+ global _patched, _original
122
125
  if _patched:
123
126
  return
124
127
 
@@ -132,6 +135,7 @@ def patch_rlm_tools() -> None:
132
135
  return
133
136
 
134
137
  orig = getattr(rlm_cls, "_prepare_execution_tools")
138
+ _original = (rlm_cls, orig)
135
139
 
136
140
  @functools.wraps(orig)
137
141
  def _prepare_execution_tools(self: Any) -> dict[str, Callable[..., Any]]:
@@ -144,3 +148,13 @@ def patch_rlm_tools() -> None:
144
148
 
145
149
  setattr(rlm_cls, "_prepare_execution_tools", _prepare_execution_tools)
146
150
  _patched = True
151
+
152
+
153
+ def unpatch_rlm_tools() -> None:
154
+ """Restore ``RLM._prepare_execution_tools`` to pristine. Idempotent."""
155
+ global _patched, _original
156
+ if _original is not None:
157
+ rlm_cls, orig = _original
158
+ setattr(rlm_cls, "_prepare_execution_tools", orig)
159
+ _original = None
160
+ _patched = False
@@ -283,9 +283,19 @@ class _CallbackBase:
283
283
  max_input_field_size: int | None = None,
284
284
  max_output_field_size: int | None = None,
285
285
  stream_trace_starts: bool | None = None,
286
+ own_config: configuration.Config | None = None,
286
287
  ) -> None:
288
+ # A client passes its own config here; the auto_instrument callback leaves
289
+ # it None and reads the process-global config live, so a reconfigure()
290
+ # after this callback is built still changes what the global path emits.
291
+ # A client's config, by contrast, is fixed at construction: the tags,
292
+ # credentials and endpoint it stamps and publishes through never follow a
293
+ # later cmpnd.configure(). That fixedness is what keeps a client
294
+ # orthogonal to the global config.
295
+ self._own_config = own_config
296
+
287
297
  # Resolve feature flags: explicit param > config > default
288
- config = configuration.get_config()
298
+ config = own_config or configuration.get_config()
289
299
  self._capture_inputs = (
290
300
  capture_inputs if capture_inputs is not None else (config.capture_inputs if config else True)
291
301
  )
@@ -368,10 +378,53 @@ class _CallbackBase:
368
378
  self._interpreter_lifecycle_starts: dict[str, datetime] = {}
369
379
 
370
380
  # Ensure exporter is running
371
- exporter.ensure_exporter_started()
381
+ self._ensure_exporter()
382
+
383
+ def _resolve_project_tags(self) -> list[str] | None:
384
+ """The project tags this callback stamps on the builders it creates.
385
+
386
+ A client callback stamps its own fixed tags. The auto_instrument callback
387
+ reads the live global config, so a reconfigure() after it was built still
388
+ changes the tags it emits — the behavior the builders' factory default
389
+ already gives, kept here so both paths resolve tags one way.
390
+ """
391
+ if self._own_config is not None:
392
+ return self._own_config.project_tags
393
+ return configuration._current_project_tags()
394
+
395
+ def _stamp(self, builder: Any) -> Any:
396
+ """Stamp this callback's project tags onto a builder it just created."""
397
+ builder.project_tags = self._resolve_project_tags()
398
+ return builder
399
+
400
+ def _current_exporter(self) -> Any:
401
+ """The exporter this callback publishes through.
402
+
403
+ A client publishes through the pool keyed by its own credentials; the
404
+ auto_instrument callback publishes through the process-global exporter.
405
+ Both honor a worker's injected in-memory collector. A client creates its
406
+ pooled transport on demand here, not only at construction: whether it owns
407
+ one depends on the config state (a worker injection vs a cmpnd config),
408
+ and that state can change between constructing the client and its first
409
+ publish — so the publish path resolves and creates, never just reads.
410
+ """
411
+ if self._own_config is not None:
412
+ from . import _exporter_pool
413
+
414
+ return _exporter_pool.ensure_exporter_started(self._own_config)
415
+ return exporter.get_exporter()
416
+
417
+ def _ensure_exporter(self) -> Any:
418
+ """Start this callback's exporter if it is not already running."""
419
+ if self._own_config is not None:
420
+ from . import _exporter_pool
421
+
422
+ return _exporter_pool.ensure_exporter_started(self._own_config)
423
+ return exporter.ensure_exporter_started()
372
424
 
373
425
  def _track_span(self, span: models.SpanBuilder) -> None:
374
426
  """Register a span under its trace for batch payload assembly."""
427
+ self._stamp(span)
375
428
  if span.trace_id:
376
429
  self._trace_spans.setdefault(str(span.trace_id), []).append(span)
377
430
 
@@ -444,7 +497,7 @@ class _ModuleMixin(_CallbackBase):
444
497
  # exporter coalesces a start with its batch trace in one flush, so a
445
498
  # fast trace pays a single insert, not two.
446
499
  if self._stream_trace_starts:
447
- exp = exporter.get_exporter()
500
+ exp = self._current_exporter()
448
501
  if exp:
449
502
  exp.export_trace_start(trace)
450
503
 
@@ -604,7 +657,7 @@ class _ModuleMixin(_CallbackBase):
604
657
 
605
658
  # Export span (only for streaming traces)
606
659
  if self._should_stream_span(span):
607
- exp = exporter.get_exporter()
660
+ exp = self._current_exporter()
608
661
  if exp:
609
662
  exp.export_span(span)
610
663
 
@@ -661,7 +714,7 @@ class _ModuleMixin(_CallbackBase):
661
714
  f"Trace collected for eval: tokens={trace.total_tokens}, cost={trace.total_cost:.4f}"
662
715
  )
663
716
 
664
- exp = exporter.get_exporter()
717
+ exp = self._current_exporter()
665
718
  if exp:
666
719
  if trace.streaming:
667
720
  exp.export_trace_finalize(trace) # Finalize the streaming trace
@@ -784,6 +837,7 @@ class _ModuleMixin(_CallbackBase):
784
837
  start_time=datetime.now(timezone.utc),
785
838
  program_name=instance.__class__.__name__,
786
839
  )
840
+ self._stamp(trace)
787
841
 
788
842
  # Capture signature info for the root module - this identifies the program
789
843
  signature = None
@@ -1056,7 +1110,7 @@ class _LMMixin(_CallbackBase):
1056
1110
  self._trace_spans[trace_id_str].append(span)
1057
1111
 
1058
1112
  if self._should_stream_span(span):
1059
- exp = exporter.get_exporter()
1113
+ exp = self._current_exporter()
1060
1114
  if exp:
1061
1115
  exp.export_span(span)
1062
1116
 
@@ -1099,6 +1153,7 @@ class _LMMixin(_CallbackBase):
1099
1153
  end_time=span.end_time,
1100
1154
  program_name="GEPA",
1101
1155
  )
1156
+ self._stamp(trace)
1102
1157
  # A metric-phase call can start while DSPy's Evaluate span is still on the
1103
1158
  # context stack, so on_lm_start stamped this span with a parent_span_id for
1104
1159
  # a span that isn't in this standalone one-span trace. This span IS the
@@ -1119,7 +1174,7 @@ class _LMMixin(_CallbackBase):
1119
1174
  trace.total_cost = span.cost or 0.0
1120
1175
  trace.span_count = 1
1121
1176
 
1122
- exp = exporter.get_exporter()
1177
+ exp = self._current_exporter()
1123
1178
  if exp:
1124
1179
  trace._batch_spans = [span]
1125
1180
  exp.export_trace(trace)
@@ -1218,7 +1273,7 @@ class _AdapterMixin(_CallbackBase):
1218
1273
  self._trace_spans[trace_id_str].append(span)
1219
1274
 
1220
1275
  if self._should_stream_span(span):
1221
- exp = exporter.get_exporter()
1276
+ exp = self._current_exporter()
1222
1277
  if exp:
1223
1278
  exp.export_span(span)
1224
1279
 
@@ -1300,7 +1355,7 @@ class _ToolMixin(_CallbackBase):
1300
1355
  self._track_span(span)
1301
1356
 
1302
1357
  if self._should_stream_span(span):
1303
- exp = exporter.get_exporter()
1358
+ exp = self._current_exporter()
1304
1359
  if exp:
1305
1360
  exp.export_span(span)
1306
1361
 
@@ -1375,7 +1430,7 @@ class _InterpreterMixin(_CallbackBase):
1375
1430
  self._track_span(span)
1376
1431
 
1377
1432
  if self._should_stream_span(span):
1378
- exp = exporter.get_exporter()
1433
+ exp = self._current_exporter()
1379
1434
  if exp:
1380
1435
  exp.export_span(span)
1381
1436
 
@@ -1467,7 +1522,7 @@ class _InterpreterMixin(_CallbackBase):
1467
1522
 
1468
1523
  self._track_span(span)
1469
1524
  if self._should_stream_span(span):
1470
- exp = exporter.get_exporter()
1525
+ exp = self._current_exporter()
1471
1526
  if exp:
1472
1527
  exp.export_span(span)
1473
1528