cmpnd 0.77.0__tar.gz → 0.78.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 (405) hide show
  1. {cmpnd-0.77.0 → cmpnd-0.78.0}/PKG-INFO +1 -1
  2. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/_gepa_patch.py +45 -0
  3. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/callback.py +38 -5
  4. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/context.py +24 -0
  5. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/optimizers/gepa.py +4 -0
  6. {cmpnd-0.77.0 → cmpnd-0.78.0}/docs/sdk-instrumentation.md +36 -1
  7. {cmpnd-0.77.0 → cmpnd-0.78.0}/pyproject.toml +1 -1
  8. cmpnd-0.78.0/tests/test_optimization_phase_callback.py +196 -0
  9. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_run_attribution_paths.py +101 -0
  10. {cmpnd-0.77.0 → cmpnd-0.78.0}/uv.lock +1 -1
  11. {cmpnd-0.77.0 → cmpnd-0.78.0}/.gitignore +0 -0
  12. {cmpnd-0.77.0 → cmpnd-0.78.0}/AGENTS.md +0 -0
  13. {cmpnd-0.77.0 → cmpnd-0.78.0}/CLAUDE.md +0 -0
  14. {cmpnd-0.77.0 → cmpnd-0.78.0}/README.md +0 -0
  15. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/__init__.py +0 -0
  16. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/_assessments.py +0 -0
  17. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/_authored_import.py +0 -0
  18. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/_deployments.py +0 -0
  19. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/_exporter_pool.py +0 -0
  20. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/_http.py +0 -0
  21. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/_program_patch.py +0 -0
  22. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/_rlm_patch.py +0 -0
  23. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/_runtime_tools.py +0 -0
  24. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/_timespec.py +0 -0
  25. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/_traces.py +0 -0
  26. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/_valsets.py +0 -0
  27. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/authored.py +0 -0
  28. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/cli/__init__.py +0 -0
  29. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/cli/api_client.py +0 -0
  30. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/cli/commands/__init__.py +0 -0
  31. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/cli/commands/admin.py +0 -0
  32. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/cli/commands/assessments.py +0 -0
  33. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/cli/commands/auth.py +0 -0
  34. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/cli/commands/datasets.py +0 -0
  35. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/cli/commands/deployments.py +0 -0
  36. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/cli/commands/evals.py +0 -0
  37. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/cli/commands/login.py +0 -0
  38. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/cli/commands/optimizations.py +0 -0
  39. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/cli/commands/org.py +0 -0
  40. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/cli/commands/programs.py +0 -0
  41. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/cli/commands/provider_credentials.py +0 -0
  42. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/cli/commands/tool_servers.py +0 -0
  43. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/cli/commands/top.py +0 -0
  44. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/cli/commands/traces.py +0 -0
  45. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/cli/commands/valsets.py +0 -0
  46. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/cli/credentials.py +0 -0
  47. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/cli/deprecation.py +0 -0
  48. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/cli/pagination.py +0 -0
  49. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/cli/shell.py +0 -0
  50. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/cli/trace_render.py +0 -0
  51. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/cli/tree_help.py +0 -0
  52. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/cli/verbs.py +0 -0
  53. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/cli/views.py +0 -0
  54. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/client.py +0 -0
  55. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/configuration.py +0 -0
  56. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/credentials.py +0 -0
  57. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/dataset_sync.py +0 -0
  58. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/datasets.py +0 -0
  59. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/decorators.py +0 -0
  60. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/deployment.py +0 -0
  61. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/deployment_metadata.py +0 -0
  62. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/deployment_runs.py +0 -0
  63. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/eval_handler.py +0 -0
  64. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/execution.py +0 -0
  65. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/exporter.py +0 -0
  66. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/exporter_http.py +0 -0
  67. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/helpers.py +0 -0
  68. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/identity.py +0 -0
  69. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/imaging.py +0 -0
  70. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/ir/__init__.py +0 -0
  71. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/ir/decode.py +0 -0
  72. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/ir/encode.py +0 -0
  73. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/ir/hash.py +0 -0
  74. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/ir/program.py +0 -0
  75. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/ir/reflection.py +0 -0
  76. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/ir/types.py +0 -0
  77. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/ir/xxh64.py +0 -0
  78. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/lm_configuration.py +0 -0
  79. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/mcp_servers.py +0 -0
  80. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/models.py +0 -0
  81. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/named_programs.py +0 -0
  82. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/optimization.py +0 -0
  83. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/optimizers/__init__.py +0 -0
  84. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/optimizers/gepa_callback.py +0 -0
  85. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/optimizers/resume.py +0 -0
  86. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/optimizers/tracker.py +0 -0
  87. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/packaging.py +0 -0
  88. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/program_naming.py +0 -0
  89. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/programs.py +0 -0
  90. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/references.py +0 -0
  91. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/retry.py +0 -0
  92. {cmpnd-0.77.0 → cmpnd-0.78.0}/cmpnd/traces.py +0 -0
  93. {cmpnd-0.77.0 → cmpnd-0.78.0}/docs/2026-04-02-code-review.md +0 -0
  94. {cmpnd-0.77.0 → cmpnd-0.78.0}/docs/cli.md +0 -0
  95. {cmpnd-0.77.0 → cmpnd-0.78.0}/docs/client.md +0 -0
  96. {cmpnd-0.77.0 → cmpnd-0.78.0}/docs/custom-proposer.md +0 -0
  97. {cmpnd-0.77.0 → cmpnd-0.78.0}/docs/plans/2026-04-05-train-val-dataset-capture-design.md +0 -0
  98. {cmpnd-0.77.0 → cmpnd-0.78.0}/docs/plans/2026-04-05-train-val-dataset-capture.md +0 -0
  99. {cmpnd-0.77.0 → cmpnd-0.78.0}/docs/reading-traces.md +0 -0
  100. {cmpnd-0.77.0 → cmpnd-0.78.0}/examples/generalizing_proposer.py +0 -0
  101. {cmpnd-0.77.0 → cmpnd-0.78.0}/examples/local_ollama.py +0 -0
  102. {cmpnd-0.77.0 → cmpnd-0.78.0}/ingest/LINEAGE.md +0 -0
  103. {cmpnd-0.77.0 → cmpnd-0.78.0}/ingest/README.md +0 -0
  104. {cmpnd-0.77.0 → cmpnd-0.78.0}/ingest/cmpnd_ingest/__init__.py +0 -0
  105. {cmpnd-0.77.0 → cmpnd-0.78.0}/ingest/cmpnd_ingest/bodies.py +0 -0
  106. {cmpnd-0.77.0 → cmpnd-0.78.0}/ingest/cmpnd_ingest/client_floor.py +0 -0
  107. {cmpnd-0.77.0 → cmpnd-0.78.0}/ingest/cmpnd_ingest/exporter_http.py +0 -0
  108. {cmpnd-0.77.0 → cmpnd-0.78.0}/ingest/cmpnd_ingest/program_names.py +0 -0
  109. {cmpnd-0.77.0 → cmpnd-0.78.0}/ingest/cmpnd_ingest/protocols.py +0 -0
  110. {cmpnd-0.77.0 → cmpnd-0.78.0}/ingest/cmpnd_ingest/py.typed +0 -0
  111. {cmpnd-0.77.0 → cmpnd-0.78.0}/ingest/cmpnd_ingest/request_log.py +0 -0
  112. {cmpnd-0.77.0 → cmpnd-0.78.0}/ingest/cmpnd_ingest/retry.py +0 -0
  113. {cmpnd-0.77.0 → cmpnd-0.78.0}/ingest/cmpnd_ingest/trace_facts.py +0 -0
  114. {cmpnd-0.77.0 → cmpnd-0.78.0}/ingest/pyproject.toml +0 -0
  115. {cmpnd-0.77.0 → cmpnd-0.78.0}/scripts/audit-sdist.sh +0 -0
  116. {cmpnd-0.77.0 → cmpnd-0.78.0}/scripts/capture_review_run.py +0 -0
  117. {cmpnd-0.77.0 → cmpnd-0.78.0}/scripts/check-pip-install.sh +0 -0
  118. {cmpnd-0.77.0 → cmpnd-0.78.0}/scripts/check_release_artifacts.py +0 -0
  119. {cmpnd-0.77.0 → cmpnd-0.78.0}/scripts/fixtures/review_run.json.gz +0 -0
  120. {cmpnd-0.77.0 → cmpnd-0.78.0}/scripts/gepa_resume_bug.py +0 -0
  121. {cmpnd-0.77.0 → cmpnd-0.78.0}/scripts/install-test-env.sh +0 -0
  122. {cmpnd-0.77.0 → cmpnd-0.78.0}/scripts/release-gate.sh +0 -0
  123. {cmpnd-0.77.0 → cmpnd-0.78.0}/scripts/release-real-models.sh +0 -0
  124. {cmpnd-0.77.0 → cmpnd-0.78.0}/scripts/require_version_bump.py +0 -0
  125. {cmpnd-0.77.0 → cmpnd-0.78.0}/scripts/seed_review_run.py +0 -0
  126. {cmpnd-0.77.0 → cmpnd-0.78.0}/scripts/selfhost_compat.py +0 -0
  127. {cmpnd-0.77.0 → cmpnd-0.78.0}/scripts/test-in-linux.sh +0 -0
  128. {cmpnd-0.77.0 → cmpnd-0.78.0}/stubs/dspy/__init__.pyi +0 -0
  129. {cmpnd-0.77.0 → cmpnd-0.78.0}/stubs/dspy/primitives/__init__.pyi +0 -0
  130. {cmpnd-0.77.0 → cmpnd-0.78.0}/stubs/dspy/primitives/prediction.pyi +0 -0
  131. {cmpnd-0.77.0 → cmpnd-0.78.0}/stubs/dspy/utils/__init__.pyi +0 -0
  132. {cmpnd-0.77.0 → cmpnd-0.78.0}/stubs/dspy/utils/callback.pyi +0 -0
  133. {cmpnd-0.77.0 → cmpnd-0.78.0}/stubs/dspy/utils/dummies.pyi +0 -0
  134. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/__init__.py +0 -0
  135. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/_credential_route.py +0 -0
  136. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/_deployment_signature_helpers.py +0 -0
  137. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/_exporter_helpers.py +0 -0
  138. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/_fake_clock.py +0 -0
  139. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/_identity_helpers.py +0 -0
  140. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/_resolve_refs.py +0 -0
  141. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/_traced_run.py +0 -0
  142. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/conftest.py +0 -0
  143. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/__init__.py +0 -0
  144. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/committee_task.py +0 -0
  145. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/conftest.py +0 -0
  146. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/continuation_task.py +0 -0
  147. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/data/committee_train.ndjson.gz +0 -0
  148. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/data/committee_val.ndjson.gz +0 -0
  149. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/fake_lm.py +0 -0
  150. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/helpers.py +0 -0
  151. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/target_capabilities.py +0 -0
  152. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_assessments_cli.py +0 -0
  153. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_capture_prompts.py +0 -0
  154. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_composite_module.py +0 -0
  155. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_dataset_auto_creation.py +0 -0
  156. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_deploy_execute.py +0 -0
  157. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_deploy_pandas.py +0 -0
  158. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_deploy_programs.py +0 -0
  159. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_edge_cases.py +0 -0
  160. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_example_hash.py +0 -0
  161. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_gepa_optimization.py +0 -0
  162. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_interpreter_spans.py +0 -0
  163. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_lm_accuracy.py +0 -0
  164. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_module_tracing.py +0 -0
  165. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_optimization_lineage.py +0 -0
  166. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_optimization_metric_identity.py +0 -0
  167. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_optimize_authored_metric.py +0 -0
  168. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_optimize_continuation.py +0 -0
  169. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_optimize_custom_proposer.py +0 -0
  170. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_optimize_failures.py +0 -0
  171. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_optimize_loop.py +0 -0
  172. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_optimize_review_committee.py +0 -0
  173. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_optimize_server_owned_completion.py +0 -0
  174. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_parallel_export.py +0 -0
  175. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_project_attribution.py +0 -0
  176. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_react_tool_spans.py +0 -0
  177. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_save_load.py +0 -0
  178. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_trace_structure.py +0 -0
  179. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_traces_read.py +0 -0
  180. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_user_attribution.py +0 -0
  181. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/e2e/test_valsets_cli.py +0 -0
  182. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/identity_vectors/01_predict_qa.json +0 -0
  183. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/identity_vectors/02_predict_renamed_field.json +0 -0
  184. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/identity_vectors/03_chain_of_thought_qa.json +0 -0
  185. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/identity_vectors/04_predict_richer_types.json +0 -0
  186. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/identity_vectors/05_composite_two_predicts.json +0 -0
  187. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/identity_vectors/06_multichaincomparison.json +0 -0
  188. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/identity_vectors/07_program_of_thought.json +0 -0
  189. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/identity_vectors/08_opaque_module.json +0 -0
  190. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/identity_vectors/09_predict_pydantic_field.json +0 -0
  191. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/identity_vectors/10_react.json +0 -0
  192. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/identity_vectors/11_refine.json +0 -0
  193. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/identity_vectors/12_retrieve.json +0 -0
  194. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/identity_vectors/13_knn.json +0 -0
  195. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/identity_vectors/14_parallel.json +0 -0
  196. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/identity_vectors/README.md +0 -0
  197. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/ingest_conformance.py +0 -0
  198. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/integration/__init__.py +0 -0
  199. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/integration/authored_optimize/judge.py +0 -0
  200. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/integration/authored_optimize/optimize_hosted.py +0 -0
  201. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/integration/conftest.py +0 -0
  202. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/integration/hosted_mcp_tool_optimize_probe.py +0 -0
  203. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/integration/hosted_optimize_probe.py +0 -0
  204. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/integration/optimization_run_probe.py +0 -0
  205. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/integration/test_cli_deploy_smoketest.py +0 -0
  206. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/integration/test_logs_renders_traces.py +0 -0
  207. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/integration/test_optimization_run.py +0 -0
  208. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/integration/test_optimizing_progress.py +0 -0
  209. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/integration/test_unhappy_path.py +0 -0
  210. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/load/__init__.py +0 -0
  211. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/load/__main__.py +0 -0
  212. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/load/drain.py +0 -0
  213. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/load/ingest.py +0 -0
  214. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/load/latency_transport.py +0 -0
  215. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/load/rlm_shape.py +0 -0
  216. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/load/rlm_size_report.py +0 -0
  217. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/load/sweep.py +0 -0
  218. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/load/test_rlm_shape.py +0 -0
  219. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/load/test_rlm_size_report.py +0 -0
  220. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/parity/__init__.py +0 -0
  221. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/parity/conftest.py +0 -0
  222. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/parity/seed_parity.py +0 -0
  223. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/parity/test_auth.py +0 -0
  224. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/parity/test_datasets.py +0 -0
  225. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/parity/test_deployments.py +0 -0
  226. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/parity/test_evals.py +0 -0
  227. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/parity/test_health.py +0 -0
  228. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/parity/test_modules.py +0 -0
  229. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/parity/test_optimizations.py +0 -0
  230. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/parity/test_programs.py +0 -0
  231. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/parity/test_signatures.py +0 -0
  232. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/parity/test_trace_streaming.py +0 -0
  233. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/parity/test_traces.py +0 -0
  234. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/real_models/__init__.py +0 -0
  235. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/real_models/_provider.py +0 -0
  236. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/real_models/_readiness.py +0 -0
  237. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/real_models/_reliability.py +0 -0
  238. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/real_models/_result.py +0 -0
  239. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/real_models/mint_bedrock_token.py +0 -0
  240. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/real_models/test_bedrock_converse.py +0 -0
  241. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/real_models/test_deploy_programs.py +0 -0
  242. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/real_models/test_multi_client_stress.py +0 -0
  243. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/real_models/test_optimize_client_committee.py +0 -0
  244. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/real_models/test_optimize_continuation.py +0 -0
  245. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/real_models/test_optimize_loop.py +0 -0
  246. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/real_models/test_optimize_loop_wasm.py +0 -0
  247. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/real_models/test_optimize_stress.py +0 -0
  248. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/real_models/test_provider_roles.py +0 -0
  249. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/real_models/test_readiness.py +0 -0
  250. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/real_models/test_real_provider_twins.py +0 -0
  251. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/real_models/test_reliability.py +0 -0
  252. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/real_models/test_reliability_classify.py +0 -0
  253. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/real_models/test_rlm_wasm.py +0 -0
  254. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/real_models/test_trace_search.py +0 -0
  255. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/span_reference_fixture.py +0 -0
  256. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_adapter_reentrancy.py +0 -0
  257. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_assessments_api.py +0 -0
  258. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_authored.py +0 -0
  259. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_authored_binding_credentials.py +0 -0
  260. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_authored_bundle_collisions.py +0 -0
  261. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_authored_metric_contract.py +0 -0
  262. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_authored_named_models.py +0 -0
  263. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_authored_optimize.py +0 -0
  264. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_callback.py +0 -0
  265. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_candidate_lineage.py +0 -0
  266. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_cli/__init__.py +0 -0
  267. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_cli/conftest.py +0 -0
  268. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_cli/test_admin.py +0 -0
  269. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_cli/test_api_client.py +0 -0
  270. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_cli/test_assessments.py +0 -0
  271. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_cli/test_auth.py +0 -0
  272. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_cli/test_credentials.py +0 -0
  273. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_cli/test_datasets.py +0 -0
  274. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_cli/test_deployment_runs.py +0 -0
  275. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_cli/test_deployments.py +0 -0
  276. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_cli/test_evals.py +0 -0
  277. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_cli/test_login.py +0 -0
  278. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_cli/test_main.py +0 -0
  279. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_cli/test_optimizations.py +0 -0
  280. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_cli/test_org.py +0 -0
  281. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_cli/test_pagination.py +0 -0
  282. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_cli/test_parity.py +0 -0
  283. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_cli/test_programs.py +0 -0
  284. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_cli/test_provider_credentials.py +0 -0
  285. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_cli/test_schema_sample.py +0 -0
  286. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_cli/test_shell.py +0 -0
  287. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_cli/test_tool_servers.py +0 -0
  288. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_cli/test_top.py +0 -0
  289. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_cli/test_traces.py +0 -0
  290. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_cli/test_traces_program_and_module.py +0 -0
  291. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_cli/test_traces_search_wire.py +0 -0
  292. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_cli/test_tree_help.py +0 -0
  293. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_cli/test_valsets.py +0 -0
  294. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_client.py +0 -0
  295. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_client_floor.py +0 -0
  296. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_client_program_names.py +0 -0
  297. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_clustering/__init__.py +0 -0
  298. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_clustering/conftest.py +0 -0
  299. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_clustering/test_edge_cases.py +0 -0
  300. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_clustering/test_hash_consistency.py +0 -0
  301. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_clustering/test_type_conversion.py +0 -0
  302. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_cmp551_grouping_identity.py +0 -0
  303. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_configuration.py +0 -0
  304. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_context.py +0 -0
  305. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_context_propagation.py +0 -0
  306. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_credential_kwargs.py +0 -0
  307. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_credential_selection.py +0 -0
  308. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_credential_values.py +0 -0
  309. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_dataset_polling.py +0 -0
  310. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_dataset_sync.py +0 -0
  311. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_datasets.py +0 -0
  312. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_decision_client_spans.py +0 -0
  313. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_decorators.py +0 -0
  314. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_deploy.py +0 -0
  315. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_deploy_mcp_tool_references.py +0 -0
  316. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_deploy_program_id.py +0 -0
  317. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_deployment_metadata.py +0 -0
  318. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_deployment_runs.py +0 -0
  319. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_deployments_read.py +0 -0
  320. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_eval.py +0 -0
  321. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_eval_example_attribution.py +0 -0
  322. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_eval_program_id.py +0 -0
  323. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_eval_start.py +0 -0
  324. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_eval_trace_grouping.py +0 -0
  325. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_exception_paths.py +0 -0
  326. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_execute.py +0 -0
  327. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_execute_run_identity.py +0 -0
  328. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_exporter.py +0 -0
  329. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_exporter_program_names.py +0 -0
  330. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_exporter_queue.py +0 -0
  331. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_exporter_real.py +0 -0
  332. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_fake_lm.py +0 -0
  333. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_gepa_callback.py +0 -0
  334. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_gepa_integration.py +0 -0
  335. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_gepa_output_normalization.py +0 -0
  336. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_gepa_resume.py +0 -0
  337. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_identity.py +0 -0
  338. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_imaging.py +0 -0
  339. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_import_weight.py +0 -0
  340. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_ingest_conformance.py +0 -0
  341. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_ingest_second_producer.py +0 -0
  342. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_integration.py +0 -0
  343. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_interpreter_callback.py +0 -0
  344. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_ir_encoder_properties.py +0 -0
  345. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_ir_golden_vectors.py +0 -0
  346. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_ir_roundtrip.py +0 -0
  347. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_lm15_native_calls.py +0 -0
  348. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_lm_configuration.py +0 -0
  349. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_lm_usage_shapes.py +0 -0
  350. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_mcp_tools.py +0 -0
  351. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_models.py +0 -0
  352. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_multi_client_stress_readiness.py +0 -0
  353. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_no_signature_hash_on_the_wire.py +0 -0
  354. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_optimization_program_names.py +0 -0
  355. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_optimization_progress.py +0 -0
  356. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_optimization_start.py +0 -0
  357. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_optimize.py +0 -0
  358. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_optimize_budget.py +0 -0
  359. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_optimize_lift.py +0 -0
  360. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_optimize_lms.py +0 -0
  361. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_optimize_proposer.py +0 -0
  362. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_optimize_reflection_lm.py +0 -0
  363. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_optimize_terminal_failure.py +0 -0
  364. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_packaging.py +0 -0
  365. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_packaging_callable_source.py +0 -0
  366. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_packaging_mcp_tools.py +0 -0
  367. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_packaging_program_source.py +0 -0
  368. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_poll_until.py +0 -0
  369. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_program_id_deprecation.py +0 -0
  370. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_program_identity.py +0 -0
  371. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_program_lineage_persist.py +0 -0
  372. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_program_names_in_deploy.py +0 -0
  373. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_program_naming.py +0 -0
  374. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_programs_create.py +0 -0
  375. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_project_payload.py +0 -0
  376. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_real_provider_twins_readiness.py +0 -0
  377. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_reflector_properties.py +0 -0
  378. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_reflector_smoke.py +0 -0
  379. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_release_gate_script.py +0 -0
  380. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_release_real_models_script.py +0 -0
  381. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_replaced_names.py +0 -0
  382. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_repro_reactv2_bugs.py +0 -0
  383. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_require_version_bump.py +0 -0
  384. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_retry.py +0 -0
  385. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_reversible_failures.py +0 -0
  386. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_rlm_patch.py +0 -0
  387. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_runtime_tools.py +0 -0
  388. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_schemas.py +0 -0
  389. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_seal_lm_credential.py +0 -0
  390. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_seed_review_run.py +0 -0
  391. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_signature_inputs.py +0 -0
  392. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_span_references.py +0 -0
  393. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_span_sent_once.py +0 -0
  394. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_system_one.py +0 -0
  395. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_trace_context_concurrency.py +0 -0
  396. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_trace_fact_wire_contract.py +0 -0
  397. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_trace_facts.py +0 -0
  398. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_trace_render.py +0 -0
  399. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_traces_read.py +0 -0
  400. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_tracked_gepa.py +0 -0
  401. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_tracker_enrich.py +0 -0
  402. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_truncation.py +0 -0
  403. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_upload_auth_headers.py +0 -0
  404. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_valsets_api.py +0 -0
  405. {cmpnd-0.77.0 → cmpnd-0.78.0}/tests/test_xxh64.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cmpnd
