cmpnd 0.42.2__tar.gz → 0.43.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 (373) hide show
  1. {cmpnd-0.42.2 → cmpnd-0.43.0}/PKG-INFO +1 -1
  2. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/__init__.py +3 -0
  3. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/callback.py +39 -3
  4. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/client.py +61 -10
  5. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/programs.py +64 -0
  6. {cmpnd-0.42.2 → cmpnd-0.43.0}/docs/client.md +37 -0
  7. {cmpnd-0.42.2 → cmpnd-0.43.0}/docs/sdk-instrumentation.md +7 -1
  8. {cmpnd-0.42.2 → cmpnd-0.43.0}/ingest/cmpnd_ingest/exporter_http.py +16 -2
  9. {cmpnd-0.42.2 → cmpnd-0.43.0}/ingest/cmpnd_ingest/program_names.py +5 -0
  10. {cmpnd-0.42.2 → cmpnd-0.43.0}/pyproject.toml +1 -1
  11. cmpnd-0.43.0/tests/test_client_program_names.py +339 -0
  12. {cmpnd-0.42.2 → cmpnd-0.43.0}/uv.lock +1 -1
  13. {cmpnd-0.42.2 → cmpnd-0.43.0}/.gitignore +0 -0
  14. {cmpnd-0.42.2 → cmpnd-0.43.0}/AGENTS.md +0 -0
  15. {cmpnd-0.42.2 → cmpnd-0.43.0}/CLAUDE.md +0 -0
  16. {cmpnd-0.42.2 → cmpnd-0.43.0}/README.md +0 -0
  17. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/_assessments.py +0 -0
  18. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/_authored_import.py +0 -0
  19. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/_deployments.py +0 -0
  20. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/_exporter_pool.py +0 -0
  21. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/_gepa_patch.py +0 -0
  22. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/_http.py +0 -0
  23. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/_program_patch.py +0 -0
  24. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/_rlm_patch.py +0 -0
  25. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/_runtime_tools.py +0 -0
  26. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/_timespec.py +0 -0
  27. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/_traces.py +0 -0
  28. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/_valsets.py +0 -0
  29. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/authored.py +0 -0
  30. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/cli/__init__.py +0 -0
  31. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/cli/api_client.py +0 -0
  32. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/cli/commands/__init__.py +0 -0
  33. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/cli/commands/admin.py +0 -0
  34. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/cli/commands/assessments.py +0 -0
  35. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/cli/commands/auth.py +0 -0
  36. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/cli/commands/datasets.py +0 -0
  37. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/cli/commands/deployments.py +0 -0
  38. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/cli/commands/evals.py +0 -0
  39. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/cli/commands/login.py +0 -0
  40. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/cli/commands/optimizations.py +0 -0
  41. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/cli/commands/org.py +0 -0
  42. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/cli/commands/programs.py +0 -0
  43. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/cli/commands/provider_credentials.py +0 -0
  44. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/cli/commands/signatures.py +0 -0
  45. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/cli/commands/tool_servers.py +0 -0
  46. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/cli/commands/top.py +0 -0
  47. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/cli/commands/traces.py +0 -0
  48. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/cli/commands/valsets.py +0 -0
  49. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/cli/credentials.py +0 -0
  50. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/cli/deprecation.py +0 -0
  51. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/cli/pagination.py +0 -0
  52. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/cli/shell.py +0 -0
  53. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/cli/trace_render.py +0 -0
  54. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/cli/tree_help.py +0 -0
  55. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/cli/verbs.py +0 -0
  56. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/configuration.py +0 -0
  57. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/context.py +0 -0
  58. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/dataset_sync.py +0 -0
  59. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/datasets.py +0 -0
  60. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/decorators.py +0 -0
  61. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/deployment.py +0 -0
  62. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/deployment_metadata.py +0 -0
  63. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/deployment_runs.py +0 -0
  64. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/eval_handler.py +0 -0
  65. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/execution.py +0 -0
  66. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/exporter.py +0 -0
  67. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/exporter_http.py +0 -0
  68. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/helpers.py +0 -0
  69. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/identity.py +0 -0
  70. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/imaging.py +0 -0
  71. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/ir/__init__.py +0 -0
  72. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/ir/decode.py +0 -0
  73. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/ir/encode.py +0 -0
  74. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/ir/hash.py +0 -0
  75. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/ir/program.py +0 -0
  76. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/ir/reflection.py +0 -0
  77. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/ir/types.py +0 -0
  78. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/ir/xxh64.py +0 -0
  79. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/lm_configuration.py +0 -0
  80. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/mcp_servers.py +0 -0
  81. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/models.py +0 -0
  82. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/optimization.py +0 -0
  83. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/optimizers/__init__.py +0 -0
  84. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/optimizers/gepa.py +0 -0
  85. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/optimizers/gepa_callback.py +0 -0
  86. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/optimizers/resume.py +0 -0
  87. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/optimizers/tracker.py +0 -0
  88. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/packaging.py +0 -0
  89. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/references.py +0 -0
  90. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/retry.py +0 -0
  91. {cmpnd-0.42.2 → cmpnd-0.43.0}/cmpnd/traces.py +0 -0
  92. {cmpnd-0.42.2 → cmpnd-0.43.0}/docs/2026-04-02-code-review.md +0 -0
  93. {cmpnd-0.42.2 → cmpnd-0.43.0}/docs/cli.md +0 -0
  94. {cmpnd-0.42.2 → cmpnd-0.43.0}/docs/custom-proposer.md +0 -0
  95. {cmpnd-0.42.2 → cmpnd-0.43.0}/docs/plans/2026-04-05-train-val-dataset-capture-design.md +0 -0
  96. {cmpnd-0.42.2 → cmpnd-0.43.0}/docs/plans/2026-04-05-train-val-dataset-capture.md +0 -0
  97. {cmpnd-0.42.2 → cmpnd-0.43.0}/docs/reading-traces.md +0 -0
  98. {cmpnd-0.42.2 → cmpnd-0.43.0}/examples/generalizing_proposer.py +0 -0
  99. {cmpnd-0.42.2 → cmpnd-0.43.0}/examples/local_ollama.py +0 -0
  100. {cmpnd-0.42.2 → cmpnd-0.43.0}/ingest/LINEAGE.md +0 -0
  101. {cmpnd-0.42.2 → cmpnd-0.43.0}/ingest/README.md +0 -0
  102. {cmpnd-0.42.2 → cmpnd-0.43.0}/ingest/cmpnd_ingest/__init__.py +0 -0
  103. {cmpnd-0.42.2 → cmpnd-0.43.0}/ingest/cmpnd_ingest/bodies.py +0 -0
  104. {cmpnd-0.42.2 → cmpnd-0.43.0}/ingest/cmpnd_ingest/protocols.py +0 -0
  105. {cmpnd-0.42.2 → cmpnd-0.43.0}/ingest/cmpnd_ingest/py.typed +0 -0
  106. {cmpnd-0.42.2 → cmpnd-0.43.0}/ingest/cmpnd_ingest/request_log.py +0 -0
  107. {cmpnd-0.42.2 → cmpnd-0.43.0}/ingest/cmpnd_ingest/retry.py +0 -0
  108. {cmpnd-0.42.2 → cmpnd-0.43.0}/ingest/cmpnd_ingest/trace_facts.py +0 -0
  109. {cmpnd-0.42.2 → cmpnd-0.43.0}/ingest/pyproject.toml +0 -0
  110. {cmpnd-0.42.2 → cmpnd-0.43.0}/scripts/audit-sdist.sh +0 -0
  111. {cmpnd-0.42.2 → cmpnd-0.43.0}/scripts/capture_review_run.py +0 -0
  112. {cmpnd-0.42.2 → cmpnd-0.43.0}/scripts/check-pip-install.sh +0 -0
  113. {cmpnd-0.42.2 → cmpnd-0.43.0}/scripts/check_release_artifacts.py +0 -0
  114. {cmpnd-0.42.2 → cmpnd-0.43.0}/scripts/fixtures/review_run.json.gz +0 -0
  115. {cmpnd-0.42.2 → cmpnd-0.43.0}/scripts/gepa_resume_bug.py +0 -0
  116. {cmpnd-0.42.2 → cmpnd-0.43.0}/scripts/release-stress.sh +0 -0
  117. {cmpnd-0.42.2 → cmpnd-0.43.0}/scripts/require_version_bump.py +0 -0
  118. {cmpnd-0.42.2 → cmpnd-0.43.0}/scripts/seed_review_run.py +0 -0
  119. {cmpnd-0.42.2 → cmpnd-0.43.0}/scripts/test-in-linux.sh +0 -0
  120. {cmpnd-0.42.2 → cmpnd-0.43.0}/stubs/dspy/__init__.pyi +0 -0
  121. {cmpnd-0.42.2 → cmpnd-0.43.0}/stubs/dspy/primitives/__init__.pyi +0 -0
  122. {cmpnd-0.42.2 → cmpnd-0.43.0}/stubs/dspy/primitives/prediction.pyi +0 -0
  123. {cmpnd-0.42.2 → cmpnd-0.43.0}/stubs/dspy/utils/__init__.pyi +0 -0
  124. {cmpnd-0.42.2 → cmpnd-0.43.0}/stubs/dspy/utils/callback.pyi +0 -0
  125. {cmpnd-0.42.2 → cmpnd-0.43.0}/stubs/dspy/utils/dummies.pyi +0 -0
  126. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/__init__.py +0 -0
  127. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/_deployment_signature_helpers.py +0 -0
  128. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/_exporter_helpers.py +0 -0
  129. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/_fake_clock.py +0 -0
  130. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/_identity_helpers.py +0 -0
  131. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/_resolve_refs.py +0 -0
  132. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/_traced_run.py +0 -0
  133. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/conftest.py +0 -0
  134. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/e2e/__init__.py +0 -0
  135. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/e2e/committee_task.py +0 -0
  136. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/e2e/conftest.py +0 -0
  137. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/e2e/data/committee_train.ndjson.gz +0 -0
  138. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/e2e/data/committee_val.ndjson.gz +0 -0
  139. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/e2e/fake_lm.py +0 -0
  140. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/e2e/helpers.py +0 -0
  141. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/e2e/target_capabilities.py +0 -0
  142. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/e2e/test_assessments_cli.py +0 -0
  143. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/e2e/test_capture_prompts.py +0 -0
  144. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/e2e/test_composite_module.py +0 -0
  145. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/e2e/test_dataset_auto_creation.py +0 -0
  146. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/e2e/test_deploy_execute.py +0 -0
  147. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/e2e/test_deploy_pandas.py +0 -0
  148. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/e2e/test_deploy_programs.py +0 -0
  149. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/e2e/test_edge_cases.py +0 -0
  150. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/e2e/test_example_hash.py +0 -0
  151. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/e2e/test_gepa_optimization.py +0 -0
  152. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/e2e/test_interpreter_spans.py +0 -0
  153. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/e2e/test_lm_accuracy.py +0 -0
  154. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/e2e/test_module_tracing.py +0 -0
  155. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/e2e/test_optimization_lineage.py +0 -0
  156. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/e2e/test_optimization_metric_identity.py +0 -0
  157. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/e2e/test_optimize_failures.py +0 -0
  158. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/e2e/test_optimize_loop.py +0 -0
  159. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/e2e/test_optimize_review_committee.py +0 -0
  160. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/e2e/test_optimize_server_owned_completion.py +0 -0
  161. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/e2e/test_parallel_export.py +0 -0
  162. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/e2e/test_project_attribution.py +0 -0
  163. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/e2e/test_react_tool_spans.py +0 -0
  164. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/e2e/test_save_load.py +0 -0
  165. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/e2e/test_trace_structure.py +0 -0
  166. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/e2e/test_traces_read.py +0 -0
  167. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/e2e/test_user_attribution.py +0 -0
  168. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/e2e/test_valsets_cli.py +0 -0
  169. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/identity_vectors/01_predict_qa.json +0 -0
  170. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/identity_vectors/02_predict_renamed_field.json +0 -0
  171. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/identity_vectors/03_chain_of_thought_qa.json +0 -0
  172. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/identity_vectors/04_predict_richer_types.json +0 -0
  173. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/identity_vectors/05_composite_two_predicts.json +0 -0
  174. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/identity_vectors/06_multichaincomparison.json +0 -0
  175. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/identity_vectors/07_program_of_thought.json +0 -0
  176. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/identity_vectors/08_opaque_module.json +0 -0
  177. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/identity_vectors/09_predict_pydantic_field.json +0 -0
  178. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/identity_vectors/10_react.json +0 -0
  179. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/identity_vectors/11_refine.json +0 -0
  180. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/identity_vectors/12_retrieve.json +0 -0
  181. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/identity_vectors/13_knn.json +0 -0
  182. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/identity_vectors/14_parallel.json +0 -0
  183. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/identity_vectors/README.md +0 -0
  184. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/ingest_conformance.py +0 -0
  185. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/integration/__init__.py +0 -0
  186. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/integration/authored_optimize/judge.py +0 -0
  187. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/integration/authored_optimize/optimize_hosted.py +0 -0
  188. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/integration/conftest.py +0 -0
  189. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/integration/hosted_mcp_tool_optimize_probe.py +0 -0
  190. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/integration/hosted_optimize_probe.py +0 -0
  191. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/integration/optimization_run_probe.py +0 -0
  192. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/integration/test_cli_deploy_smoketest.py +0 -0
  193. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/integration/test_logs_renders_traces.py +0 -0
  194. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/integration/test_optimization_run.py +0 -0
  195. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/integration/test_optimizing_progress.py +0 -0
  196. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/integration/test_unhappy_path.py +0 -0
  197. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/load/__init__.py +0 -0
  198. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/load/__main__.py +0 -0
  199. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/load/drain.py +0 -0
  200. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/load/ingest.py +0 -0
  201. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/load/latency_transport.py +0 -0
  202. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/load/rlm_shape.py +0 -0
  203. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/load/rlm_size_report.py +0 -0
  204. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/load/sweep.py +0 -0
  205. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/load/test_rlm_shape.py +0 -0
  206. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/load/test_rlm_size_report.py +0 -0
  207. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/parity/__init__.py +0 -0
  208. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/parity/conftest.py +0 -0
  209. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/parity/seed_parity.py +0 -0
  210. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/parity/test_auth.py +0 -0
  211. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/parity/test_datasets.py +0 -0
  212. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/parity/test_deployments.py +0 -0
  213. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/parity/test_evals.py +0 -0
  214. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/parity/test_health.py +0 -0
  215. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/parity/test_modules.py +0 -0
  216. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/parity/test_optimizations.py +0 -0
  217. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/parity/test_programs.py +0 -0
  218. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/parity/test_signatures.py +0 -0
  219. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/parity/test_trace_streaming.py +0 -0
  220. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/parity/test_traces.py +0 -0
  221. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/span_reference_fixture.py +0 -0
  222. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/stress/__init__.py +0 -0
  223. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/stress/_provider.py +0 -0
  224. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/stress/_readiness.py +0 -0
  225. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/stress/_reliability.py +0 -0
  226. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/stress/_result.py +0 -0
  227. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/stress/mint_bedrock_token.py +0 -0
  228. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/stress/test_deploy_programs.py +0 -0
  229. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/stress/test_multi_client_stress.py +0 -0
  230. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/stress/test_optimize_client_committee.py +0 -0
  231. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/stress/test_optimize_loop.py +0 -0
  232. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/stress/test_optimize_loop_wasm.py +0 -0
  233. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/stress/test_optimize_stress.py +0 -0
  234. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/stress/test_provider_roles.py +0 -0
  235. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/stress/test_readiness.py +0 -0
  236. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/stress/test_real_provider_twins.py +0 -0
  237. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/stress/test_reliability.py +0 -0
  238. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/stress/test_reliability_classify.py +0 -0
  239. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/stress/test_rlm_wasm.py +0 -0
  240. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/stress/test_trace_search.py +0 -0
  241. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_adapter_reentrancy.py +0 -0
  242. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_assessments_api.py +0 -0
  243. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_authored.py +0 -0
  244. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_authored_bundle_collisions.py +0 -0
  245. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_authored_metric_contract.py +0 -0
  246. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_authored_named_models.py +0 -0
  247. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_authored_optimize.py +0 -0
  248. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_callback.py +0 -0
  249. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_candidate_lineage.py +0 -0
  250. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_cli/__init__.py +0 -0
  251. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_cli/conftest.py +0 -0
  252. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_cli/test_admin.py +0 -0
  253. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_cli/test_api_client.py +0 -0
  254. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_cli/test_assessments.py +0 -0
  255. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_cli/test_auth.py +0 -0
  256. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_cli/test_credentials.py +0 -0
  257. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_cli/test_datasets.py +0 -0
  258. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_cli/test_deployment_runs.py +0 -0
  259. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_cli/test_deployments.py +0 -0
  260. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_cli/test_evals.py +0 -0
  261. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_cli/test_login.py +0 -0
  262. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_cli/test_main.py +0 -0
  263. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_cli/test_optimizations.py +0 -0
  264. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_cli/test_org.py +0 -0
  265. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_cli/test_pagination.py +0 -0
  266. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_cli/test_parity.py +0 -0
  267. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_cli/test_programs.py +0 -0
  268. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_cli/test_provider_credentials.py +0 -0
  269. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_cli/test_schema_sample.py +0 -0
  270. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_cli/test_shell.py +0 -0
  271. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_cli/test_signatures.py +0 -0
  272. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_cli/test_tool_servers.py +0 -0
  273. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_cli/test_top.py +0 -0
  274. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_cli/test_traces.py +0 -0
  275. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_cli/test_traces_search_wire.py +0 -0
  276. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_cli/test_tree_help.py +0 -0
  277. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_cli/test_valsets.py +0 -0
  278. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_client.py +0 -0
  279. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_clustering/__init__.py +0 -0
  280. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_clustering/conftest.py +0 -0
  281. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_clustering/test_edge_cases.py +0 -0
  282. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_clustering/test_hash_consistency.py +0 -0
  283. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_clustering/test_type_conversion.py +0 -0
  284. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_cmp551_grouping_identity.py +0 -0
  285. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_configuration.py +0 -0
  286. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_context.py +0 -0
  287. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_context_propagation.py +0 -0
  288. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_dataset_polling.py +0 -0
  289. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_dataset_sync.py +0 -0
  290. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_datasets.py +0 -0
  291. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_decorators.py +0 -0
  292. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_deploy.py +0 -0
  293. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_deploy_mcp_tool_references.py +0 -0
  294. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_deploy_program_id.py +0 -0
  295. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_deployment_metadata.py +0 -0
  296. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_deployment_runs.py +0 -0
  297. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_deployments_read.py +0 -0
  298. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_eval.py +0 -0
  299. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_eval_example_attribution.py +0 -0
  300. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_eval_start.py +0 -0
  301. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_eval_trace_grouping.py +0 -0
  302. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_exception_paths.py +0 -0
  303. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_execute.py +0 -0
  304. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_execute_run_identity.py +0 -0
  305. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_exporter.py +0 -0
  306. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_exporter_program_names.py +0 -0
  307. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_exporter_queue.py +0 -0
  308. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_exporter_real.py +0 -0
  309. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_fake_lm.py +0 -0
  310. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_gepa_callback.py +0 -0
  311. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_gepa_integration.py +0 -0
  312. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_gepa_output_normalization.py +0 -0
  313. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_gepa_resume.py +0 -0
  314. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_identity.py +0 -0
  315. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_imaging.py +0 -0
  316. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_import_weight.py +0 -0
  317. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_ingest_conformance.py +0 -0
  318. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_ingest_second_producer.py +0 -0
  319. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_integration.py +0 -0
  320. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_interpreter_callback.py +0 -0
  321. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_ir_encoder_properties.py +0 -0
  322. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_ir_golden_vectors.py +0 -0
  323. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_ir_roundtrip.py +0 -0
  324. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_lm15_native_calls.py +0 -0
  325. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_lm_configuration.py +0 -0
  326. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_lm_usage_shapes.py +0 -0
  327. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_mcp_tools.py +0 -0
  328. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_models.py +0 -0
  329. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_multi_client_stress_readiness.py +0 -0
  330. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_optimization_progress.py +0 -0
  331. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_optimization_start.py +0 -0
  332. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_optimize.py +0 -0
  333. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_optimize_budget.py +0 -0
  334. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_optimize_lift.py +0 -0
  335. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_optimize_lms.py +0 -0
  336. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_optimize_proposer.py +0 -0
  337. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_optimize_reflection_lm.py +0 -0
  338. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_optimize_terminal_failure.py +0 -0
  339. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_packaging.py +0 -0
  340. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_packaging_callable_source.py +0 -0
  341. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_packaging_mcp_tools.py +0 -0
  342. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_packaging_program_source.py +0 -0
  343. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_poll_until.py +0 -0
  344. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_program_identity.py +0 -0
  345. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_program_lineage_persist.py +0 -0
  346. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_programs_create.py +0 -0
  347. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_project_payload.py +0 -0
  348. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_real_provider_twins_readiness.py +0 -0
  349. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_reflector_properties.py +0 -0
  350. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_reflector_smoke.py +0 -0
  351. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_release_stress_script.py +0 -0
  352. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_repro_reactv2_bugs.py +0 -0
  353. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_require_version_bump.py +0 -0
  354. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_retry.py +0 -0
  355. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_reversible_failures.py +0 -0
  356. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_rlm_patch.py +0 -0
  357. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_run_attribution_paths.py +0 -0
  358. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_runtime_tools.py +0 -0
  359. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_schemas.py +0 -0
  360. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_seed_review_run.py +0 -0
  361. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_signature_inputs.py +0 -0
  362. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_span_references.py +0 -0
  363. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_span_sent_once.py +0 -0
  364. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_trace_context_concurrency.py +0 -0
  365. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_trace_fact_wire_contract.py +0 -0
  366. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_trace_facts.py +0 -0
  367. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_trace_render.py +0 -0
  368. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_traces_read.py +0 -0
  369. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_tracked_gepa.py +0 -0
  370. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_tracker_enrich.py +0 -0
  371. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_truncation.py +0 -0
  372. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_valsets_api.py +0 -0
  373. {cmpnd-0.42.2 → cmpnd-0.43.0}/tests/test_xxh64.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cmpnd
