cmpnd 0.31.0__tar.gz → 0.32.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 (354) hide show
  1. {cmpnd-0.31.0 → cmpnd-0.32.0}/PKG-INFO +1 -1
  2. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/__init__.py +2 -1
  3. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/_gepa_patch.py +11 -9
  4. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/_http.py +56 -5
  5. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/callback.py +52 -13
  6. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/cli/commands/deployments.py +7 -1
  7. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/cli/commands/top.py +20 -3
  8. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/execution.py +40 -14
  9. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/models.py +7 -1
  10. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/optimizers/gepa.py +10 -7
  11. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/optimizers/gepa_callback.py +21 -0
  12. {cmpnd-0.31.0 → cmpnd-0.32.0}/docs/sdk-instrumentation.md +73 -0
  13. {cmpnd-0.31.0 → cmpnd-0.32.0}/pyproject.toml +1 -1
  14. {cmpnd-0.31.0 → cmpnd-0.32.0}/scripts/test-in-linux.sh +13 -1
  15. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_callback.py +203 -9
  16. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_cli/test_deployments.py +4 -2
  17. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_cli/test_top.py +8 -4
  18. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_execute.py +8 -7
  19. cmpnd-0.32.0/tests/test_execute_run_identity.py +203 -0
  20. cmpnd-0.32.0/tests/test_run_attribution_paths.py +368 -0
  21. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_tracked_gepa.py +25 -18
  22. {cmpnd-0.31.0 → cmpnd-0.32.0}/uv.lock +1 -1
  23. {cmpnd-0.31.0 → cmpnd-0.32.0}/.gitignore +0 -0
  24. {cmpnd-0.31.0 → cmpnd-0.32.0}/AGENTS.md +0 -0
  25. {cmpnd-0.31.0 → cmpnd-0.32.0}/CLAUDE.md +0 -0
  26. {cmpnd-0.31.0 → cmpnd-0.32.0}/README.md +0 -0
  27. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/_assessments.py +0 -0
  28. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/_authored_import.py +0 -0
  29. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/_deployments.py +0 -0
  30. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/_exporter_pool.py +0 -0
  31. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/_program_patch.py +0 -0
  32. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/_rlm_patch.py +0 -0
  33. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/_runtime_tools.py +0 -0
  34. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/_timespec.py +0 -0
  35. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/_traces.py +0 -0
  36. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/_valsets.py +0 -0
  37. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/authored.py +0 -0
  38. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/cli/__init__.py +0 -0
  39. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/cli/api_client.py +0 -0
  40. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/cli/commands/__init__.py +0 -0
  41. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/cli/commands/admin.py +0 -0
  42. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/cli/commands/assessments.py +0 -0
  43. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/cli/commands/auth.py +0 -0
  44. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/cli/commands/datasets.py +0 -0
  45. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/cli/commands/evals.py +0 -0
  46. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/cli/commands/login.py +0 -0
  47. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/cli/commands/optimizations.py +0 -0
  48. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/cli/commands/org.py +0 -0
  49. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/cli/commands/programs.py +0 -0
  50. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/cli/commands/provider_credentials.py +0 -0
  51. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/cli/commands/signatures.py +0 -0
  52. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/cli/commands/tool_servers.py +0 -0
  53. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/cli/commands/traces.py +0 -0
  54. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/cli/commands/valsets.py +0 -0
  55. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/cli/credentials.py +0 -0
  56. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/cli/deprecation.py +0 -0
  57. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/cli/pagination.py +0 -0
  58. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/cli/shell.py +0 -0
  59. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/cli/trace_render.py +0 -0
  60. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/cli/tree_help.py +0 -0
  61. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/cli/verbs.py +0 -0
  62. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/client.py +0 -0
  63. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/configuration.py +0 -0
  64. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/context.py +0 -0
  65. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/dataset_sync.py +0 -0
  66. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/datasets.py +0 -0
  67. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/decorators.py +0 -0
  68. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/deployment.py +0 -0
  69. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/deployment_metadata.py +0 -0
  70. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/eval_handler.py +0 -0
  71. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/exporter.py +0 -0
  72. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/exporter_http.py +0 -0
  73. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/helpers.py +0 -0
  74. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/identity.py +0 -0
  75. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/imaging.py +0 -0
  76. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/ir/__init__.py +0 -0
  77. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/ir/decode.py +0 -0
  78. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/ir/encode.py +0 -0
  79. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/ir/hash.py +0 -0
  80. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/ir/program.py +0 -0
  81. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/ir/reflection.py +0 -0
  82. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/ir/types.py +0 -0
  83. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/ir/xxh64.py +0 -0
  84. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/lm_configuration.py +0 -0
  85. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/mcp_servers.py +0 -0
  86. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/optimization.py +0 -0
  87. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/optimizers/__init__.py +0 -0
  88. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/optimizers/resume.py +0 -0
  89. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/optimizers/tracker.py +0 -0
  90. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/packaging.py +0 -0
  91. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/references.py +0 -0
  92. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/retry.py +0 -0
  93. {cmpnd-0.31.0 → cmpnd-0.32.0}/cmpnd/traces.py +0 -0
  94. {cmpnd-0.31.0 → cmpnd-0.32.0}/docs/2026-04-02-code-review.md +0 -0
  95. {cmpnd-0.31.0 → cmpnd-0.32.0}/docs/cli.md +0 -0
  96. {cmpnd-0.31.0 → cmpnd-0.32.0}/docs/client.md +0 -0
  97. {cmpnd-0.31.0 → cmpnd-0.32.0}/docs/custom-proposer.md +0 -0
  98. {cmpnd-0.31.0 → cmpnd-0.32.0}/docs/plans/2026-04-05-train-val-dataset-capture-design.md +0 -0
  99. {cmpnd-0.31.0 → cmpnd-0.32.0}/docs/plans/2026-04-05-train-val-dataset-capture.md +0 -0
  100. {cmpnd-0.31.0 → cmpnd-0.32.0}/docs/reading-traces.md +0 -0
  101. {cmpnd-0.31.0 → cmpnd-0.32.0}/examples/generalizing_proposer.py +0 -0
  102. {cmpnd-0.31.0 → cmpnd-0.32.0}/examples/local_ollama.py +0 -0
  103. {cmpnd-0.31.0 → cmpnd-0.32.0}/ingest/LINEAGE.md +0 -0
  104. {cmpnd-0.31.0 → cmpnd-0.32.0}/ingest/README.md +0 -0
  105. {cmpnd-0.31.0 → cmpnd-0.32.0}/ingest/cmpnd_ingest/__init__.py +0 -0
  106. {cmpnd-0.31.0 → cmpnd-0.32.0}/ingest/cmpnd_ingest/bodies.py +0 -0
  107. {cmpnd-0.31.0 → cmpnd-0.32.0}/ingest/cmpnd_ingest/exporter_http.py +0 -0
  108. {cmpnd-0.31.0 → cmpnd-0.32.0}/ingest/cmpnd_ingest/protocols.py +0 -0
  109. {cmpnd-0.31.0 → cmpnd-0.32.0}/ingest/cmpnd_ingest/py.typed +0 -0
  110. {cmpnd-0.31.0 → cmpnd-0.32.0}/ingest/cmpnd_ingest/request_log.py +0 -0
  111. {cmpnd-0.31.0 → cmpnd-0.32.0}/ingest/cmpnd_ingest/retry.py +0 -0
  112. {cmpnd-0.31.0 → cmpnd-0.32.0}/ingest/pyproject.toml +0 -0
  113. {cmpnd-0.31.0 → cmpnd-0.32.0}/scripts/audit-sdist.sh +0 -0
  114. {cmpnd-0.31.0 → cmpnd-0.32.0}/scripts/capture_review_run.py +0 -0
  115. {cmpnd-0.31.0 → cmpnd-0.32.0}/scripts/check_release_artifacts.py +0 -0
  116. {cmpnd-0.31.0 → cmpnd-0.32.0}/scripts/fixtures/review_run.json.gz +0 -0
  117. {cmpnd-0.31.0 → cmpnd-0.32.0}/scripts/gepa_resume_bug.py +0 -0
  118. {cmpnd-0.31.0 → cmpnd-0.32.0}/scripts/release-stress.sh +0 -0
  119. {cmpnd-0.31.0 → cmpnd-0.32.0}/scripts/require_version_bump.py +0 -0
  120. {cmpnd-0.31.0 → cmpnd-0.32.0}/scripts/seed_review_run.py +0 -0
  121. {cmpnd-0.31.0 → cmpnd-0.32.0}/stubs/dspy/__init__.pyi +0 -0
  122. {cmpnd-0.31.0 → cmpnd-0.32.0}/stubs/dspy/primitives/__init__.pyi +0 -0
  123. {cmpnd-0.31.0 → cmpnd-0.32.0}/stubs/dspy/primitives/prediction.pyi +0 -0
  124. {cmpnd-0.31.0 → cmpnd-0.32.0}/stubs/dspy/utils/__init__.pyi +0 -0
  125. {cmpnd-0.31.0 → cmpnd-0.32.0}/stubs/dspy/utils/callback.pyi +0 -0
  126. {cmpnd-0.31.0 → cmpnd-0.32.0}/stubs/dspy/utils/dummies.pyi +0 -0
  127. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/__init__.py +0 -0
  128. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/_deployment_signature_helpers.py +0 -0
  129. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/_exporter_helpers.py +0 -0
  130. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/_fake_clock.py +0 -0
  131. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/_identity_helpers.py +0 -0
  132. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/_resolve_refs.py +0 -0
  133. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/_traced_run.py +0 -0
  134. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/conftest.py +0 -0
  135. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/e2e/__init__.py +0 -0
  136. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/e2e/committee_task.py +0 -0
  137. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/e2e/conftest.py +0 -0
  138. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/e2e/data/committee_train.ndjson.gz +0 -0
  139. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/e2e/data/committee_val.ndjson.gz +0 -0
  140. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/e2e/fake_lm.py +0 -0
  141. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/e2e/helpers.py +0 -0
  142. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/e2e/target_capabilities.py +0 -0
  143. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/e2e/test_assessments_cli.py +0 -0
  144. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/e2e/test_capture_prompts.py +0 -0
  145. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/e2e/test_composite_module.py +0 -0
  146. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/e2e/test_dataset_auto_creation.py +0 -0
  147. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/e2e/test_deploy_execute.py +0 -0
  148. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/e2e/test_deploy_programs.py +0 -0
  149. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/e2e/test_edge_cases.py +0 -0
  150. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/e2e/test_example_hash.py +0 -0
  151. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/e2e/test_gepa_optimization.py +0 -0
  152. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/e2e/test_interpreter_spans.py +0 -0
  153. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/e2e/test_lm_accuracy.py +0 -0
  154. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/e2e/test_module_tracing.py +0 -0
  155. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/e2e/test_optimization_lineage.py +0 -0
  156. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/e2e/test_optimization_metric_identity.py +0 -0
  157. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/e2e/test_optimize_failures.py +0 -0
  158. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/e2e/test_optimize_loop.py +0 -0
  159. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/e2e/test_optimize_review_committee.py +0 -0
  160. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/e2e/test_optimize_server_owned_completion.py +0 -0
  161. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/e2e/test_parallel_export.py +0 -0
  162. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/e2e/test_project_attribution.py +0 -0
  163. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/e2e/test_react_tool_spans.py +0 -0
  164. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/e2e/test_save_load.py +0 -0
  165. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/e2e/test_trace_structure.py +0 -0
  166. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/e2e/test_traces_read.py +0 -0
  167. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/e2e/test_user_attribution.py +0 -0
  168. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/e2e/test_valsets_cli.py +0 -0
  169. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/identity_vectors/01_predict_qa.json +0 -0
  170. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/identity_vectors/02_predict_renamed_field.json +0 -0
  171. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/identity_vectors/03_chain_of_thought_qa.json +0 -0
  172. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/identity_vectors/04_predict_richer_types.json +0 -0
  173. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/identity_vectors/05_composite_two_predicts.json +0 -0
  174. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/identity_vectors/06_multichaincomparison.json +0 -0
  175. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/identity_vectors/07_program_of_thought.json +0 -0
  176. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/identity_vectors/08_opaque_module.json +0 -0
  177. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/identity_vectors/09_predict_pydantic_field.json +0 -0
  178. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/identity_vectors/10_react.json +0 -0
  179. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/identity_vectors/11_refine.json +0 -0
  180. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/identity_vectors/12_retrieve.json +0 -0
  181. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/identity_vectors/13_knn.json +0 -0
  182. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/identity_vectors/14_parallel.json +0 -0
  183. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/identity_vectors/README.md +0 -0
  184. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/ingest_conformance.py +0 -0
  185. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/integration/__init__.py +0 -0
  186. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/integration/authored_optimize/judge.py +0 -0
  187. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/integration/authored_optimize/optimize_hosted.py +0 -0
  188. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/integration/conftest.py +0 -0
  189. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/integration/hosted_mcp_tool_optimize_probe.py +0 -0
  190. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/integration/hosted_optimize_probe.py +0 -0
  191. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/integration/optimization_run_probe.py +0 -0
  192. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/integration/test_cli_deploy_smoketest.py +0 -0
  193. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/integration/test_logs_renders_traces.py +0 -0
  194. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/integration/test_optimization_run.py +0 -0
  195. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/integration/test_optimizing_progress.py +0 -0
  196. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/integration/test_unhappy_path.py +0 -0
  197. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/load/__init__.py +0 -0
  198. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/load/__main__.py +0 -0
  199. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/load/drain.py +0 -0
  200. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/load/ingest.py +0 -0
  201. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/load/latency_transport.py +0 -0
  202. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/load/rlm_shape.py +0 -0
  203. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/load/rlm_size_report.py +0 -0
  204. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/load/sweep.py +0 -0
  205. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/load/test_rlm_shape.py +0 -0
  206. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/load/test_rlm_size_report.py +0 -0
  207. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/parity/__init__.py +0 -0
  208. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/parity/conftest.py +0 -0
  209. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/parity/seed_parity.py +0 -0
  210. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/parity/test_auth.py +0 -0
  211. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/parity/test_datasets.py +0 -0
  212. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/parity/test_deployments.py +0 -0
  213. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/parity/test_evals.py +0 -0
  214. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/parity/test_health.py +0 -0
  215. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/parity/test_modules.py +0 -0
  216. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/parity/test_optimizations.py +0 -0
  217. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/parity/test_programs.py +0 -0
  218. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/parity/test_signatures.py +0 -0
  219. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/parity/test_trace_streaming.py +0 -0
  220. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/parity/test_traces.py +0 -0
  221. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/span_reference_fixture.py +0 -0
  222. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/stress/__init__.py +0 -0
  223. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/stress/_provider.py +0 -0
  224. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/stress/_readiness.py +0 -0
  225. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/stress/_reliability.py +0 -0
  226. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/stress/_result.py +0 -0
  227. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/stress/mint_bedrock_token.py +0 -0
  228. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/stress/test_deploy_programs.py +0 -0
  229. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/stress/test_multi_client_stress.py +0 -0
  230. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/stress/test_optimize_client_committee.py +0 -0
  231. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/stress/test_optimize_loop.py +0 -0
  232. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/stress/test_optimize_loop_wasm.py +0 -0
  233. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/stress/test_optimize_stress.py +0 -0
  234. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/stress/test_provider_roles.py +0 -0
  235. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/stress/test_readiness.py +0 -0
  236. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/stress/test_real_provider_twins.py +0 -0
  237. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/stress/test_reliability.py +0 -0
  238. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/stress/test_reliability_classify.py +0 -0
  239. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/stress/test_rlm_wasm.py +0 -0
  240. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/stress/test_trace_search.py +0 -0
  241. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_adapter_reentrancy.py +0 -0
  242. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_assessments_api.py +0 -0
  243. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_authored.py +0 -0
  244. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_authored_bundle_collisions.py +0 -0
  245. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_authored_metric_contract.py +0 -0
  246. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_authored_named_models.py +0 -0
  247. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_authored_optimize.py +0 -0
  248. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_candidate_lineage.py +0 -0
  249. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_cli/__init__.py +0 -0
  250. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_cli/conftest.py +0 -0
  251. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_cli/test_admin.py +0 -0
  252. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_cli/test_api_client.py +0 -0
  253. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_cli/test_assessments.py +0 -0
  254. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_cli/test_auth.py +0 -0
  255. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_cli/test_credentials.py +0 -0
  256. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_cli/test_datasets.py +0 -0
  257. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_cli/test_evals.py +0 -0
  258. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_cli/test_login.py +0 -0
  259. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_cli/test_main.py +0 -0
  260. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_cli/test_optimizations.py +0 -0
  261. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_cli/test_org.py +0 -0
  262. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_cli/test_pagination.py +0 -0
  263. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_cli/test_parity.py +0 -0
  264. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_cli/test_programs.py +0 -0
  265. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_cli/test_provider_credentials.py +0 -0
  266. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_cli/test_schema_sample.py +0 -0
  267. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_cli/test_shell.py +0 -0
  268. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_cli/test_signatures.py +0 -0
  269. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_cli/test_tool_servers.py +0 -0
  270. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_cli/test_traces.py +0 -0
  271. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_cli/test_traces_search_wire.py +0 -0
  272. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_cli/test_tree_help.py +0 -0
  273. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_cli/test_valsets.py +0 -0
  274. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_client.py +0 -0
  275. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_clustering/__init__.py +0 -0
  276. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_clustering/conftest.py +0 -0
  277. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_clustering/test_edge_cases.py +0 -0
  278. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_clustering/test_hash_consistency.py +0 -0
  279. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_clustering/test_type_conversion.py +0 -0
  280. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_cmp551_grouping_identity.py +0 -0
  281. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_configuration.py +0 -0
  282. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_context.py +0 -0
  283. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_context_propagation.py +0 -0
  284. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_dataset_polling.py +0 -0
  285. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_dataset_sync.py +0 -0
  286. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_datasets.py +0 -0
  287. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_decorators.py +0 -0
  288. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_deploy.py +0 -0
  289. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_deploy_mcp_tool_references.py +0 -0
  290. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_deployment_metadata.py +0 -0
  291. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_deployments_read.py +0 -0
  292. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_eval.py +0 -0
  293. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_eval_example_attribution.py +0 -0
  294. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_eval_start.py +0 -0
  295. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_eval_trace_grouping.py +0 -0
  296. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_exception_paths.py +0 -0
  297. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_exporter.py +0 -0
  298. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_exporter_queue.py +0 -0
  299. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_exporter_real.py +0 -0
  300. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_fake_lm.py +0 -0
  301. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_gepa_callback.py +0 -0
  302. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_gepa_integration.py +0 -0
  303. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_gepa_output_normalization.py +0 -0
  304. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_gepa_resume.py +0 -0
  305. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_identity.py +0 -0
  306. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_imaging.py +0 -0
  307. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_import_weight.py +0 -0
  308. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_ingest_conformance.py +0 -0
  309. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_ingest_second_producer.py +0 -0
  310. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_integration.py +0 -0
  311. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_interpreter_callback.py +0 -0
  312. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_ir_encoder_properties.py +0 -0
  313. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_ir_golden_vectors.py +0 -0
  314. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_ir_roundtrip.py +0 -0
  315. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_lm_configuration.py +0 -0
  316. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_lm_usage_shapes.py +0 -0
  317. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_mcp_tools.py +0 -0
  318. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_models.py +0 -0
  319. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_optimization_progress.py +0 -0
  320. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_optimization_start.py +0 -0
  321. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_optimize.py +0 -0
  322. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_optimize_budget.py +0 -0
  323. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_optimize_lift.py +0 -0
  324. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_optimize_lms.py +0 -0
  325. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_optimize_proposer.py +0 -0
  326. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_optimize_reflection_lm.py +0 -0
  327. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_optimize_terminal_failure.py +0 -0
  328. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_packaging.py +0 -0
  329. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_packaging_callable_source.py +0 -0
  330. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_packaging_mcp_tools.py +0 -0
  331. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_packaging_program_source.py +0 -0
  332. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_poll_until.py +0 -0
  333. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_program_lineage_persist.py +0 -0
  334. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_project_payload.py +0 -0
  335. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_reflector_properties.py +0 -0
  336. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_reflector_smoke.py +0 -0
  337. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_release_stress_script.py +0 -0
  338. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_repro_reactv2_bugs.py +0 -0
  339. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_require_version_bump.py +0 -0
  340. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_retry.py +0 -0
  341. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_reversible_failures.py +0 -0
  342. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_rlm_patch.py +0 -0
  343. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_runtime_tools.py +0 -0
  344. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_schemas.py +0 -0
  345. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_seed_review_run.py +0 -0
  346. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_span_references.py +0 -0
  347. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_span_sent_once.py +0 -0
  348. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_trace_render.py +0 -0
  349. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_traces_read.py +0 -0
  350. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_tracker_enrich.py +0 -0
  351. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_truncation.py +0 -0
  352. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_valsets_api.py +0 -0
  353. {cmpnd-0.31.0 → cmpnd-0.32.0}/tests/test_xxh64.py +0 -0
  354. {cmpnd-0.31.0 → cmpnd-0.32.0}/todo.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cmpnd