3
- Version: 0.77.0
3
+ Version: 0.78.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>
@@ -9,6 +9,10 @@ module-style (``from . import identity``); basedpyright can't resolve
9
9
  """
10
10
 
11
11
  import contextvars
12
+ import functools
13
+ from collections.abc import Callable
14
+
15
+ from . import context
12
16
 
13
17
  _gepa_patched = False
14
18
 
@@ -39,6 +43,43 @@ def _gepa_supports_callbacks() -> bool:
39
43
  return False
40
44
 
41
45
 
46
+ # Marks a metric wrapper _judge_metric_calls installed, so a compile nested in another
47
+ # (TrackedGEPA under auto_instrument) leaves the outer wrapper in place. An attribute
48
+ # on our own wrapper, read with getattr, because the user's metric may be any
49
+ # callable: a dataclass instance is unhashable, so no set or dict can hold it.
50
+ _JUDGED_METRIC_MARK = "_cmpnd_judges_metric_calls"
51
+
52
+
53
+ def _judge_metric_calls(gepa) -> Callable[[], None]:
54
+ """Run each of ``gepa``'s metric calls as the metric phase; return the undo.
55
+
56
+ dspy.GEPA reads its metric from ``metric_fn`` once its compile starts, so
57
+ swapping in a wrapper for the compile's span reaches every metric call, those
58
+ DSPy Evaluate makes and those GEPA's feedback makes. A trace a grading module
59
+ roots inside a call is then tagged ``optimization_phase: metric``, as a hosted
60
+ run's judge traces are, while the rollout the metric grades ran before it and
61
+ carries no phase. The wrapper keeps the metric's signature (``functools.wraps``)
62
+ for DSPy's checks of it, and the undo restores the optimizer's own metric.
63
+ """
64
+ metric = getattr(gepa, "metric_fn", None)
65
+ if not callable(metric) or getattr(metric, _JUDGED_METRIC_MARK, False) is True:
66
+ return lambda: None
67
+
68
+ @functools.wraps(metric)
69
+ def judged_metric(*args, **kwargs):
70
+ with context.optimization_phase("metric"):
71
+ return metric(*args, **kwargs)
72
+
73
+ setattr(judged_metric, _JUDGED_METRIC_MARK, True)
74
+ gepa.metric_fn = judged_metric
75
+
76
+ def restore() -> None:
77
+ if getattr(gepa, "metric_fn", None) is judged_metric:
78
+ gepa.metric_fn = metric
79
+
80
+ return restore
81
+
82
+
42
83
  def _run_tracked_compile(
43
84
  gepa,
44
85
  student,
@@ -137,8 +178,10 @@ def _run_tracked_compile(
137
178
  trk.generated_program_name = program_naming.generated_program_name(student)
138
179
 
139
180
  restore_attribution = callback.start_trace_attribution()
181
+ restore_metric = None
140
182
 
141
183
  try:
184
+ restore_metric = _judge_metric_calls(gepa)
142
185
  result = run_original()
143
186
 
144
187
  if not has_callbacks:
@@ -164,6 +207,8 @@ def _run_tracked_compile(
164
207
  raise
165
208
 
166
209
  finally:
210
+ if restore_metric is not None:
211
+ restore_metric()
167
212
  restore_attribution()
168
213
 
169
214
 
@@ -517,6 +517,11 @@ def _finish_reason(response: Any) -> str | None:
517
517
  return "stop"
518
518
 
519
519
 
520
+ # The `optimization.role` an LM call in each GEPA phase is priced under. A metric scores a
521
+ # rollout, so its calls are rollout spend; reflection proposes the next candidate.
522
+ _OPTIMIZATION_PHASE_ROLES = {"metric": "rollout", "reflection": "reflection"}
523
+
524
+
520
525
  class _CallbackBase:
521
526
  """Shared state and span-tracking primitives for the CmpndCallback mixins."""
522
527
 
@@ -532,6 +537,7 @@ class _CallbackBase:
532
537
  max_output_field_size: int | None = None,
533
538
  stream_trace_starts: bool | None = None,
534
539
  own_config: configuration.Config | None = None,
540
+ optimization_phase: str | None = None,
535
541
  ) -> None:
536
542
  # A client passes its own config here; the auto_instrument callback leaves
537
543
  # it None and reads the process-global config live, so a reconfigure()
@@ -541,6 +547,19 @@ class _CallbackBase:
541
547
  # later cmpnd.configure(). That fixedness is what keeps a client
542
548
  # orthogonal to the global config.
543
549
  self._own_config = own_config
550
+ # The optimization phase this callback traces for a run it does not own: an
551
+ # executor that scores a hosted rollout with the author's metric ("metric") or runs
552
+ # the author's GEPA proposer ("reflection"). Such a process knows the program the
553
+ # run optimizes and not the run's id, which the ingest key it pushes with decides.
554
+ # With a phase set, an LM call outside any trace becomes a standalone GEPA trace as
555
+ # a local optimization's judge and reflection calls do, and a trace a module opens
556
+ # is tagged with the phase. None is every other caller.
557
+ if optimization_phase not in (None, *_OPTIMIZATION_PHASE_ROLES):
558
+ raise ValueError(
559
+ f"optimization_phase must be one of {sorted(_OPTIMIZATION_PHASE_ROLES)} or None, "
560
+ f"got {optimization_phase!r}"
561
+ )
562
+ self._optimization_phase = optimization_phase
544
563
  # Set once this callback has warned that a declared name replaced the program
545
564
  # id its config read from the environment, so the warning appears once per client.
546
565
  self._warned_name_over_id = False
@@ -1348,6 +1367,12 @@ class _ModuleMixin(_CallbackBase):
1348
1367
  # Apply default tags
1349
1368
  if config and config.default_tags:
1350
1369
  trace.tags.update(config.default_tags)
1370
+ # A trace opened while this callback traces a phase, or while a local run
1371
+ # calls its metric, is the metric's or the proposer's, never a rollout, even
1372
+ # when its module is the program's own class.
1373
+ phase = self._optimization_phase or context.get_current_optimization_phase()
1374
+ if phase is not None:
1375
+ trace.tags["optimization_phase"] = phase
1351
1376
 
1352
1377
  return trace
1353
1378
 
@@ -1401,7 +1426,9 @@ class _LMMixin(_CallbackBase):
1401
1426
  span.model_name = getattr(instance, "model", None)
1402
1427
  span.model_provider = helpers.infer_provider(instance)
1403
1428
 
1404
- if self._active_optimization_run_id is not None or context.get_current_optimization_run_id() is not None:
1429
+ if self._optimization_phase is not None:
1430
+ span.attributes["optimization.role"] = _OPTIMIZATION_PHASE_ROLES[self._optimization_phase]
1431
+ elif self._active_optimization_run_id is not None or context.get_current_optimization_run_id() is not None:
1405
1432
  span.attributes["optimization.role"] = "rollout"
1406
1433
 
1407
1434
  # The usage fields read one named value each out of the raw kwargs. The
@@ -1603,7 +1630,9 @@ class _LMMixin(_CallbackBase):
1603
1630
  # must NOT also be added to _orphaned_lm_usage / untraced_lm_usage — that
1604
1631
  # would double-count them in the optimization total.
1605
1632
  current_trace = context.get_current_trace()
1606
- if current_trace is None and self._active_optimization_run_id is not None:
1633
+ if current_trace is None and (
1634
+ self._active_optimization_run_id is not None or self._optimization_phase is not None
1635
+ ):
1607
1636
  self._emit_optimization_lm_trace(span)
1608
1637
  return # handled as a standalone GEPA-process trace
1609
1638
 
@@ -1644,14 +1673,18 @@ class _LMMixin(_CallbackBase):
1644
1673
  carries the distinction. Each becomes a standalone one-span
1645
1674
  trace so the prompt, response, tokens and cost are visible alongside the
1646
1675
  student traces instead of being reduced to an aggregate token count.
1676
+
1677
+ A callback built with an ``optimization_phase`` takes its phase from that
1678
+ instead, and may have no run id: it traces the metric or the proposer of a
1679
+ run whose id the ingest key the trace is pushed with decides.
1647
1680
  """