3
- Version: 0.42.2
3
+ Version: 0.43.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>
@@ -60,6 +60,7 @@ from .optimization import (
60
60
  OptimizeFailed,
61
61
  optimize,
62
62
  )
63
+ from .programs import Program
63
64
 
64
65
  try:
65
66
  __version__ = importlib.metadata.version("cmpnd")
@@ -100,6 +101,8 @@ __all__ = [
100
101
  "traces",
101
102
  # Program ids (cmpnd.programs.create)
102
103
  "programs",
104
+ # A program declared by name
105
+ "Program",
103
106
  "authored",
104
107
  "ApiError",
105
108
  # Types
@@ -24,6 +24,21 @@ _current_interpreter_tool_frame: contextvars.ContextVar[dict[str, Any] | None] =
24
24
  )
25
25
 
26
26
 
27
+ # The DSPy settings key that carries the name of the program a client is running.
28
+ # ``cmpnd.Client`` sets it with ``dspy.context`` around a call, so it reaches the
29
+ # threads DSPy starts for evaluation and parallel work, and the callback stamps it on
30
+ # the trace it creates for the call. None means the call declares no program.
31
+ DECLARED_PROGRAM_SETTING = "cmpnd_declared_program"
32
+
33
+
34
+ def _declared_program_name() -> str | None:
35
+ """The program name the current call declares, read from DSPy's settings."""
36
+ import dspy
37
+
38
+ name = dspy.settings.get(DECLARED_PROGRAM_SETTING)
39
+ return name if isinstance(name, str) else None
40
+
41
+
27
42
  def trace_id(prediction: Any) -> str | None:
28
43
  """Return the cmpnd trace id for a prediction returned by an instrumented program.
29
44
 
@@ -502,6 +517,9 @@ class _CallbackBase:
502
517
  # later cmpnd.configure(). That fixedness is what keeps a client
503
518
  # orthogonal to the global config.
504
519
  self._own_config = own_config
520
+ # Set once this callback has warned that a declared name replaced its
521
+ # configured program id, so the warning appears once per client.
522
+ self._warned_name_over_id = False
505
523
 
506
524
  # Resolve feature flags: explicit param > config > default
507
525
  config = own_config or configuration.get_config()
@@ -652,12 +670,30 @@ class _CallbackBase:
652
670
  return configuration._current_program_id()
653
671
 
654
672
  def _stamp(self, builder: Any) -> Any:
655
- """Stamp this callback's project tags, and on a trace its program id, onto a
673
+ """Stamp this callback's project tags, and on a trace its program, onto a
656
674
  builder it just created. A span's program is its trace's, so a span carries
657
- no id of its own."""
675
+ no program of its own.
676
+
677
+ A trace from a call that declares a program name carries the name and no id,
678
+ and the exporter resolves the name. The declared name wins over a program id
679
+ the client was configured with, and the callback warns once that it did.
680
+ """
658
681
  builder.project_tags = self._resolve_project_tags()
659
682
  if isinstance(builder, models.TraceBuilder):
660
- builder.program_id = self._resolve_program_id()
683
+ program_id = self._resolve_program_id()
684
+ name = _declared_program_name()
685
+ if name is not None:
686
+ if program_id is not None and not self._warned_name_over_id:
687
+ self._warned_name_over_id = True
688
+ logger.warning(
689
+ "cmpnd: traces of program %r go to the program with that name, not to the configured "
690
+ "program id %s.",
691
+ name,
692
+ program_id,
693
+ )
694
+ builder.declared_name = name
695
+ program_id = None
696
+ builder.program_id = program_id
661
697
  return builder
662
698
 
663
699
  def _current_exporter(self) -> Any:
@@ -25,10 +25,11 @@ CLI groups from customers.
25
25
 
26
26
  from __future__ import annotations
27
27
 
28
+ import dataclasses
28
29
  import os
29
30
  from typing import Any, Literal
30
31
 
31
- from . import _assessments, _deployments, _exporter_pool, _traces, _valsets, callback, configuration
32
+ from . import _assessments, _deployments, _exporter_pool, _traces, _valsets, callback, configuration, programs
32
33
  from .deployment_metadata import DeploymentSignatureResponse
33
34
 
34
35
 
@@ -105,13 +106,28 @@ class Client:
105
106
 
106
107
  # -- scoped runs ------------------------------------------------------
107
108
 
109
+ def _declared(self, program: Any) -> dict[str, Any]:
110
+ """The ``dspy.context`` settings that run ``program`` under its declared name.
111
+
112
+ A ``cmpnd.Program`` runs on its own LM and declares its name. Any other module
113
+ declares no name, which also clears a name an enclosing call declared.
114
+ """
115
+ if isinstance(program, programs.Program):
116
+ return {"lm": program.lm, callback.DECLARED_PROGRAM_SETTING: program.name}
117
+ return {callback.DECLARED_PROGRAM_SETTING: None}
118
+
108
119
  def run_program(self, program: Any, **inputs: Any) -> Any:
109
- """Run ``program(**inputs)`` traced on this client's account."""
120
+ """Run ``program(**inputs)`` traced on this client's account.
121
+
122
+ ``program`` is a ``cmpnd.Program`` or a plain DSPy module. A ``Program`` runs
123
+ on its own LM, and its traces go to the program with its name.
124
+ """
110
125
  self._check_active()
111
126
  import dspy
112
127
 
113
- with dspy.context(callbacks=self._scoped_callbacks()):
114
- return program(**inputs)
128
+ module = program.module if isinstance(program, programs.Program) else program
129
+ with dspy.context(callbacks=self._scoped_callbacks(), **self._declared(program)):
130
+ return module(**inputs)
115
131
 
116
132
  def run_optimization(
117
133
  self,
@@ -133,17 +149,33 @@ class Client:
133
149
  client's own transport, and — when ``auto_instrument`` is also active —
134
150
  suppresses the global class patch so the run is recorded once, on the
135
151
  client's account, not the global one.
152
+
153
+ ``student`` is a ``cmpnd.Program`` or a plain DSPy module. A ``Program`` is
154
+ optimized on its own LM, its rollouts and run record go to the program with
155
+ its name, and the result is a ``Program`` with the same name and LM.
136
156
  """
137
157
  self._check_active()
138
158
  import dspy
139
159
 
140
- with dspy.context(callbacks=self._scoped_callbacks()):
160
+ module = student.module if isinstance(student, programs.Program) else student
161
+ with dspy.context(callbacks=self._scoped_callbacks(), **self._declared(student)):
141
162
  try:
142
163
  if isinstance(optimizer, dspy.GEPA):
143
- return self._run_gepa(optimizer, student, trainset=trainset, valset=valset, kwargs=kwargs)
144
- return optimizer.compile(student, trainset=trainset, valset=valset, **kwargs)
164
+ result = self._run_gepa(
165
+ optimizer,
166
+ module,
167
+ trainset=trainset,
168
+ valset=valset,
169
+ kwargs=kwargs,
170
+ declared_name=student.name if isinstance(student, programs.Program) else None,
171
+ )
172
+ else:
173
+ result = optimizer.compile(module, trainset=trainset, valset=valset, **kwargs)
145
174
  finally:
146
175
  self._callback._clear_optimization_trace_state()
176
+ if isinstance(student, programs.Program):
177
+ return programs.Program(student.name, module=result, lm=student.lm)
178
+ return result
147
179
 
148
180
  # -- data surface (flat verb-object over the client's credentials) -----
149
181
  #
@@ -285,11 +317,30 @@ class Client:
285
317
  self._check_active()
286
318
  return _valsets.delete_entry(entry_id, api_key=self._config.api_key, endpoint=self._config.endpoint)
287
319
 
288
- def _run_gepa(self, optimizer: Any, student: Any, *, trainset: Any, valset: Any, kwargs: dict[str, Any]) -> Any:
289
- """Track a GEPA compile on this client's account and suppress the global patch."""
320
+ def _run_gepa(
321
+ self,
322
+ optimizer: Any,
323
+ student: Any,
324
+ *,
325
+ trainset: Any,
326
+ valset: Any,
327
+ kwargs: dict[str, Any],
328
+ declared_name: str | None = None,
329
+ ) -> Any:
330
+ """Track a GEPA compile on this client's account and suppress the global patch.
331
+
332
+ A declared name is resolved here, on the caller's thread, because the run
333
+ record is sent synchronously when the run starts and must carry its program
334
+ id then. The record goes without an id when the name does not resolve.
335
+ """
290
336
  from . import _exporter_pool, _gepa_patch
291
337
 
292
338
  client_exporter = _exporter_pool.ensure_exporter_started(self._config)
339
+ run_config = self._config
340
+ if declared_name is not None:
341
+ resolve = getattr(client_exporter, "resolve_program_name", None)
342
+ program_id = resolve(declared_name) if callable(resolve) else None
343
+ run_config = dataclasses.replace(self._config, program_id=program_id)
293
344
 
294
345
  def _search() -> Any:
295
346
  return optimizer.compile(student, trainset=trainset, valset=valset, **kwargs)
@@ -302,7 +353,7 @@ class Client:
302
353
  trainset=trainset,
303
354
  valset=valset,
304
355
  run_original=_search,
305
- client_config=self._config,
356
+ client_config=run_config,
306
357
  exporter_override=client_exporter,
307
358
  )
308
359
  finally:
@@ -13,10 +13,17 @@ call that lost its answer leaves a second, empty program behind. That is why
13
13
  from __future__ import annotations
14
14
 
15
15
  import os
16
+ import re
16
17
  from typing import Any
17
18
 
18
19
  from . import _http, configuration
19
20
 
21
+ # A program name is an ASCII letter or digit, then letters, digits, ".", "_" or "-",
22
+ # at most 64 characters. Serve compares names without case and keeps the case typed.
23
+ _NAME = re.compile(r"[A-Za-z0-9][A-Za-z0-9._-]{0,63}")
24
+ # Reserved in any case, so that a name never looks like a program id.
25
+ _RESERVED_PREFIX = "p_"
26
+
20
27
  DEFAULT_ENDPOINT = "https://platform.cmpnd.ai"
21
28
 
22
29
  # Self-host release 2026-001 answers a programs create with 405, because it routes
@@ -87,3 +94,60 @@ def _client(api_key: str | None, endpoint: str | None) -> _http.ApiClient:
87
94
  raise _http.ApiError("No API key. Pass api_key=, call cmpnd.configure(api_key=...), or set CMPND_API_KEY.")
88
95
  resolved_endpoint = endpoint or (cfg.endpoint if cfg else None) or os.getenv("CMPND_ENDPOINT") or DEFAULT_ENDPOINT
89
96
  return _http.ApiClient(resolved_key, resolved_endpoint)
97
+
98
+
99
+ def validate_program_name(name: object) -> str:
100
+ """Return ``name`` when it is a valid program name, and raise ``ValueError`` otherwise.
101
+
102
+ Serve checks the same syntax. Checking it here makes a bad name fail where it is
103
+ declared, with no network call, instead of in the exporter after the program runs.
104
+ """
105
+ if not isinstance(name, str) or not _NAME.fullmatch(name) or name.lower().startswith(_RESERVED_PREFIX):
106
+ raise ValueError(
107
+ f"{name!r} is not a valid program name. A program name is a letter or digit followed by "
108
+ "letters, digits, '.', '_' or '-', at most 64 characters, and does not start with p_."
109
+ )
110
+ return name
111
+
112
+
113
+ class Program:
114
+ """A DSPy module, the LM it runs on, and the name of the program it is.
115
+
116
+ The name is the program's handle: traces from a ``cmpnd.Client`` run of this
117
+ program go to the program with this name in the caller's organization, which the
118
+ platform creates on first use. Programs in one organization that share a name
119
+ share a history.
120
+
121
+ Declaring a program makes no network call. The SDK asks the platform for the
122
+ program behind the name the first time a client sends one of its traces.
123
+
124
+ >>> qa = cmpnd.Program("capital-qa", module=dspy.Predict("question -> answer"), lm=dspy.LM("openai/gpt-5-mini"))
125
+ >>> with cmpnd.Client() as client:
126
+ ... client.run_program(qa, question="What is the capital of France?")
127
+ """
128
+
129
+ def __init__(self, name: str, *, module: Any, lm: Any) -> None:
130
+ import dspy
131
+ from dspy.clients import base_lm # pyright: ignore[reportMissingTypeStubs] # dspy re-exports BaseLM untyped
132
+
133
+ self.name = validate_program_name(name)
134
+ if not isinstance(module, dspy.Module):
135
+ raise TypeError(f"A Program needs a DSPy module, not {type(module).__name__}")
136
+ if not isinstance(lm, base_lm.BaseLM):
137
+ raise TypeError(f"A Program needs an LM such as dspy.LM(...), not {type(lm).__name__}")
138
+ self.module = module
139
+ self.lm = lm
140
+
141
+ def __call__(self, **inputs: Any) -> Any:
142
+ """Run the module on this program's LM, as plain DSPy.
143
+
144
+ A direct call records nothing under the program's name. Run the program
145
+ through ``cmpnd.Client.run_program`` to trace it under its name.
146
+ """
147
+ import dspy
148
+
149
+ with dspy.context(lm=self.lm):
150
+ return self.module(**inputs)
151
+
152
+ def __repr__(self) -> str:
153
+ return f"Program({self.name!r}, lm={getattr(self.lm, 'model', self.lm)!r})"
@@ -43,6 +43,43 @@ and run records use the client's own id and never the id given to
43
43
  `configure()`. A client with no id declares no program, so its runs go out
44
44
  without one. Project tags follow the same rule.
45
45
 
46
+ ## Programs declared by name
47
+
48
+ `cmpnd.Program(name, module=..., lm=...)` declares a program by name. It holds a
49
+ DSPy module and the `dspy.LM` it runs on. A client that runs a `Program` traces
50
+ it under its name:
51
+
52
+ ```python
53
+ qa = cmpnd.Program("capital-qa", module=dspy.Predict("question -> answer"), lm=dspy.LM("openai/gpt-5-mini"))
54
+
55
+ with cmpnd.Client() as sdk:
56
+ sdk.run_program(qa, question="What is the capital of France?")
57
+ better = sdk.run_optimization(gepa, qa, trainset=train, valset=val)
58
+ ```
59
+
60
+ - **A name is checked where it is declared.** It is an ASCII letter or digit,
61
+ then letters, digits, `.`, `_` or `-`, at most 64 characters, and it does not
62
+ start with `p_` in any case. A bad name raises `ValueError`. Declaring a
63
+ program makes no network call.
64
+ - **The name finds the program.** The exporter asks the platform for the
65
+ program with that name in the caller's organization, which the platform
66
+ creates on first use, and sends the program's id on each trace. Each process
67
+ asks once per name. Two processes that declare one name write to one program.
68
+ - **A `Program` runs on its own LM**, with no global `dspy.configure` needed.
69
+ Calling it directly runs the module on its LM as plain DSPy, and traces
70
+ nothing under its name.
71
+ - **An optimized program keeps its name.** `run_optimization` returns a
72
+ `Program` with the same name and LM and the optimized module. For a
73
+ `dspy.GEPA` optimizer, the client resolves the name before the run starts, so
74
+ the run record carries the program's id.
75
+ - **The name wins over `program_id=`.** A client configured with an id sends a
76
+ declared program's traces to the named program, and warns once.
77
+ - **A serve without program names**, such as self-host release 2026-001, gets
78
+ the traces without an id. The exporter warns once per name.
79
+
80
+ A plain DSPy module passed to `run_program` or `run_optimization` declares no
81
+ name, as before.
82
+
46
83
  ## Membership
47
84
 
48
85
  A client records a call only when the call runs through one of its methods.
@@ -771,7 +771,10 @@ the caller's id rides only the steps on the caller's own deployment.
771
771
  #### A declared program name resolves to an id in the exporter
772
772
 
773
773
  A trace can declare its program by name instead of by id, in `TraceBuilder.declared_name`.
774
- No public API sets it yet. The name never goes on the wire. The exporter resolves it with
774
+ A `cmpnd.Client` run of a `cmpnd.Program` sets it: the client puts the name in the DSPy
775
+ settings key `callback.DECLARED_PROGRAM_SETTING` with `dspy.context`, which reaches the
776
+ threads DSPy starts, and `CmpndCallback._stamp` copies it onto the trace and drops any id
777
+ the client was configured with. The name never goes on the wire. The exporter resolves it with
775
778
  `PUT /api/v1/programs/by-name/{name}` and writes the id into the trace start and full
776
779
  trace bodies, at the top level and in `started_fact`, before it compares or sends them.
777
780
  `cmpnd_ingest/program_names.py` keeps the answers, and
@@ -784,6 +787,9 @@ trace bodies, at the top level and in `started_fact`, before it compares or send
784
787
  does. The exit flush resolves the names still waiting, with no retries.
785
788
  - **An id already on the trace stands.** Inside a worker, a deployment's traces carry
786
789
  its id from the start, and no name is resolved.
790
+ - **A run record resolves on the caller's thread.** An optimization run's start is sent
791
+ synchronously, so `Client.run_optimization` calls `BatchExporter.resolve_program_name`
792
+ before a `dspy.GEPA` run starts. It shares the flush's answers.
787
793
  - **A refusal settles the name with no id.** Serve answers 404 or 405 when it has no
788
794
  program names, as self-host release 2026-001 does, and 409, 422 or 403 for a name it
789
795
  will not resolve. The traces go without an id, and the exporter warns once per name.
@@ -1435,7 +1435,21 @@ class BatchExporter:
1435
1435
  for trace_id in ended_traces:
1436
1436
  self._program_names.trace_ended(trace_id)
1437
1437
 
1438
- def _resolve_program_name(self, name: str, tally: retry.TransientTally) -> None:
1438
+ def resolve_program_name(self, name: str) -> str | None:
1439
+ """The program id for a declared name, resolved now on the caller's thread.
1440
+
1441
+ For a record that must carry its program id when it is sent synchronously,
1442
+ such as an optimization run's start. It shares the answers the flush keeps, so
1443
+ a name the flush resolved costs no request here, and the reverse. Returns None
1444
+ when serve refused the name or could not be reached, and the record goes
1445
+ without an id.
1446
+ """
1447
+ if not self._program_names.unsettled([name]):
1448
+ return self._program_names.program_id(name)
1449
+ self._resolve_program_name(name, retry.TransientTally(), self._sync_http_client())
1450
+ return self._program_names.program_id(name)
1451
+
1452
+ def _resolve_program_name(self, name: str, tally: retry.TransientTally, client: httpx.Client | None = None) -> None:
1439
1453
  """Ask serve for a name's program id, and settle the name unless serve was unreachable.
1440
1454
 
1441
1455
  The request has no body. A 429, a 5xx or an unreachable server is retried on
@@ -1443,7 +1457,7 @@ class BatchExporter:
1443
1457
  leaves the name unsettled for the next batch. A refusal settles the name with
1444
1458
  no id. Each name warns once.
1445
1459
  """
1446
- client = self._require_client()
1460
+ client = client if client is not None else self._require_client()
1447
1461
  path = "/api/v1/programs/by-name/" + urllib.parse.quote(name, safe="")
1448
1462
  try:
1449
1463
  response = self._send_retrying(
@@ -57,6 +57,11 @@ class ProgramNames:
57
57
  with self._lock:
58
58
  self._ids[name] = program_id
59
59
 
60
+ def program_id(self, name: str) -> str | None:
61
+ """The id serve gave for a settled name, or None for a refused or unsettled one."""
62
+ with self._lock:
63
+ return self._ids.get(name)
64
+
60
65
  def first_warning(self, name: str) -> bool:
61
66
  """True the first time this is asked for a name, so each name warns once."""
62
67
  with self._lock:
@@ -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.42.2"
8
+ version = "0.43.0"
9
9
  description = "DSPy observability and deployment SDK for cmpnd"
10
10
  readme = "README.md"
11
11
  requires-python = ">=3.11"