3
- Version: 0.31.0
3
+ Version: 0.32.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>
@@ -33,7 +33,7 @@ from .deployment_metadata import (
33
33
  DeploymentSignatureResponse,
34
34
  UnsupportedDeploymentMetadataVersion,
35
35
  )
36
- from .execution import DeployedProgram, RetryableProviderError, execute
36
+ from .execution import DeployedProgram, RetryableProviderError, deployment_run_id, execute
37
37
  from .exporter import flush_exporter, shutdown_exporter
38
38
  from .lm_configuration import configure_lm
39
39
  from .mcp_servers import (
@@ -104,6 +104,7 @@ __all__ = [
104
104
  "DeploymentMetadataError",
105
105
  "UnsupportedDeploymentMetadataVersion",
106
106
  "execute",
107
+ "deployment_run_id",
107
108
  "DeployedProgram",
108
109
  "RetryableProviderError",
109
110
  # Optimize
@@ -10,8 +10,6 @@ module-style (``from . import identity``); basedpyright can't resolve
10
10
 
11
11
  import contextvars
12
12
 
13
- from . import context
14
-
15
13
  _gepa_patched = False
16
14
 
17
15
  # Set by a client's run_optimization while it owns the run. The class patch reads
@@ -22,6 +20,12 @@ _client_owns_optimization: contextvars.ContextVar[bool] = contextvars.ContextVar
22
20
  "cmpnd_client_owns_optimization", default=False
23
21
  )
24
22
 
23
+ # TrackedGEPA reserves its own run before invoking this native optimizer. Match
24
+ # the instance so an unrelated optimizer nested inside it still gets a run.
25
+ _tracked_compile_optimizer: contextvars.ContextVar[object | None] = contextvars.ContextVar(
26
+ "cmpnd_tracked_compile_optimizer", default=None
27
+ )
28
+
25
29
 
26
30
  def _gepa_supports_callbacks() -> bool:
27
31
  """Check if the installed gepa version supports callbacks in optimize()."""
@@ -110,8 +114,7 @@ def _run_tracked_compile(
110
114
  max_metric_calls=max_metric_calls,
111
115
  )
112
116
 
113
- trk.start()
114
- context.set_current_optimization_run_id(trk.optimization_run_id)
117
+ restore_attribution = callback.start_trace_attribution()
115
118
 
116
119
  try:
117
120
  result = run_original()
@@ -139,8 +142,7 @@ def _run_tracked_compile(
139
142
  raise
140
143
 
141
144
  finally:
142
- context.set_current_optimization_run_id(None)
143
- callback.clear_trace_association_state()
145
+ restore_attribution()
144
146
 
145
147
 
146
148
  def patch_gepa() -> None:
@@ -176,9 +178,9 @@ def patch_gepa() -> None:
176
178
  type(self)._cmpnd_original_init(self, *args, **kwargs)
177
179
 
178
180
  def _patched_compile(self, student, *, trainset, teacher=None, valset=None, **kwargs):
179
- # When a client's run_optimization owns this run it records the run on its
180
- # own account and has suppressed the global track; run the search only.
181
- if _client_owns_optimization.get():
181
+ # A client or TrackedGEPA already records this run. TrackedGEPA claims
182
+ # only its own optimizer instance, leaving distinct nested runs tracked.
183
+ if _client_owns_optimization.get() or _tracked_compile_optimizer.get() is self:
182
184
  return type(self)._cmpnd_original_compile(
183
185
  self, student, trainset=trainset, teacher=teacher, valset=valset, **kwargs
184
186
  )
@@ -11,6 +11,7 @@ resolution; this module is only the wire.
11
11
  from __future__ import annotations
12
12
 
13
13
  import os
14
+ import re
14
15
  from typing import Any
15
16
 
16
17
  import httpx
@@ -24,10 +25,45 @@ class ApiError(Exception):
24
25
  is the process exit the CLI maps the error to; the library client ignores it.
25
26
  """
26
27
 
27
- def __init__(self, message: str, exit_code: int = 1, *, status_code: int | None = None):
28
+ def __init__(
29
+ self,
30
+ message: str,
31
+ exit_code: int = 1,
32
+ *,
33
+ status_code: int | None = None,
34
+ deployment_run_id: str | None = None,
35
+ ):
28
36
  super().__init__(message)
29
37
  self.exit_code = exit_code
30
38
  self.status_code = status_code
39
+ self._cmpnd_deployment_run_id = deployment_run_id
40
+
41
+
42
+ def response_deployment_run_id(response: httpx.Response) -> str | None:
43
+ """Read the server's execute correlation key from its JSON response body."""
44
+ try:
45
+ body = response.json()
46
+ except ValueError:
47
+ return None
48
+ return _body_deployment_run_id(body)
49
+
50
+
51
+ def _body_deployment_run_id(body: object) -> str | None:
52
+ run_id = body.get("deployment_run_id") if isinstance(body, dict) else None
53
+ if isinstance(run_id, str) and re.fullmatch(r"dr_[A-Za-z0-9]{12}", run_id):
54
+ return run_id
55
+ return None
56
+
57
+
58
+ def execute_response_output(response: httpx.Response) -> tuple[dict[str, Any], str]:
59
+ """Read program output and serve's run id from the execute envelope."""
60
+ body = response.json()
61
+ run_id = _body_deployment_run_id(body)
62
+ if not isinstance(body, dict) or run_id is None or not isinstance(body.get("output"), dict):
63
+ error = RuntimeError("Execute response has an invalid run envelope")
64
+ error._cmpnd_deployment_run_id = run_id # pyright: ignore[reportAttributeAccessIssue]
65
+ raise error
66
+ return body["output"], run_id
31
67
 
32
68
 
33
69
  def server_detail(response: httpx.Response) -> str:
@@ -81,6 +117,10 @@ class ApiClient:
81
117
  def post(self, path: str, json: dict[str, Any] | None = None) -> Any:
82
118
  return self._request("POST", path, json=json)
83
119
 
120
+ def post_response(self, path: str, json: dict[str, Any] | None = None) -> httpx.Response:
121
+ """POST through the shared error ladder and return the complete response."""
122
+ return self._request("POST", path, json=json, return_response=True)
123
+
84
124
  def patch(self, path: str, json: dict[str, Any] | None = None) -> Any:
85
125
  return self._request("PATCH", path, json=json)
86
126
 
@@ -100,6 +140,7 @@ class ApiClient:
100
140
  params: dict[str, str] | None = None,
101
141
  json: dict[str, Any] | None = None,
102
142
  decode: bool | None = True,
143
+ return_response: bool = False,
103
144
  ) -> Any:
104
145
  url = f"{self._endpoint}{path}"
105
146
  # 180s default: deploy/optimize/run wait in-band on an LM round-trip
@@ -137,15 +178,22 @@ class ApiClient:
137
178
  except httpx.TimeoutException:
138
179
  raise ApiError(f"Request timed out connecting to {self._endpoint}.")
139
180
 
181
+ run_id = response_deployment_run_id(response) if response.status_code >= 400 else None
182
+
183
+ def error(message: str, exit_code: int = 1) -> ApiError:
184
+ if run_id:
185
+ message += f"\nDeployment run: {run_id}"
186
+ return ApiError(message, exit_code, status_code=response.status_code, deployment_run_id=run_id)
187
+
140
188
  if response.status_code == 401:
141
- raise ApiError("Invalid or missing API key. Check CMPND_API_KEY or --api-key.", status_code=401)
189
+ raise error("Invalid or missing API key. Check CMPND_API_KEY or --api-key.")
142
190
  if response.status_code == 404:
143
191
  # The server's own words, not a generic stand-in. Every route here 404s
144
192
  # for a reason it already states — "signature not found", "deployment
145
193
  # not found", "no such organization" — and flattening them into one
146
194
  # sentence makes a missing record and a mistyped URL read identically,
147
195
  # which is the difference a user needs most and had the least of.
148
- raise ApiError(server_detail(response) or "Resource not found.", status_code=404)
196
+ raise error(server_detail(response) or "Resource not found.")
149
197
  if response.status_code >= 500:
150
198
  # 5xx usually means the server is up but blew up handling the
151
199
  # request — a database hiccup, an unhandled exception, etc.
@@ -157,13 +205,16 @@ class ApiClient:
157
205
  if body_excerpt:
158
206
  msg += f": {body_excerpt}"
159
207
  msg += "\n If you operate this server, check its logs for the underlying exception."
160
- raise ApiError(msg, exit_code=2, status_code=response.status_code)
208
+ raise error(msg, exit_code=2)
161
209
  if response.status_code >= 400:
162
210
  try:
163
211
  detail = response.json()
164
212
  except Exception:
165
213
  detail = response.text
166
- raise ApiError(f"Request failed ({response.status_code}): {detail}", status_code=response.status_code)
214
+ raise error(f"Request failed ({response.status_code}): {detail}")
215
+
216
+ if return_response:
217
+ return response
167
218
 
168
219
  # A bytes route hands the raw body back before httpx decodes it.
169
220
  if decode is None:
@@ -929,19 +929,21 @@ class _ModuleMixin(_CallbackBase):
929
929
  trace.span_count = len(trace_spans)
930
930
  logger.debug(f"Trace totals: {trace.total_tokens} tokens, ${trace.total_cost}")
931
931
 
932
- # Link trace to optimization run if we're inside one
933
- # Try instance var first (thread-safe), fall back to contextvar
934
-
935
- opt_run_id = self._active_optimization_run_id or context.get_current_optimization_run_id()
936
- if opt_run_id:
937
- trace.optimization_run_id = opt_run_id
938
-
939
- # If we're in an eval run, collect trace for metric correlation
940
- # Use instance var (thread-safe) instead of contextvar (thread-local)
941
- current_eval = self._active_eval_run
942
- if current_eval:
943
- eval_run_id = str(current_eval.eval_run_id)
944
- trace.eval_run_id = current_eval.eval_run_id
932
+ # trace.optimization_run_id and trace.eval_run_id were attached
933
+ # in _create_trace, from this callback's own active-run state
934
+ # when the trace started. Do not re-read that state here: by
935
+ # finalize the run may already have ended (a trace that
936
+ # outlives it), and re-reading would drop the attribution the
937
+ # start row already sent — the trace keeps the ids it started
938
+ # with either way.
939
+
940
+ # If this trace's eval run is still collecting traces for
941
+ # metric correlation, add it — keyed by the id the trace
942
+ # already carries, not by whether the eval is still the live
943
+ # one (it may have finalized already, in which case there is
944
+ # nothing left to add to).
945
+ if trace.eval_run_id:
946
+ eval_run_id = str(trace.eval_run_id)
945
947
  eval_traces = self._eval_handler._eval_traces
946
948
  if eval_run_id in eval_traces:
947
949
  eval_traces[eval_run_id].append(trace)
@@ -1085,6 +1087,41 @@ class _ModuleMixin(_CallbackBase):
1085
1087
  )
1086
1088
  self._stamp(trace)
1087
1089
 
1090
+ # Attach the run ids this trace starts under, from this callback's own
1091
+ # state right now — not at finalize (on_module_end), where the
1092
+ # optimization or eval may already have ended for a trace that
1093
+ # outlives it. These are the same sources finalization read before
1094
+ # this change; reading them here only moves *when* they're read, not
1095
+ # what they read.
1096
+ #
1097
+ # Optimization reads the callback field first, then the shared
1098
+ # contextvar. GEPA's start hook and both SDK compile wrappers
1099
+ # both publish the reserved id on the installed callback before
1100
+ # evaluator workers start. The contextvar supports a call with no
1101
+ # active optimization field on this callback.
1102
+ #
1103
+ # Eval has no such gap — on_evaluate_start always sets the instance
1104
+ # field in the same call that sets the contextvar — so it stays
1105
+ # instance-field only, deliberately without the same fallback: adding
1106
+ # one would read a DIFFERENT callback's eval off the same process-wide
1107
+ # contextvar for a nested scoped client with no eval of its own.
1108
+ #
1109
+ # Both are callback-wide fields, not thread- or call-scoped: a
1110
+ # concurrent unrelated call on another thread sharing this callback,
1111
+ # or a nested scoped client's call inside another client's
1112
+ # optimization metric, can still adopt a run it did not start via the
1113
+ # contextvar fallback. Pre-existing behavior, unchanged by this
1114
+ # attach-at-start change.
1115
+ if self._active_optimization_run_id:
1116
+ trace.optimization_run_id = self._active_optimization_run_id
1117
+ else:
1118
+ ctx_opt_run_id = context.get_current_optimization_run_id()
1119
+ if ctx_opt_run_id:
1120
+ trace.optimization_run_id = ctx_opt_run_id
1121
+
1122
+ if self._active_eval_run:
1123
+ trace.eval_run_id = self._active_eval_run.eval_run_id
1124
+
1088
1125
  # Capture signature info for the root module - this identifies the program
1089
1126
  signature = None
1090
1127
  if hasattr(instance, "signature"):
@@ -1494,6 +1531,8 @@ class _LMMixin(_CallbackBase):
1494
1531
  trace.signature_name = span.model_name or phase
1495
1532
  trace.status = span.status
1496
1533
  trace.optimization_run_id = run_id
1534
+ if self._active_eval_run is not None:
1535
+ trace.eval_run_id = self._active_eval_run.eval_run_id
1497
1536
  trace.tags["optimization_phase"] = phase
1498
1537
 
1499
1538
  # Single-span trace: roll the span's usage up onto the trace so the
@@ -6,9 +6,11 @@ top-level `cmpnd ps`, `status`, `logs`, `inspect`, `run` shortcuts.
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
+ import sys
9
10
  from argparse import ArgumentParser, _SubParsersAction
10
11
  from typing import Any
11
12
 
13
+ from ... import _http
12
14
  from .. import api_client, deprecation, pagination
13
15
 
14
16
 
@@ -64,7 +66,11 @@ def handle_run(client: api_client.ApiClient, args: Any) -> Any:
64
66
  body = json.loads(args.body)
65
67
  except json.JSONDecodeError as exc:
66
68
  raise api_client.CliError(f"Invalid JSON for run body: {exc}")
67
- return client.post(f"/api/v1/deployments/{args.id}/execute", json=body)
69
+ response = client.post_response(f"/api/v1/deployments/{args.id}/execute", json=body)
70
+ output, run_id = _http.execute_response_output(response)
71
+ if run_id:
72
+ print(f"Deployment run: {run_id}", file=sys.stderr)
73
+ return output
68
74
 
69
75
 
70
76
  def handle_openapi(client: api_client.ApiClient, args: Any) -> None:
@@ -863,6 +863,11 @@ def cmd_deploy(client: api_client.ApiClient, args: Any) -> None:
863
863
  raise api_client.CliError(f"--run JSON is invalid: {exc}")
864
864
  print(_c(f"executing {_short_id(deployment_id)}…", "dim"), file=sys.stderr)
865
865
  result = deployed(**inputs)
866
+ from ...execution import deployment_run_id
867
+
868
+ run_id = deployment_run_id(result)
869
+ if run_id:
870
+ print(f"Deployment run: {run_id}", file=sys.stderr)
866
871
  if args.json:
867
872
  # result is a dspy.Prediction; json.dumps would fall through to
868
873
  # str(result) and emit the repr. Coerce to a dict first.
@@ -990,20 +995,32 @@ def cmd_run(client: api_client.ApiClient, args: Any) -> None:
990
995
  raise api_client.CliError(f"Invalid JSON: {exc}")
991
996
 
992
997
  try:
993
- result = client.post(f"/api/v1/deployments/{did}/execute", json=body)
998
+ response = client.post_response(f"/api/v1/deployments/{did}/execute", json=body)
994
999
  except api_client.CliError as e:
995
1000
  # Map known 409/422/502 errors to friendlier hints. ApiClient already
996
1001
  # formats the message; just append next-step hints when we recognize
997
1002
  # the pattern.
998
1003
  msg = str(e)
999
1004
  if "409" in msg or "is parsing" in msg or "is pending" in msg:
1000
- raise api_client.CliError(f"{msg}\nWait for it: {_hint_status(did, follow=True)}")
1005
+ raise api_client.CliError(
1006
+ f"{msg}\nWait for it: {_hint_status(did, follow=True)}",
1007
+ status_code=e.status_code,
1008
+ deployment_run_id=e._cmpnd_deployment_run_id,
1009
+ )
1001
1010
  if "422" in msg and "provider" in msg.lower():
1002
1011
  raise api_client.CliError(
1003
- f"{msg}\nNo provider key configured. Provider-key management is not yet available."
1012
+ f"{msg}\nNo provider key configured. Provider-key management is not yet available.",
1013
+ status_code=e.status_code,
1014
+ deployment_run_id=e._cmpnd_deployment_run_id,
1004
1015
  )
1005
1016
  raise
1006
1017
 
1018
+ from ... import _http
1019
+
1020
+ result, run_id = _http.execute_response_output(response)
1021
+ if run_id:
1022
+ print(f"Deployment run: {run_id}", file=sys.stderr)
1023
+
1007
1024
  if args.json:
1008
1025
  print(json.dumps(result, indent=2, default=str))
1009
1026
  return
@@ -8,7 +8,7 @@ from __future__ import annotations
8
8
 
9
9
  import inspect
10
10
  import keyword
11
- from typing import TYPE_CHECKING
11
+ from typing import TYPE_CHECKING, Any
12
12
 
13
13
  import httpx
14
14
 
@@ -25,6 +25,21 @@ if TYPE_CHECKING:
25
25
  from dspy.primitives.prediction import Prediction
26
26
 
27
27
 
28
+ def deployment_run_id(result_or_error: Any) -> str | None:
29
+ """Return the server-owned run id from an execute prediction or exception.
30
+
31
+ The id lives outside the prediction's program output fields. Older servers
32
+ and failures before the server resolves the deployment supply no run id and
33
+ return ``None``.
34
+ """
35
+ return getattr(result_or_error, "_cmpnd_deployment_run_id", None)
36
+
37
+
38
+ def _with_run_id(error: Exception, response: httpx.Response) -> Exception:
39
+ error._cmpnd_deployment_run_id = _http.response_deployment_run_id(response) # pyright: ignore[reportAttributeAccessIssue]
40
+ return error
41
+
42
+
28
43
  class RetryableProviderError(HTTPStatusError):
29
44
  """A provider failure the platform identified as safe to retry.
30
45
 
@@ -256,7 +271,9 @@ def execute(deployment_id, timeout=30, metadata=None, tags=None, **inputs):
256
271
  keys (``lms``, ``deployment_id``, ...).
257
272
 
258
273
  Returns:
259
- A dspy.Prediction with the program's output fields.
274
+ A dspy.Prediction with the program's output fields. Read the run id with
275
+ ``cmpnd.deployment_run_id(result)``. The same accessor accepts an
276
+ exception from a failed execute.
260
277
 
261
278
  Raises:
262
279
  TypeError: If metadata or tags is provided and is not a ``dict[str, str]``.
@@ -288,10 +305,10 @@ def execute(deployment_id, timeout=30, metadata=None, tags=None, **inputs):
288
305
  )
289
306
 
290
307
  if resp.status_code == 409:
291
- raise RuntimeError(f"Deployment {dep_id} is not ready")
308
+ raise _with_run_id(RuntimeError(f"Deployment {dep_id} is not ready"), resp)
292
309
  if resp.status_code == 422:
293
310
  data = resp.json()
294
- raise RuntimeError(data.get("error", "Execution failed"))
311
+ raise _with_run_id(RuntimeError(data.get("error", "Execution failed")), resp)
295
312
 
296
313
  if resp.status_code >= 400:
297
314
  # Same exception `raise_for_status()` would raise — the type is a contract:
@@ -310,17 +327,26 @@ def execute(deployment_id, timeout=30, metadata=None, tags=None, **inputs):
310
327
  provider_failure = _retryable_provider_failure(resp)
311
328
  if provider_failure is not None:
312
329
  provider_status, attempts, retry_after = provider_failure
313
- raise RetryableProviderError(
330
+ raise _with_run_id(
331
+ RetryableProviderError(
332
+ message,
333
+ request=resp.request,
334
+ response=resp,
335
+ provider_status=provider_status,
336
+ attempts=attempts,
337
+ retry_after=retry_after,
338
+ ),
339
+ resp,
340
+ )
341
+ raise _with_run_id(
342
+ HTTPStatusError(
314
343
  message,
315
344
  request=resp.request,
316
345
  response=resp,
317
- provider_status=provider_status,
318
- attempts=attempts,
319
- retry_after=retry_after,
320
- )
321
- raise HTTPStatusError(
322
- message,
323
- request=resp.request,
324
- response=resp,
346
+ ),
347
+ resp,
325
348
  )
326
- return Prediction(**resp.json())
349
+ output, run_id = _http.execute_response_output(resp)
350
+ prediction = Prediction(**output)
351
+ setattr(prediction, "_cmpnd_deployment_run_id", run_id)
352
+ return prediction
@@ -949,7 +949,7 @@ class TraceBuilder:
949
949
  }
950
950
 
951
951
  def to_start_dict(self) -> dict[str, Any]:
952
- """Lightweight payload for trace start — identity + signature only."""
952
+ """Lightweight payload for trace identity, signature and run attribution."""
953
953
  return {
954
954
  "start_time": self.start_time.isoformat() if self.start_time else None,
955
955
  "program_name": self.program_name,
@@ -963,8 +963,14 @@ class TraceBuilder:
963
963
  "signature_summary": self.signature_summary,
964
964
  "component_signatures": _safe_serialize(self.component_signatures),
965
965
  "root_span_type": self.root_span_type,
966
+ "compiled_from_run_id": self.compiled_from_run_id,
966
967
  "project": self.project_tags[0] if self.project_tags else None,
967
968
  "project_tags": self.project_tags if self.project_tags else [],
969
+ # Known at start (the callback attaches them in _create_trace), so
970
+ # the start row carries them from the first send instead of the
971
+ # finalize row re-attributing them later.
972
+ "eval_run_id": str(self.eval_run_id) if self.eval_run_id else None,
973
+ "optimization_run_id": (str(self.optimization_run_id) if self.optimization_run_id else None),
968
974
  }
969
975
 
970
976
  def to_finalize_dict(self) -> dict[str, Any]:
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  import logging
6
6
  from typing import TYPE_CHECKING, Any
7
7
 
8
- from .. import identity
8
+ from .. import _gepa_patch, identity
9
9
  from . import gepa_callback, tracker
10
10
 
11
11
  if TYPE_CHECKING:
@@ -199,10 +199,16 @@ class TrackedGEPA:
199
199
  # Inject callback into GEPA's gepa_kwargs
200
200
  existing_callbacks = self._gepa.gepa_kwargs.get("callbacks") or []
201
201
  self._gepa.gepa_kwargs["callbacks"] = existing_callbacks + [gcb]
202
+ restore_attribution = None
202
203
 
203
204
  try:
205
+ restore_attribution = gcb.start_trace_attribution()
204
206
  # Run optimization (trainset/valset are keyword-only in GEPA)
205
- result = self._gepa.compile(student, trainset=trainset, teacher=teacher, valset=valset)
207
+ compile_token = _gepa_patch._tracked_compile_optimizer.set(self._gepa)
208
+ try:
209
+ result = self._gepa.compile(student, trainset=trainset, teacher=teacher, valset=valset)
210
+ finally:
211
+ _gepa_patch._tracked_compile_optimizer.reset(compile_token)
206
212
 
207
213
  # Capture the compiled program state
208
214
  self._tracker.capture_compiled_program(result)
@@ -235,11 +241,8 @@ class TrackedGEPA:
235
241
  raise
236
242
 
237
243
  finally:
238
- # Clear optimization context (in case on_optimization_end didn't fire)
239
- from .. import context
240
-
241
- context.set_current_optimization_run_id(None)
242
- gcb.clear_trace_association_state()
244
+ if restore_attribution is not None:
245
+ restore_attribution()
243
246
  # Remove our callback from gepa_kwargs
244
247
  cbs = self._gepa.gepa_kwargs.get("callbacks", [])
245
248
  self._gepa.gepa_kwargs["callbacks"] = [cb for cb in cbs if cb is not gcb]
@@ -14,6 +14,7 @@ from __future__ import annotations
14
14
 
15
15
  import logging
16
16
  import time
17
+ from collections.abc import Callable
17
18
  from typing import Any
18
19
  from uuid import UUID
19
20
 
@@ -171,6 +172,26 @@ class CmpndGEPACallback:
171
172
  if cmpnd_cb:
172
173
  cmpnd_cb._clear_optimization_trace_state()
173
174
 
175
+ def start_trace_attribution(self) -> Callable[[], None]:
176
+ """Reserve and publish this run before compile, then return its cleanup."""
177
+ cmpnd_cb = self._get_cmpnd_callback()
178
+ prior_callback_run_id = cmpnd_cb._active_optimization_run_id if cmpnd_cb is not None else None
179
+ prior_context_run_id = context.get_current_optimization_run_id()
180
+ self._tracker.start()
181
+ context.set_current_optimization_run_id(self._tracker.optimization_run_id)
182
+ if cmpnd_cb is not None:
183
+ # Evaluator threads need not inherit contextvars, and older GEPA
184
+ # versions notify their start hook after the seed evaluation.
185
+ cmpnd_cb._active_optimization_run_id = self._tracker.optimization_run_id
186
+
187
+ def restore() -> None:
188
+ if cmpnd_cb is not None:
189
+ cmpnd_cb._clear_optimization_trace_state()
190
+ cmpnd_cb._active_optimization_run_id = prior_callback_run_id
191
+ context.set_current_optimization_run_id(prior_context_run_id)
192
+
193
+ return restore
194
+
174
195
  def on_optimization_start(self, event: dict[str, Any]) -> None:
175
196
  """Handle optimization start: set context and start tracker."""
176
197
  try:
@@ -345,6 +345,38 @@ The create POST is the one that mints something, so it carries the [minting-POST
345
345
 
346
346
  The poll loop has no retry budget of its own: the caller's `timeout` is the only bound, since giving up early on a deployment that is still progressing is the failure it exists to avoid. Running that budget out is still a `TimeoutError`, now naming what it rode through. A `4xx` and a `status: "failed"` are answers, not blips, and still raise (`RuntimeError` / `DeployFailed`).
347
347
 
348
+ ### Execute returns a deployment run id
349
+
350
+ Serve mints a deployment run id before it sends an execute to the worker. A
351
+ successful JSON response contains `deployment_run_id` and `output`. A failed
352
+ execution keeps its error body and adds the run id. Errors before serve resolves
353
+ the deployment have no id.
354
+
355
+ `cmpnd.execute()` and a callable `DeployedProgram` return the program's output
356
+ as a `dspy.Prediction`. `cmpnd.deployment_run_id(result)` reads the id from
357
+ private prediction metadata, so a program can use `output` or
358
+ `deployment_run_id` as an output field. The accessor also accepts an execute
359
+ exception. Trace delivery can finish after the execute response arrives.
360
+
361
+ ```python
362
+ try:
363
+ result = deployed(question="What changed?")
364
+ run_id = cmpnd.deployment_run_id(result)
365
+ except Exception as error:
366
+ run_id = cmpnd.deployment_run_id(error)
367
+ raise
368
+ ```
369
+
370
+ The SDK reads the run id and program output from the response body.
371
+ `cmpnd run`, `cmpnd deployments run` and `cmpnd deploy --run` print the run id
372
+ on stderr and program output on stdout. `--json` keeps stdout as the program's
373
+ JSON output. A CLI execution
374
+ error carries the id in its message and exception metadata.
375
+
376
+ SDK and CLI version 0.32.0 support this envelope. The API does not yet guarantee
377
+ response compatibility across client versions. Upgrade `cmpnd` to the latest
378
+ version when a client has trouble with execute responses.
379
+
348
380
  ### A programmatic deploy carries the types it names as generated source
349
381
 
350
382
  `cmpnd.deploy(module)` receives a live object rather than an authored file. Its
@@ -510,6 +542,47 @@ Two producers put a `span` item and its trace's `trace` item in the same flush.
510
542
 
511
543
  `trace.streaming` (set only for RLM) is a separate axis: it still governs whether individual spans stream (`export_span`) and whether finalize goes through `/finalize` (streaming) vs a bundled `export_trace` (batch). CMP-257 generalized only the start emission, not the whole streaming model.
512
544
 
545
+ `_create_trace` also attaches `optimization_run_id` and `eval_run_id` at trace start, so the start row carries both from its first send. `on_module_end` never re-reads either source at finalize: a trace's module call can outlive the optimization or eval it started under, and re-reading at finalize would send a later update that clears the ids the start row already carried. A finalizing trace always keeps the ids it started with.
546
+
547
+ Optimization reads this callback's `_active_optimization_run_id` field first,
548
+ then the shared `cmpnd.context` contextvar. Both SDK compile wrappers publish
549
+ the reserved id on the installed callback before evaluator workers start,
550
+ including when native GEPA fires its start hook after the seed evaluation or
551
+ has no callback support. The wrappers restore the previous
552
+ callback and context ids when the search returns or raises. Eval reads only
553
+ the callback field. `on_evaluate_start` sets it before the evaluator workers
554
+ start, so eval needs no contextvar fallback.
555
+
556
+ The callback fields let evaluator threads read the active run ids. A plain
557
+ thread need not inherit Python contextvars, and DSPy versions and wrappers
558
+ differ in how they carry context to workers. The fields also let an unrelated
559
+ concurrent call sharing the callback adopt the active run. A nested scoped
560
+ client can adopt another client's optimization through the contextvar
561
+ fallback. Run attribution therefore requires calls that belong to the run
562
+ owning this callback. Callback-wide state does not prove ownership across
563
+ overlapping runs or scoped clients.
564
+
565
+ The callback has two automatic trace constructors. `_create_trace` snapshots
566
+ the root module's run ids before it exports a start. `EvalHandler` obtains the
567
+ eval id synchronously before evaluator workers call the program.
568
+ `OptimizationTracker.start()` obtains the optimization id before rollouts.
569
+ Both SDK compile wrappers set that id on the installed callback and in the
570
+ contextvar before compile. GEPA's start callback also sets it when the event
571
+ arrives. The tracker reserves one id even when both paths call `start()`.
572
+ `TrackedGEPA` also claims its native optimizer while calling compile. The
573
+ automatic class patch then runs that instance's search without reserving a
574
+ second id. The claim ends when compile returns or raises. An instance override
575
+ still runs, and a distinct optimizer nested inside it gets its own run.
576
+ The start payload also carries the compiled program's `compiled_from_run_id`.
577
+
578
+ Nested modules add spans to their root trace. `asyncify`, native `acall` and
579
+ `streamify` dispatch through the module hooks. RLM exports its start before it
580
+ streams spans and keeps those run ids through finalize. A standalone GEPA LM
581
+ trace is constructed when the LM call ends and exports as one complete trace.
582
+ Its first payload carries the optimization id, plus the active eval id for a
583
+ metric call. A reflection call outside evaluation has no eval id. Manual
584
+ decorators and `start_span` create spans rather than another automatic trace.
585
+
513
586
  Two things keep the extra start cheap and correct:
514
587
 
515
588
  - **Upsert, not duplicate.** The batch finalize (`export_trace`) and the start write the same row: serve's `traceUpsertSQL` keys `ON CONFLICT (org_id, trace_id, start_time)`, and the SDK's `start_time` is immutable across a trace's life, so the batch overwrites the `in_progress` row with the final status. A trace that never finalizes (crash / cancel) correctly stays `in_progress`.
@@ -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.31.0"
8
+ version = "0.32.0"
9
9
  description = "DSPy observability and deployment SDK for cmpnd"
10
10
  readme = "README.md"
11
11
  requires-python = ">=3.10"