1648
1681
  run_id = self._active_optimization_run_id
1649
- if not run_id:
1682
+ if not run_id and self._optimization_phase is None:
1650
1683
  return
1651
1684
 
1652
- phase = "metric" if self._active_eval_run is not None else "reflection"
1685
+ phase = self._optimization_phase or ("metric" if self._active_eval_run is not None else "reflection")
1653
1686
  span.attributes["gepa.phase"] = phase
1654
- span.attributes["optimization.role"] = "reflection" if phase == "reflection" else "rollout"
1687
+ span.attributes["optimization.role"] = _OPTIMIZATION_PHASE_ROLES[phase]
1655
1688
 
1656
1689
  trace = models.TraceBuilder(
1657
1690
  trace_id=span.trace_id or uuid4(),
@@ -2,6 +2,8 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from collections.abc import Generator
6
+ from contextlib import contextmanager
5
7
  from contextvars import ContextVar
6
8
  from datetime import datetime, timezone
7
9
  from typing import TYPE_CHECKING, Any
@@ -27,6 +29,12 @@ _current_eval_run: ContextVar[models.EvalRunBuilder | None] = ContextVar("cmpnd_
27
29
  # evals). Prefixed text id (`op_…`), minted via cmpnd.identity.
28
30
  _current_optimization_run_id: ContextVar[str | None] = ContextVar("cmpnd_current_optimization_run_id", default=None)
29
31
 
32
+ # The optimization phase the code running now serves, when that code is an
33
+ # optimizer's metric rather than the program it optimizes. A local GEPA run sets it
34
+ # around each metric call, so a trace the metric's grading module roots is tagged as
35
+ # the metric's, as a hosted run's kernel tags it through its callback's own phase.
36
+ _current_optimization_phase: ContextVar[str | None] = ContextVar("cmpnd_current_optimization_phase", default=None)
37
+
30
38
 
31
39
  def get_current_trace() -> models.TraceBuilder | None:
32
40
  """Get the current trace."""
@@ -79,6 +87,21 @@ def set_current_optimization_run_id(optimization_run_id: str | None) -> None:
79
87
  _current_optimization_run_id.set(optimization_run_id)
80
88
 
81
89
 
90
+ def get_current_optimization_phase() -> str | None:
91
+ """The optimization phase the running code serves, or None outside a metric call."""
92
+ return _current_optimization_phase.get()
93
+
94
+
95
+ @contextmanager
96
+ def optimization_phase(phase: str) -> Generator[None]:
97
+ """Run the block as the optimization phase ``phase``, restoring the outer phase after."""
98
+ token = _current_optimization_phase.set(phase)
99
+ try:
100
+ yield
101
+ finally:
102
+ _current_optimization_phase.reset(token)
103
+
104
+
82
105
  # The program each optimization run this process started optimizes, by run id. A trace
83
106
  # linked to a run that names no program of its own takes its run's, so everything a run
84
107
  # emits is reachable through that program: GEPA's reflection and judge calls, a judge
@@ -148,4 +171,5 @@ def clear_context() -> None:
148
171
  _span_stack.set(())
149
172
  _current_eval_run.set(None)
150
173
  _current_optimization_run_id.set(None)
174
+ _current_optimization_phase.set(None)
151
175
  _optimization_program_ids.clear()
@@ -205,9 +205,11 @@ class TrackedGEPA:
205
205
  existing_callbacks = self._gepa.gepa_kwargs.get("callbacks") or []
206
206
  self._gepa.gepa_kwargs["callbacks"] = existing_callbacks + [gcb]
207
207
  restore_attribution = None
208
+ restore_metric = None
208
209
 
209
210
  try:
210
211
  restore_attribution = gcb.start_trace_attribution()
212
+ restore_metric = _gepa_patch._judge_metric_calls(self._gepa)
211
213
  # Run optimization (trainset/valset are keyword-only in GEPA)
212
214
  compile_token = _gepa_patch._tracked_compile_optimizer.set(self._gepa)
213
215
  try:
@@ -246,6 +248,8 @@ class TrackedGEPA:
246
248
  raise
247
249
 
248
250
  finally:
251
+ if restore_metric is not None:
252
+ restore_metric()
249
253
  if restore_attribution is not None:
250
254
  restore_attribution()
251
255
  # Remove our callback from gepa_kwargs
@@ -180,6 +180,7 @@ Everything lives in `python-sdk/cmpnd/context.py`:
180
180
  - `_span_stack` — an immutable tuple of `SpanBuilder`s. `push_span` and `pop_span` set a new tuple rather than mutating the old one, because a context copy shares the objects its parent context holds: a mutable stack would be one list shared by every sibling copy, so concurrent runs (GEPA's `DspyAdapter.batch_evaluate`, which runs each candidate in a `copy_context()`, or `asyncio.gather` over `acall`) would read each other's spans as their parent. `get_current_span` returns the innermost. `tests/test_trace_context_concurrency.py` pins this.
181
181
  - `_current_eval_run` (`context.py:24`) — set while an `EvalHandler` is active.
182
182
  - `_current_optimization_run_id` (`context.py:29`) — set while a patched `GEPA.compile` is running, so evals triggered inside it can be linked back.
183
+ - `_current_optimization_phase` — set to `"metric"` for the span of each metric call a tracked GEPA compile makes, so a trace the metric's grading module roots is tagged as the metric's. `context.optimization_phase(phase)` sets it for a block.
183
184
 
184
185
  `create_child_span` at `context.py:83` is the standard constructor: it reads the current trace + span, assigns `trace_id` and `parent_span_id`, and returns an unpushed `SpanBuilder`. The callback uses it for every child span; so does the `start_span` context manager below. Pass `root=True` to have it ignore the stack and leave `parent_span_id` unset, for a span that starts a trace beneath one that does not parent it.
185
186
 
@@ -202,7 +203,41 @@ callback never fires. This prevents an ordinary evaluation after a failure from
202
203
  being queued under the failed run or being claimed as the next optimization's
203
204
  seed evidence.
204
205
 
205
- For tests that need a clean slate, `clear_context()` at `context.py:122` resets all four vars — see the `cmpnd-testing` skill and [`../AGENTS.md`](../AGENTS.md) § "Running Tests & Checks".
206
+ ### A callback that traces one phase of a run it does not own
207
+
208
+ `CmpndCallback(optimization_phase=...)` takes `"metric"` or `"reflection"`. A worker
209
+ kernel builds one around a hosted run's metric and around its custom GEPA proposer.
210
+ That process knows the program the run optimizes, from its config, and not the run's
211
+ id, which serve stamps from the ingest key the traces are pushed with. Such a callback
212
+ makes those calls look like a local optimization's:
213
+
214
+ - An LM call outside any trace becomes the standalone one-span `GEPA` trace that
215
+ `_emit_optimization_lm_trace` writes for a local run. Its span carries `gepa.phase`
216
+ and `optimization.role`, and its trace carries the `optimization_phase` tag. The
217
+ callback takes the phase from its constructor, not from an active eval run, and
218
+ leaves `optimization_run_id` unset.
219
+ - A trace that a dspy module opens is tagged `optimization_phase`, so a program's trace
220
+ list tells a judge module from a rollout of the same class.
221
+ - Every LM span gets `optimization.role`: `rollout` for a metric, `reflection` for a
222
+ proposer.
223
+
224
+ A local run owns its run, so its callback declares no phase. It tags its judge traces
225
+ through context instead. While a tracked GEPA compile runs, through `auto_instrument`,
226
+ `TrackedGEPA` or a client's `optimize_program`, `_gepa_patch._judge_metric_calls`
227
+ swaps the optimizer's `metric_fn` for a wrapper that runs each call under
228
+ `context.optimization_phase("metric")`, and restores the metric when the compile ends.
229
+ `_create_trace` reads that phase when its callback has none. So a judge module the
230
+ metric grades through roots a trace tagged `optimization_phase: metric`, as a hosted
231
+ judge's module trace is. The rollout the metric grades ran before the call and carries
232
+ no tag. The context phase changes only that tag: a direct judge or reflection call is
233
+ still the trace `_emit_optimization_lm_trace` builds, and module spans get no
234
+ `optimization.role`.
235
+
236
+ `tests/test_optimization_phase_callback.py` drives both paths, and the context phase,
237
+ through real DSPy dispatch. `tests/test_run_attribution_paths.py` runs a real GEPA
238
+ search whose metric grades through a module.
239
+
240
+ For tests that need a clean slate, `clear_context()` in `context.py` resets these vars — see the `cmpnd-testing` skill and [`../AGENTS.md`](../AGENTS.md) § "Running Tests & Checks".
206
241
 
207
242
  ### Across a real fork
208
243
 
@@ -5,7 +5,7 @@ name = "cmpnd"
5
5
  # it's new) — no git tags. Bump with `uv version --bump {minor,patch}` or by
6
6
  # hand. Runtime `cmpnd.__version__` reads installed metadata, so it tracks
7
7
  # this automatically. See docs/plans/2026-07-02-sdk-pypi-tag-free-publish.md.
8
- version = "0.77.0"
8
+ version = "0.78.0"
9
9
  description = "DSPy observability and deployment SDK for cmpnd"
10
10
  readme = "README.md"
11
11
  requires-python = ">=3.11"
@@ -0,0 +1,196 @@
1
+ """A callback that traces one optimization phase on behalf of a run it does not own.
2
+
3
+ A worker kernel scores a hosted rollout with the author's metric and runs the author's
4
+ GEPA proposer, and both may call a model. The kernel knows the program the run
5
+ optimizes but not the run's id: serve stamps that from the run key the traces are
6
+ pushed with. So the kernel builds its callback with ``optimization_phase`` and these
7
+ calls must come out as a local optimization's judge and reflection calls do:
8
+
9
+ * an LM call outside any trace is a standalone one-span ``GEPA`` trace, with
10
+ ``gepa.phase`` and ``optimization.role`` on its span and ``optimization_phase`` in
11
+ its tags;
12
+ * a call made through a dspy module roots a trace at that module, tagged with the phase
13
+ so a program's trace list tells it from a rollout of the same module class;
14
+ * every such trace carries the configured program id, and no run id.
15
+
16
+ A local GEPA run owns its run, so its callback declares no phase. It runs each metric
17
+ call under ``context.optimization_phase("metric")`` instead, and a trace the metric's
18
+ grading module roots there carries the same tag a hosted judge trace does.
19
+
20
+ Driven through real DSPy dispatch (``dspy.Predict`` and ``DummyLM``), because the
21
+ paths under test are which callback hooks DSPy fires and with what trace in scope.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import dataclasses
27
+ from typing import Any
28
+
29
+ import dspy
30
+ import pytest
31
+ from dspy.utils.dummies import DummyLM
32
+
33
+ from cmpnd import configuration, context
34
+ from cmpnd.configuration import configure
35
+ from cmpnd.context import clear_context
36
+ from tests._traced_run import make_callback, run
37
+
38
+ PROGRAM_ID = "p_0123456789Ab"
39
+
40
+
41
+ @pytest.fixture(autouse=True)
42
+ def configured_program():
43
+ """The kernel's config: the SDK configured, holding the deployment's program id."""
44
+ config = configure(api_key="test_key", endpoint="http://localhost:8000")
45
+ with configuration._config_lock:
46
+ configuration._config = dataclasses.replace(config, program_id=PROGRAM_ID)
47
+ yield
48
+ clear_context()
49
+ configuration._config = None
50
+ from cmpnd.exporter import shutdown_exporter
51
+
52
+ shutdown_exporter()
53
+
54
+
55
+ def _lm(answers: list[dict[str, str]]) -> DummyLM:
56
+ return DummyLM(answers, adapter=dspy.ChatAdapter())
57
+
58
+
59
+ def _direct_judge(lm: Any):
60
+ """A metric that asks its model directly, with no dspy module in between."""
61
+
62
+ def judge() -> Any:
63
+ return lm("Is 'four' right for 2+2? Say yes or no.")
64
+
65
+ return judge
66
+
67
+
68
+ class TestADirectCallIsAStandaloneTrace:
69
+ @pytest.mark.parametrize(("phase", "role"), [("metric", "rollout"), ("reflection", "reflection")])
70
+ def test_it_is_one_gepa_span_carrying_the_phase_and_the_program(self, phase: str, role: str):
71
+ lm = _lm([{"answer": "yes"}])
72
+ _, exp = run(_direct_judge(lm), lm, dspy.ChatAdapter(), callback=make_callback(optimization_phase=phase))
73
+
74
+ assert len(exp.traces) == 1, exp.traces
75
+ trace = exp.traces[0]
76
+ assert trace.module_class == "GEPA"
77
+ assert trace.tags.get("optimization_phase") == phase
78
+ assert trace.program_id == PROGRAM_ID
79
+ # Serve stamps the run from the key the trace is pushed with; the kernel claims none.
80
+ assert trace.optimization_run_id is None
81
+ assert len(trace._batch_spans) == 1
82
+ span = trace._batch_spans[0]
83
+ assert span.span_type.value == "lm_call"
84
+ assert span.parent_span_id is None
85
+ assert span.trace_id == trace.trace_id
86
+ wire = span.to_api_dict()
87
+ assert wire["attributes"]["gepa.phase"] == phase
88
+ assert wire["attributes"]["optimization.role"] == role
89
+
90
+ def test_each_call_is_its_own_trace(self):
91
+ lm = _lm([{"answer": "yes"}, {"answer": "no"}])
92
+ judge = _direct_judge(lm)
93
+
94
+ def judge_twice() -> None:
95
+ judge()
96
+ judge()
97
+
98
+ _, exp = run(judge_twice, lm, dspy.ChatAdapter(), callback=make_callback(optimization_phase="metric"))
99
+
100
+ assert len(exp.traces) == 2
101
+ assert len({trace.trace_id for trace in exp.traces}) == 2
102
+
103
+
104
+ class TestACallThroughAModuleRootsItsOwnTrace:
105
+ @pytest.mark.parametrize(("phase", "role"), [("metric", "rollout"), ("reflection", "reflection")])
106
+ def test_the_module_trace_is_tagged_with_the_phase(self, phase: str, role: str):
107
+ lm = _lm([{"verdict": "right"}])
108
+ grader = dspy.Predict("question, answer -> verdict: str")
109
+ _, exp = run(
110
+ grader,
111
+ lm,
112
+ dspy.ChatAdapter(),
113
+ callback=make_callback(optimization_phase=phase),
114
+ question="2+2?",
115
+ answer="four",
116
+ )
117
+
118
+ assert len(exp.traces) == 1, exp.traces
119
+ trace = exp.traces[0]
120
+ assert trace.module_class == "Predict"
121
+ assert trace.tags.get("optimization_phase") == phase
122
+ assert trace.program_id == PROGRAM_ID
123
+ assert trace.optimization_run_id is None
124
+ lm_spans = [span.to_api_dict() for span in exp.spans if span.span_type.value == "lm_call"]
125
+ assert lm_spans, exp.wire()
126
+ assert all(span["attributes"]["optimization.role"] == role for span in lm_spans)
127
+ assert all("gepa.phase" not in span["attributes"] for span in lm_spans)
128
+
129
+
130
+ class TestWithoutAPhase:
131
+ def test_a_program_trace_carries_no_phase(self):
132
+ """A rollout's own callback declares no phase, so its traces stay rollouts."""
133
+ lm = _lm([{"answer": "4"}])
134
+ _, exp = run(dspy.Predict("question -> answer"), lm, dspy.ChatAdapter(), question="2+2?")
135
+
136
+ assert len(exp.traces) == 1
137
+ assert "optimization_phase" not in exp.traces[0].tags
138
+ assert all("optimization.role" not in span.to_api_dict()["attributes"] for span in exp.spans)
139
+
140
+ def test_a_direct_call_outside_an_optimization_is_no_trace(self):
141
+ lm = _lm([{"answer": "yes"}])
142
+ _, exp = run(_direct_judge(lm), lm, dspy.ChatAdapter())
143
+
144
+ assert exp.traces == []
145
+
146
+
147
+ class TestALocalMetricCallsPhase:
148
+ """The phase a local run sets around its metric, read by a callback that declares none."""
149
+
150
+ def test_a_module_the_metric_grades_through_is_tagged_as_the_metric(self):
151
+ lm = _lm([{"verdict": "right"}])
152
+ grader = dspy.Predict("question, answer -> verdict: str")
153
+
154
+ def grade() -> Any:
155
+ with context.optimization_phase("metric"):
156
+ return grader(question="2+2?", answer="four")
157
+
158
+ _, exp = run(grade, lm, dspy.ChatAdapter())
159
+
160
+ assert len(exp.traces) == 1, exp.traces
161
+ assert exp.traces[0].module_class == "Predict"
162
+ assert exp.traces[0].tags.get("optimization_phase") == "metric"
163
+ # Only the trace's tag changes: its spans are built as any module trace's are.
164
+ assert all("optimization.role" not in span.to_api_dict()["attributes"] for span in exp.spans)
165
+
166
+ def test_a_program_traced_after_the_metric_returns_carries_no_phase(self):
167
+ lm = _lm([{"verdict": "right"}, {"answer": "4"}])
168
+ grader = dspy.Predict("question, answer -> verdict: str")
169
+ program = dspy.Predict("question -> answer")
170
+
171
+ def grade_then_run() -> None:
172
+ with context.optimization_phase("metric"):
173
+ grader(question="2+2?", answer="four")
174
+ program(question="2+2?")
175
+
176
+ _, exp = run(grade_then_run, lm, dspy.ChatAdapter())
177
+
178
+ assert [trace.tags.get("optimization_phase") for trace in exp.traces] == ["metric", None]
179
+
180
+ def test_a_direct_call_outside_an_optimization_stays_untraced(self):
181
+ """The phase tags module traces only: a direct call still needs a run to trace."""
182
+ lm = _lm([{"answer": "yes"}])
183
+ judge = _direct_judge(lm)
184
+
185
+ def judge_in_phase() -> None:
186
+ with context.optimization_phase("metric"):
187
+ judge()
188
+
189
+ _, exp = run(judge_in_phase, lm, dspy.ChatAdapter())
190
+
191
+ assert exp.traces == []
192
+
193
+
194
+ def test_an_unknown_phase_is_refused():
195
+ with pytest.raises(ValueError, match="optimization_phase"):
196
+ make_callback(optimization_phase="rollout")
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
+ import dataclasses
6
7
  from types import SimpleNamespace
7
8
  from unittest import mock
8
9
 
@@ -222,6 +223,106 @@ def test_real_gepa_seed_evaluation_starts_with_run_id(captured, native_gepa, wra
222
223
  captured.exporter.export_optimization_start.assert_called_once()
223
224
 
224
225
 
226
+ class Grader(dspy.Module):
227
+ """A judge the metric grades through: a dspy module of its own, on its own model."""
228
+
229
+ def __init__(self):
230
+ super().__init__()
231
+ self.judge = dspy.Predict("question, answer -> verdict: str")
232
+ self.judge.set_lm(DummyLM([{"verdict": "right"}] * 8))
233
+
234
+ def forward(self, question: str, answer: str):
235
+ return self.judge(question=question, answer=answer)
236
+
237
+
238
+ @pytest.mark.parametrize("wrapper", ["automatic", "tracked"])
239
+ def test_real_gepa_tags_the_metrics_judge_trace_and_not_the_rollout(captured, native_gepa, wrapper):
240
+ if wrapper == "tracked" and not _gepa_patch._gepa_supports_callbacks():
241
+ pytest.skip("native GEPA callback path requires callback support")
242
+ program = dspy.Predict("question -> answer")
243
+ examples = [dspy.Example(question="hello", answer="HELLO").with_inputs("question")]
244
+ grader = Grader()
245
+
246
+ def metric(example, prediction, trace=None, pred_name=None, pred_trace=None):
247
+ verdict = grader(question=example.question, answer=prediction.answer).verdict
248
+ return dspy.Prediction(score=float(verdict == "right"), feedback="graded")
249
+
250
+ kwargs = dict(metric=metric, max_metric_calls=1, reflection_lm=DummyLM([]), num_threads=1)
251
+ with dspy.context(callbacks=[captured.callback], lm=DummyLM([{"answer": "HELLO"}] * 8)):
252
+ if wrapper == "tracked":
253
+ with pytest.warns(DeprecationWarning):
254
+ optimizer = TrackedGEPA(**kwargs)
255
+ else:
256
+ cmpnd.auto_instrument()
257
+ optimizer = dspy.GEPA(**kwargs)
258
+ optimizer.compile(program, trainset=examples, valset=examples)
259
+
260
+ phases = {(row["module_class"], (row.get("tags") or {}).get("optimization_phase")) for row in captured.completed}
261
+ assert ("Grader", "metric") in phases, captured.completed
262
+ assert ("Predict", None) in phases, captured.completed
263
+ assert {phase for module_class, phase in phases if module_class == "Predict"} == {None}
264
+ # The run hands the optimizer back the metric it was built with.
265
+ assert optimizer_metric(optimizer) is metric
266
+ assert context.get_current_optimization_phase() is None
267
+
268
+
269
+ def optimizer_metric(optimizer):
270
+ return (optimizer._gepa if isinstance(optimizer, TrackedGEPA) else optimizer).metric_fn
271
+
272
+
273
+ @dataclasses.dataclass
274
+ class GradingMetric:
275
+ """A metric object, as a user may write one: a dataclass, so unhashable."""
276
+
277
+ grader: Grader
278
+
279
+ def __call__(self, example, prediction, trace=None, pred_name=None, pred_trace=None):
280
+ verdict = self.grader(question=example.question, answer=prediction.answer).verdict
281
+ return dspy.Prediction(score=float(verdict == "right"), feedback="graded")
282
+
283
+
284
+ @pytest.mark.parametrize("wrapper", ["automatic", "tracked"])
285
+ def test_real_gepa_tags_the_judge_trace_of_an_unhashable_metric(captured, native_gepa, wrapper):
286
+ if wrapper == "tracked" and not _gepa_patch._gepa_supports_callbacks():
287
+ pytest.skip("native GEPA callback path requires callback support")
288
+ program = dspy.Predict("question -> answer")
289
+ examples = [dspy.Example(question="hello", answer="HELLO").with_inputs("question")]
290
+ metric = GradingMetric(Grader())
291
+ with pytest.raises(TypeError):
292
+ hash(metric)
293
+
294
+ kwargs = dict(metric=metric, max_metric_calls=1, reflection_lm=DummyLM([]), num_threads=1)
295
+ with dspy.context(callbacks=[captured.callback], lm=DummyLM([{"answer": "HELLO"}] * 8)):
296
+ if wrapper == "tracked":
297
+ with pytest.warns(DeprecationWarning):
298
+ optimizer = TrackedGEPA(**kwargs)
299
+ else:
300
+ cmpnd.auto_instrument()
301
+ optimizer = dspy.GEPA(**kwargs)
302
+ optimizer.compile(program, trainset=examples, valset=examples)
303
+
304
+ phases = {(row["module_class"], (row.get("tags") or {}).get("optimization_phase")) for row in captured.completed}
305
+ assert ("Grader", "metric") in phases, captured.completed
306
+ assert optimizer_metric(optimizer) is metric
307
+
308
+
309
+ @pytest.mark.parametrize("wrapper", ["automatic_without_callbacks", "tracked"])
310
+ def test_a_metric_that_cannot_be_wrapped_still_ends_run_attribution(captured, monkeypatch, wrapper):
311
+ def refuse(_gepa):
312
+ raise RuntimeError("metric not wrappable")
313
+
314
+ monkeypatch.setattr(_gepa_patch, "_judge_metric_calls", refuse)
315
+ program = DirectProgram()
316
+
317
+ with dspy.context(callbacks=[captured.callback]):
318
+ with pytest.raises(RuntimeError, match="metric not wrappable"):
319
+ _compile_without_native_events(wrapper, program, lambda: program)
320
+
321
+ assert captured.callback._active_optimization_run_id is None
322
+ assert context.get_current_optimization_run_id() is None
323
+ assert _gepa_patch._tracked_compile_optimizer.get() is None
324
+
325
+
225
326
  def test_tracked_gepa_under_auto_runs_instance_override_and_tracks_distinct_optimizer(captured, native_gepa):
226
327
  if not _gepa_patch._gepa_supports_callbacks():
227
328
  pytest.skip("native GEPA callback path requires callback support")
@@ -505,7 +505,7 @@ wheels = [
505
505
 
506
506
  [[package]]
507
507
  name = "cmpnd"
508
- version = "0.77.0"
508
+ version = "0.78.0"
509
509
  source = { editable = "." }
510
510
  default-groups = ["dspy-3-4-1"]
511
511
  dependencies = [
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes