cmpnd 0.42.0__tar.gz → 0.42.2__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 (372) hide show
  1. {cmpnd-0.42.0 → cmpnd-0.42.2}/PKG-INFO +1 -1
  2. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/authored.py +1 -1
  3. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/models.py +4 -0
  4. {cmpnd-0.42.0 → cmpnd-0.42.2}/docs/sdk-instrumentation.md +27 -0
  5. {cmpnd-0.42.0 → cmpnd-0.42.2}/ingest/cmpnd_ingest/exporter_http.py +157 -12
  6. cmpnd-0.42.2/ingest/cmpnd_ingest/program_names.py +90 -0
  7. {cmpnd-0.42.0 → cmpnd-0.42.2}/ingest/cmpnd_ingest/protocols.py +7 -1
  8. {cmpnd-0.42.0 → cmpnd-0.42.2}/pyproject.toml +1 -1
  9. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_authored.py +2 -2
  10. cmpnd-0.42.2/tests/test_exporter_program_names.py +440 -0
  11. {cmpnd-0.42.0 → cmpnd-0.42.2}/uv.lock +1 -1
  12. {cmpnd-0.42.0 → cmpnd-0.42.2}/.gitignore +0 -0
  13. {cmpnd-0.42.0 → cmpnd-0.42.2}/AGENTS.md +0 -0
  14. {cmpnd-0.42.0 → cmpnd-0.42.2}/CLAUDE.md +0 -0
  15. {cmpnd-0.42.0 → cmpnd-0.42.2}/README.md +0 -0
  16. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/__init__.py +0 -0
  17. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/_assessments.py +0 -0
  18. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/_authored_import.py +0 -0
  19. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/_deployments.py +0 -0
  20. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/_exporter_pool.py +0 -0
  21. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/_gepa_patch.py +0 -0
  22. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/_http.py +0 -0
  23. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/_program_patch.py +0 -0
  24. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/_rlm_patch.py +0 -0
  25. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/_runtime_tools.py +0 -0
  26. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/_timespec.py +0 -0
  27. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/_traces.py +0 -0
  28. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/_valsets.py +0 -0
  29. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/callback.py +0 -0
  30. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/cli/__init__.py +0 -0
  31. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/cli/api_client.py +0 -0
  32. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/cli/commands/__init__.py +0 -0
  33. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/cli/commands/admin.py +0 -0
  34. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/cli/commands/assessments.py +0 -0
  35. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/cli/commands/auth.py +0 -0
  36. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/cli/commands/datasets.py +0 -0
  37. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/cli/commands/deployments.py +0 -0
  38. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/cli/commands/evals.py +0 -0
  39. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/cli/commands/login.py +0 -0
  40. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/cli/commands/optimizations.py +0 -0
  41. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/cli/commands/org.py +0 -0
  42. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/cli/commands/programs.py +0 -0
  43. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/cli/commands/provider_credentials.py +0 -0
  44. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/cli/commands/signatures.py +0 -0
  45. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/cli/commands/tool_servers.py +0 -0
  46. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/cli/commands/top.py +0 -0
  47. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/cli/commands/traces.py +0 -0
  48. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/cli/commands/valsets.py +0 -0
  49. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/cli/credentials.py +0 -0
  50. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/cli/deprecation.py +0 -0
  51. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/cli/pagination.py +0 -0
  52. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/cli/shell.py +0 -0
  53. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/cli/trace_render.py +0 -0
  54. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/cli/tree_help.py +0 -0
  55. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/cli/verbs.py +0 -0
  56. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/client.py +0 -0
  57. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/configuration.py +0 -0
  58. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/context.py +0 -0
  59. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/dataset_sync.py +0 -0
  60. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/datasets.py +0 -0
  61. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/decorators.py +0 -0
  62. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/deployment.py +0 -0
  63. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/deployment_metadata.py +0 -0
  64. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/deployment_runs.py +0 -0
  65. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/eval_handler.py +0 -0
  66. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/execution.py +0 -0
  67. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/exporter.py +0 -0
  68. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/exporter_http.py +0 -0
  69. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/helpers.py +0 -0
  70. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/identity.py +0 -0
  71. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/imaging.py +0 -0
  72. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/ir/__init__.py +0 -0
  73. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/ir/decode.py +0 -0
  74. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/ir/encode.py +0 -0
  75. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/ir/hash.py +0 -0
  76. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/ir/program.py +0 -0
  77. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/ir/reflection.py +0 -0
  78. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/ir/types.py +0 -0
  79. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/ir/xxh64.py +0 -0
  80. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/lm_configuration.py +0 -0
  81. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/mcp_servers.py +0 -0
  82. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/optimization.py +0 -0
  83. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/optimizers/__init__.py +0 -0
  84. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/optimizers/gepa.py +0 -0
  85. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/optimizers/gepa_callback.py +0 -0
  86. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/optimizers/resume.py +0 -0
  87. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/optimizers/tracker.py +0 -0
  88. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/packaging.py +0 -0
  89. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/programs.py +0 -0
  90. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/references.py +0 -0
  91. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/retry.py +0 -0
  92. {cmpnd-0.42.0 → cmpnd-0.42.2}/cmpnd/traces.py +0 -0
  93. {cmpnd-0.42.0 → cmpnd-0.42.2}/docs/2026-04-02-code-review.md +0 -0
  94. {cmpnd-0.42.0 → cmpnd-0.42.2}/docs/cli.md +0 -0
  95. {cmpnd-0.42.0 → cmpnd-0.42.2}/docs/client.md +0 -0
  96. {cmpnd-0.42.0 → cmpnd-0.42.2}/docs/custom-proposer.md +0 -0
  97. {cmpnd-0.42.0 → cmpnd-0.42.2}/docs/plans/2026-04-05-train-val-dataset-capture-design.md +0 -0
  98. {cmpnd-0.42.0 → cmpnd-0.42.2}/docs/plans/2026-04-05-train-val-dataset-capture.md +0 -0
  99. {cmpnd-0.42.0 → cmpnd-0.42.2}/docs/reading-traces.md +0 -0
  100. {cmpnd-0.42.0 → cmpnd-0.42.2}/examples/generalizing_proposer.py +0 -0
  101. {cmpnd-0.42.0 → cmpnd-0.42.2}/examples/local_ollama.py +0 -0
  102. {cmpnd-0.42.0 → cmpnd-0.42.2}/ingest/LINEAGE.md +0 -0
  103. {cmpnd-0.42.0 → cmpnd-0.42.2}/ingest/README.md +0 -0
  104. {cmpnd-0.42.0 → cmpnd-0.42.2}/ingest/cmpnd_ingest/__init__.py +0 -0
  105. {cmpnd-0.42.0 → cmpnd-0.42.2}/ingest/cmpnd_ingest/bodies.py +0 -0
  106. {cmpnd-0.42.0 → cmpnd-0.42.2}/ingest/cmpnd_ingest/py.typed +0 -0
  107. {cmpnd-0.42.0 → cmpnd-0.42.2}/ingest/cmpnd_ingest/request_log.py +0 -0
  108. {cmpnd-0.42.0 → cmpnd-0.42.2}/ingest/cmpnd_ingest/retry.py +0 -0
  109. {cmpnd-0.42.0 → cmpnd-0.42.2}/ingest/cmpnd_ingest/trace_facts.py +0 -0
  110. {cmpnd-0.42.0 → cmpnd-0.42.2}/ingest/pyproject.toml +0 -0
  111. {cmpnd-0.42.0 → cmpnd-0.42.2}/scripts/audit-sdist.sh +0 -0
  112. {cmpnd-0.42.0 → cmpnd-0.42.2}/scripts/capture_review_run.py +0 -0
  113. {cmpnd-0.42.0 → cmpnd-0.42.2}/scripts/check-pip-install.sh +0 -0
  114. {cmpnd-0.42.0 → cmpnd-0.42.2}/scripts/check_release_artifacts.py +0 -0
  115. {cmpnd-0.42.0 → cmpnd-0.42.2}/scripts/fixtures/review_run.json.gz +0 -0
  116. {cmpnd-0.42.0 → cmpnd-0.42.2}/scripts/gepa_resume_bug.py +0 -0
  117. {cmpnd-0.42.0 → cmpnd-0.42.2}/scripts/release-stress.sh +0 -0
  118. {cmpnd-0.42.0 → cmpnd-0.42.2}/scripts/require_version_bump.py +0 -0
  119. {cmpnd-0.42.0 → cmpnd-0.42.2}/scripts/seed_review_run.py +0 -0
  120. {cmpnd-0.42.0 → cmpnd-0.42.2}/scripts/test-in-linux.sh +0 -0
  121. {cmpnd-0.42.0 → cmpnd-0.42.2}/stubs/dspy/__init__.pyi +0 -0
  122. {cmpnd-0.42.0 → cmpnd-0.42.2}/stubs/dspy/primitives/__init__.pyi +0 -0
  123. {cmpnd-0.42.0 → cmpnd-0.42.2}/stubs/dspy/primitives/prediction.pyi +0 -0
  124. {cmpnd-0.42.0 → cmpnd-0.42.2}/stubs/dspy/utils/__init__.pyi +0 -0
  125. {cmpnd-0.42.0 → cmpnd-0.42.2}/stubs/dspy/utils/callback.pyi +0 -0
  126. {cmpnd-0.42.0 → cmpnd-0.42.2}/stubs/dspy/utils/dummies.pyi +0 -0
  127. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/__init__.py +0 -0
  128. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/_deployment_signature_helpers.py +0 -0
  129. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/_exporter_helpers.py +0 -0
  130. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/_fake_clock.py +0 -0
  131. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/_identity_helpers.py +0 -0
  132. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/_resolve_refs.py +0 -0
  133. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/_traced_run.py +0 -0
  134. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/conftest.py +0 -0
  135. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/e2e/__init__.py +0 -0
  136. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/e2e/committee_task.py +0 -0
  137. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/e2e/conftest.py +0 -0
  138. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/e2e/data/committee_train.ndjson.gz +0 -0
  139. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/e2e/data/committee_val.ndjson.gz +0 -0
  140. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/e2e/fake_lm.py +0 -0
  141. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/e2e/helpers.py +0 -0
  142. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/e2e/target_capabilities.py +0 -0
  143. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/e2e/test_assessments_cli.py +0 -0
  144. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/e2e/test_capture_prompts.py +0 -0
  145. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/e2e/test_composite_module.py +0 -0
  146. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/e2e/test_dataset_auto_creation.py +0 -0
  147. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/e2e/test_deploy_execute.py +0 -0
  148. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/e2e/test_deploy_pandas.py +0 -0
  149. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/e2e/test_deploy_programs.py +0 -0
  150. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/e2e/test_edge_cases.py +0 -0
  151. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/e2e/test_example_hash.py +0 -0
  152. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/e2e/test_gepa_optimization.py +0 -0
  153. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/e2e/test_interpreter_spans.py +0 -0
  154. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/e2e/test_lm_accuracy.py +0 -0
  155. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/e2e/test_module_tracing.py +0 -0
  156. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/e2e/test_optimization_lineage.py +0 -0
  157. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/e2e/test_optimization_metric_identity.py +0 -0
  158. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/e2e/test_optimize_failures.py +0 -0
  159. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/e2e/test_optimize_loop.py +0 -0
  160. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/e2e/test_optimize_review_committee.py +0 -0
  161. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/e2e/test_optimize_server_owned_completion.py +0 -0
  162. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/e2e/test_parallel_export.py +0 -0
  163. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/e2e/test_project_attribution.py +0 -0
  164. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/e2e/test_react_tool_spans.py +0 -0
  165. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/e2e/test_save_load.py +0 -0
  166. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/e2e/test_trace_structure.py +0 -0
  167. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/e2e/test_traces_read.py +0 -0
  168. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/e2e/test_user_attribution.py +0 -0
  169. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/e2e/test_valsets_cli.py +0 -0
  170. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/identity_vectors/01_predict_qa.json +0 -0
  171. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/identity_vectors/02_predict_renamed_field.json +0 -0
  172. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/identity_vectors/03_chain_of_thought_qa.json +0 -0
  173. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/identity_vectors/04_predict_richer_types.json +0 -0
  174. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/identity_vectors/05_composite_two_predicts.json +0 -0
  175. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/identity_vectors/06_multichaincomparison.json +0 -0
  176. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/identity_vectors/07_program_of_thought.json +0 -0
  177. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/identity_vectors/08_opaque_module.json +0 -0
  178. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/identity_vectors/09_predict_pydantic_field.json +0 -0
  179. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/identity_vectors/10_react.json +0 -0
  180. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/identity_vectors/11_refine.json +0 -0
  181. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/identity_vectors/12_retrieve.json +0 -0
  182. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/identity_vectors/13_knn.json +0 -0
  183. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/identity_vectors/14_parallel.json +0 -0
  184. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/identity_vectors/README.md +0 -0
  185. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/ingest_conformance.py +0 -0
  186. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/integration/__init__.py +0 -0
  187. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/integration/authored_optimize/judge.py +0 -0
  188. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/integration/authored_optimize/optimize_hosted.py +0 -0
  189. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/integration/conftest.py +0 -0
  190. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/integration/hosted_mcp_tool_optimize_probe.py +0 -0
  191. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/integration/hosted_optimize_probe.py +0 -0
  192. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/integration/optimization_run_probe.py +0 -0
  193. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/integration/test_cli_deploy_smoketest.py +0 -0
  194. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/integration/test_logs_renders_traces.py +0 -0
  195. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/integration/test_optimization_run.py +0 -0
  196. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/integration/test_optimizing_progress.py +0 -0
  197. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/integration/test_unhappy_path.py +0 -0
  198. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/load/__init__.py +0 -0
  199. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/load/__main__.py +0 -0
  200. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/load/drain.py +0 -0
  201. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/load/ingest.py +0 -0
  202. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/load/latency_transport.py +0 -0
  203. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/load/rlm_shape.py +0 -0
  204. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/load/rlm_size_report.py +0 -0
  205. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/load/sweep.py +0 -0
  206. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/load/test_rlm_shape.py +0 -0
  207. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/load/test_rlm_size_report.py +0 -0
  208. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/parity/__init__.py +0 -0
  209. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/parity/conftest.py +0 -0
  210. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/parity/seed_parity.py +0 -0
  211. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/parity/test_auth.py +0 -0
  212. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/parity/test_datasets.py +0 -0
  213. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/parity/test_deployments.py +0 -0
  214. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/parity/test_evals.py +0 -0
  215. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/parity/test_health.py +0 -0
  216. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/parity/test_modules.py +0 -0
  217. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/parity/test_optimizations.py +0 -0
  218. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/parity/test_programs.py +0 -0
  219. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/parity/test_signatures.py +0 -0
  220. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/parity/test_trace_streaming.py +0 -0
  221. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/parity/test_traces.py +0 -0
  222. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/span_reference_fixture.py +0 -0
  223. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/stress/__init__.py +0 -0
  224. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/stress/_provider.py +0 -0
  225. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/stress/_readiness.py +0 -0
  226. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/stress/_reliability.py +0 -0
  227. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/stress/_result.py +0 -0
  228. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/stress/mint_bedrock_token.py +0 -0
  229. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/stress/test_deploy_programs.py +0 -0
  230. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/stress/test_multi_client_stress.py +0 -0
  231. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/stress/test_optimize_client_committee.py +0 -0
  232. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/stress/test_optimize_loop.py +0 -0
  233. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/stress/test_optimize_loop_wasm.py +0 -0
  234. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/stress/test_optimize_stress.py +0 -0
  235. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/stress/test_provider_roles.py +0 -0
  236. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/stress/test_readiness.py +0 -0
  237. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/stress/test_real_provider_twins.py +0 -0
  238. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/stress/test_reliability.py +0 -0
  239. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/stress/test_reliability_classify.py +0 -0
  240. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/stress/test_rlm_wasm.py +0 -0
  241. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/stress/test_trace_search.py +0 -0
  242. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_adapter_reentrancy.py +0 -0
  243. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_assessments_api.py +0 -0
  244. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_authored_bundle_collisions.py +0 -0
  245. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_authored_metric_contract.py +0 -0
  246. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_authored_named_models.py +0 -0
  247. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_authored_optimize.py +0 -0
  248. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_callback.py +0 -0
  249. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_candidate_lineage.py +0 -0
  250. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_cli/__init__.py +0 -0
  251. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_cli/conftest.py +0 -0
  252. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_cli/test_admin.py +0 -0
  253. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_cli/test_api_client.py +0 -0
  254. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_cli/test_assessments.py +0 -0
  255. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_cli/test_auth.py +0 -0
  256. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_cli/test_credentials.py +0 -0
  257. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_cli/test_datasets.py +0 -0
  258. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_cli/test_deployment_runs.py +0 -0
  259. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_cli/test_deployments.py +0 -0
  260. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_cli/test_evals.py +0 -0
  261. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_cli/test_login.py +0 -0
  262. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_cli/test_main.py +0 -0
  263. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_cli/test_optimizations.py +0 -0
  264. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_cli/test_org.py +0 -0
  265. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_cli/test_pagination.py +0 -0
  266. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_cli/test_parity.py +0 -0
  267. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_cli/test_programs.py +0 -0
  268. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_cli/test_provider_credentials.py +0 -0
  269. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_cli/test_schema_sample.py +0 -0
  270. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_cli/test_shell.py +0 -0
  271. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_cli/test_signatures.py +0 -0
  272. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_cli/test_tool_servers.py +0 -0
  273. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_cli/test_top.py +0 -0
  274. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_cli/test_traces.py +0 -0
  275. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_cli/test_traces_search_wire.py +0 -0
  276. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_cli/test_tree_help.py +0 -0
  277. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_cli/test_valsets.py +0 -0
  278. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_client.py +0 -0
  279. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_clustering/__init__.py +0 -0
  280. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_clustering/conftest.py +0 -0
  281. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_clustering/test_edge_cases.py +0 -0
  282. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_clustering/test_hash_consistency.py +0 -0
  283. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_clustering/test_type_conversion.py +0 -0
  284. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_cmp551_grouping_identity.py +0 -0
  285. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_configuration.py +0 -0
  286. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_context.py +0 -0
  287. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_context_propagation.py +0 -0
  288. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_dataset_polling.py +0 -0
  289. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_dataset_sync.py +0 -0
  290. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_datasets.py +0 -0
  291. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_decorators.py +0 -0
  292. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_deploy.py +0 -0
  293. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_deploy_mcp_tool_references.py +0 -0
  294. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_deploy_program_id.py +0 -0
  295. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_deployment_metadata.py +0 -0
  296. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_deployment_runs.py +0 -0
  297. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_deployments_read.py +0 -0
  298. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_eval.py +0 -0
  299. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_eval_example_attribution.py +0 -0
  300. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_eval_start.py +0 -0
  301. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_eval_trace_grouping.py +0 -0
  302. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_exception_paths.py +0 -0
  303. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_execute.py +0 -0
  304. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_execute_run_identity.py +0 -0
  305. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_exporter.py +0 -0
  306. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_exporter_queue.py +0 -0
  307. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_exporter_real.py +0 -0
  308. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_fake_lm.py +0 -0
  309. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_gepa_callback.py +0 -0
  310. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_gepa_integration.py +0 -0
  311. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_gepa_output_normalization.py +0 -0
  312. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_gepa_resume.py +0 -0
  313. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_identity.py +0 -0
  314. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_imaging.py +0 -0
  315. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_import_weight.py +0 -0
  316. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_ingest_conformance.py +0 -0
  317. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_ingest_second_producer.py +0 -0
  318. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_integration.py +0 -0
  319. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_interpreter_callback.py +0 -0
  320. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_ir_encoder_properties.py +0 -0
  321. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_ir_golden_vectors.py +0 -0
  322. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_ir_roundtrip.py +0 -0
  323. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_lm15_native_calls.py +0 -0
  324. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_lm_configuration.py +0 -0
  325. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_lm_usage_shapes.py +0 -0
  326. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_mcp_tools.py +0 -0
  327. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_models.py +0 -0
  328. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_multi_client_stress_readiness.py +0 -0
  329. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_optimization_progress.py +0 -0
  330. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_optimization_start.py +0 -0
  331. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_optimize.py +0 -0
  332. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_optimize_budget.py +0 -0
  333. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_optimize_lift.py +0 -0
  334. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_optimize_lms.py +0 -0
  335. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_optimize_proposer.py +0 -0
  336. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_optimize_reflection_lm.py +0 -0
  337. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_optimize_terminal_failure.py +0 -0
  338. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_packaging.py +0 -0
  339. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_packaging_callable_source.py +0 -0
  340. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_packaging_mcp_tools.py +0 -0
  341. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_packaging_program_source.py +0 -0
  342. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_poll_until.py +0 -0
  343. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_program_identity.py +0 -0
  344. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_program_lineage_persist.py +0 -0
  345. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_programs_create.py +0 -0
  346. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_project_payload.py +0 -0
  347. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_real_provider_twins_readiness.py +0 -0
  348. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_reflector_properties.py +0 -0
  349. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_reflector_smoke.py +0 -0
  350. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_release_stress_script.py +0 -0
  351. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_repro_reactv2_bugs.py +0 -0
  352. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_require_version_bump.py +0 -0
  353. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_retry.py +0 -0
  354. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_reversible_failures.py +0 -0
  355. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_rlm_patch.py +0 -0
  356. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_run_attribution_paths.py +0 -0
  357. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_runtime_tools.py +0 -0
  358. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_schemas.py +0 -0
  359. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_seed_review_run.py +0 -0
  360. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_signature_inputs.py +0 -0
  361. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_span_references.py +0 -0
  362. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_span_sent_once.py +0 -0
  363. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_trace_context_concurrency.py +0 -0
  364. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_trace_fact_wire_contract.py +0 -0
  365. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_trace_facts.py +0 -0
  366. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_trace_render.py +0 -0
  367. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_traces_read.py +0 -0
  368. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_tracked_gepa.py +0 -0
  369. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_tracker_enrich.py +0 -0
  370. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_truncation.py +0 -0
  371. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_valsets_api.py +0 -0
  372. {cmpnd-0.42.0 → cmpnd-0.42.2}/tests/test_xxh64.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cmpnd
3
- Version: 0.42.0
3
+ Version: 0.42.2
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>
@@ -32,7 +32,7 @@ from typing import Any
32
32
  _MAX_BUNDLE_BYTES = 50 * 1024 * 1024
33
33
  _CHECK_TIMEOUT_SECONDS = 60.0
34
34
  _CHECK_TIMEOUT_ENV = "CMPND_AUTHORED_IMPORT_TIMEOUT_SECONDS"
35
- _WORKER_DSPY_BASELINE = (3, 3, 1)
35
+ _WORKER_DSPY_BASELINE = (3, 4, 1)
36
36
 
37
37
 
38
38
  class SourceCheckError(ValueError):
@@ -961,6 +961,10 @@ class TraceBuilder:
961
961
  # the configuration at construction like project_tags. None declares none,
962
962
  # and the key is then left off the wire rather than sent as null.
963
963
  program_id: str | None = field(default_factory=configuration._current_program_id)
964
+ # The program name the trace declares, or None. It never goes on the wire: the
965
+ # exporter resolves it to a program id and writes that into ``program_id`` on the
966
+ # bodies it sends, unless the trace already carries an id.
967
+ declared_name: str | None = None
964
968
 
965
969
  # Optimization lineage - tracks which optimization produced this program
966
970
  compiled_from_run_id: str | None = None
@@ -768,6 +768,33 @@ names its program, so the id is optional there. Later steps
768
768
  optimize a result deployment the platform created and whose program it allocated, so
769
769
  the caller's id rides only the steps on the caller's own deployment.
770
770
 
771
+ #### A declared program name resolves to an id in the exporter
772
+
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
775
+ `PUT /api/v1/programs/by-name/{name}` and writes the id into the trace start and full
776
+ trace bodies, at the top level and in `started_fact`, before it compares or sends them.
777
+ `cmpnd_ingest/program_names.py` keeps the answers, and
778
+ `BatchExporter._stamp_program_ids` makes the requests:
779
+
780
+ - **Once per name per process.** Each exporter keeps its answers for its own life, and
781
+ the pool keeps one exporter per endpoint and key, so each name resolves once for each.
782
+ A forked child keeps its parent's answers.
783
+ - **On the export thread.** The batch waits for the resolve, and the producer never
784
+ does. The exit flush resolves the names still waiting, with no retries.
785
+ - **An id already on the trace stands.** Inside a worker, a deployment's traces carry
786
+ its id from the start, and no name is resolved.
787
+ - **A refusal settles the name with no id.** Serve answers 404 or 405 when it has no
788
+ program names, as self-host release 2026-001 does, and 409, 422 or 403 for a name it
789
+ will not resolve. The traces go without an id, and the exporter warns once per name.
790
+ - **An outage leaves the name unsettled.** A 5xx, a 429 or an unreachable server is
791
+ retried with backoff, wider than the data sends' rule because the request is a
792
+ get-or-create that a repeat cannot double. The names in one batch share one retry
793
+ budget, so an outage delays a batch by one budget however many new names it holds.
794
+ When the budget is spent, the batch's traces go without an id, and later batches try
795
+ the names again. A trace keeps the decision its first body carried,
796
+ because serve refuses a trace whose started fact changes between sends.
797
+
771
798
  ### LM usage evidence
772
799
 
773
800
  Optimization LM spans carry `optimization.role` in their string attributes.
@@ -25,6 +25,7 @@ import os
25
25
  import queue
26
26
  import threading
27
27
  import time
28
+ import urllib.parse
28
29
  import weakref
29
30
  from collections.abc import Callable
30
31
  from concurrent.futures import ThreadPoolExecutor, as_completed
@@ -33,7 +34,7 @@ from typing import Any, cast
33
34
 
34
35
  import httpx
35
36
 
36
- from . import request_log, retry
37
+ from . import program_names, request_log, retry
37
38
  from .bodies import EncodedTraceBody, TraceBody
38
39
  from .protocols import (
39
40
  EvalRunRecord,
@@ -96,6 +97,16 @@ def _send_encoded(client: httpx.Client, method: str, path: str, body: EncodedTra
96
97
  _SHED_STATUS = 429
97
98
 
98
99
 
100
+ def _error_text(response: httpx.Response) -> str:
101
+ """The ``error`` field of a refusal, else its body, shortened for a log line."""
102
+ try:
103
+ error = response.json().get("error")
104
+ except (ValueError, AttributeError):
105
+ error = None
106
+ text = error if isinstance(error, str) and error else (response.text or "").strip()
107
+ return text[:200] or "no detail"
108
+
109
+
99
110
  class ExportRetriesExhausted(RuntimeError):
100
111
  """A data POST rode through transient answers until its budget ran out.
101
112
 
@@ -183,6 +194,22 @@ def _is_retryable_export_error(exc: BaseException) -> bool:
183
194
  return isinstance(exc, (httpx.TimeoutException, httpx.TransportError))
184
195
 
185
196
 
197
+ def _may_retry_a_name_resolve(exc: BaseException) -> bool:
198
+ """Whether a failed program-name resolve is worth asking again.
199
+
200
+ Wider than the data sends' rule, for two reasons. A failed resolve sends its batch
201
+ without program ids, and a trace whose start went without an id keeps none, so
202
+ giving up early costs more than waiting. And the request is a get-or-create that
203
+ returns one id per name however often it is repeated, so a repeat can never
204
+ allocate twice. So a 429 and a plain 500 are retried here too, with ``Retry-After``
205
+ honoured, while a 4xx refusal is final.
206
+ """
207
+ if isinstance(exc, httpx.HTTPStatusError):
208
+ status = exc.response.status_code
209
+ return status == _SHED_STATUS or status >= 500 or retry.is_retryable(exc.response)
210
+ return isinstance(exc, (httpx.TimeoutException, httpx.TransportError))
211
+
212
+
186
213
  def _may_resend_a_minting_post(exc: BaseException) -> bool:
187
214
  """The same question as above, asked of a POST that BOTH mints an id and
188
215
  creates the record under it — on this contract, only ``POST /api/v1/evals``.
@@ -225,6 +252,20 @@ class ExportItem:
225
252
  # documentation and to catch misuse at enqueue.
226
253
  data: TraceRecord | SpanRecord | EvalRunRecord | OptimizationRunRecord | EncodedTraceBody
227
254
  timestamp: float
255
+ # The program name a trace declared, which the flush resolves to an id. It rides the
256
+ # queue item because the body is serialized at enqueue and the name never goes on
257
+ # the wire.
258
+ declared_name: str | None = None
259
+
260
+
261
+ def _declared_name(trace: TraceRecord) -> str | None:
262
+ """The program name a trace record declares, or None.
263
+
264
+ Read with ``getattr`` because the member is optional: a ``Protocol`` cannot declare
265
+ an optional member, and a producer that names no programs should not need one.
266
+ """
267
+ name = getattr(trace, "declared_name", None)
268
+ return name if isinstance(name, str) and name else None
228
269
 
229
270
 
230
271
  # The only record the exporter will shed under load, and the reason overflow can
@@ -366,6 +407,10 @@ class BatchExporter:
366
407
  self._dropped_at_last_log = 0
367
408
  self._stats_lock = threading.Lock()
368
409
 
410
+ # The program ids serve gave for the names traces declare. Kept across a fork:
411
+ # see _reinit_after_fork.
412
+ self._program_names = program_names.ProgramNames()
413
+
369
414
  def start(self) -> None:
370
415
  """Start the background export thread."""
371
416
  # Before the liveness check, not after: in a forked child `_thread` is a
@@ -472,6 +517,8 @@ class BatchExporter:
472
517
 
473
518
  self._sync_client_lock = threading.Lock()
474
519
  self._stats_lock = threading.Lock()
520
+ # The resolved program names stay: the ids are still right in the child.
521
+ self._program_names.after_fork()
475
522
 
476
523
  self._flush_cond = threading.Condition()
477
524
  self._flush_requested_gen = 0
@@ -584,7 +631,7 @@ class BatchExporter:
584
631
 
585
632
  def export_trace(self, trace: TraceRecord) -> None:
586
633
  """Queue a trace for export (non-blocking)."""
587
- self._enqueue(ExportItem("trace", TraceBody(trace.to_api_dict()), time.time()))
634
+ self._enqueue(ExportItem("trace", TraceBody(trace.to_api_dict()), time.time(), _declared_name(trace)))
588
635
 
589
636
  def export_encoded_trace(self, trace: EncodedTraceBody) -> None:
590
637
  """Queue a compressed finished trace for export (non-blocking).
@@ -622,12 +669,12 @@ class BatchExporter:
622
669
  def export_trace_start(self, trace: TraceRecord) -> None:
623
670
  """Queue a trace start for export (non-blocking)."""
624
671
  body = {**trace.to_start_dict(), "trace_id": str(trace.trace_id)}
625
- self._enqueue(ExportItem("trace_start", TraceBody(body), time.time()))
672
+ self._enqueue(ExportItem("trace_start", TraceBody(body), time.time(), _declared_name(trace)))
626
673
 
627
674
  def export_trace_finalize(self, trace: TraceRecord) -> None:
628
675
  """Queue a trace finalization for export (non-blocking)."""
629
676
  body = {**trace.to_finalize_dict(), "trace_id": str(trace.trace_id)}
630
- self._enqueue(ExportItem("trace_finalize", TraceBody(body), time.time()))
677
+ self._enqueue(ExportItem("trace_finalize", TraceBody(body), time.time(), _declared_name(trace)))
631
678
 
632
679
  def export_optimization_start(self, optimization_run: OptimizationRunRecord) -> str:
633
680
  """Create the optimization record server-side and return the
@@ -1089,7 +1136,15 @@ class BatchExporter:
1089
1136
  def _send_json_retrying(self, client: httpx.Client, method: str, path: str, payload: Any) -> httpx.Response:
1090
1137
  return self._send_retrying(method, path, lambda: _send_json(client, method, path, payload))
1091
1138
 
1092
- def _send_retrying(self, method: str, path: str, send: Callable[[], httpx.Response]) -> httpx.Response:
1139
+ def _send_retrying(
1140
+ self,
1141
+ method: str,
1142
+ path: str,
1143
+ send: Callable[[], httpx.Response],
1144
+ *,
1145
+ retryable: Callable[[BaseException], bool] = _is_retryable_export_error,
1146
+ tally: retry.TransientTally | None = None,
1147
+ ) -> httpx.Response:
1093
1148
  """`_send_json` + `raise_for_status`, riding through transient blips.
1094
1149
 
1095
1150
  Retries a failure the retryable-error contract calls transient (see
@@ -1103,21 +1158,26 @@ class BatchExporter:
1103
1158
 
1104
1159
  An exhausted budget raises ExportRetriesExhausted naming the count and
1105
1160
  the window, so a run that could not be saved says what it endured.
1161
+
1162
+ ``retryable`` replaces the data sends' rule for a caller with a different one.
1163
+ ``tally`` lets several calls share one budget: a caller passes the same tally
1164
+ to each, and once it is spent every later call gives up at its first failure.
1106
1165
  """
1107
- tally = retry.TransientTally()
1166
+ tally = tally if tally is not None else retry.TransientTally()
1167
+ ridden_before = tally.count
1108
1168
  while True:
1109
1169
  try:
1110
1170
  response = send()
1111
1171
  response.raise_for_status()
1112
- if tally.count:
1113
- self._note_ridden_through(tally.count)
1172
+ if tally.count > ridden_before:
1173
+ self._note_ridden_through(tally.count - ridden_before)
1114
1174
  logger.warning("cmpnd: rode through %s to %s %s", tally.describe(time.monotonic()), method, path)
1115
1175
  return response
1116
1176
  except Exception as e:
1117
1177
  resp = e.response if isinstance(e, httpx.HTTPStatusError) else None
1118
1178
  if resp is not None:
1119
1179
  self._note_status(resp)
1120
- if not _is_retryable_export_error(e):
1180
+ if not retryable(e):
1121
1181
  raise
1122
1182
  tally.record(
1123
1183
  str(resp.status_code) if resp is not None else type(e).__name__,
@@ -1126,7 +1186,7 @@ class BatchExporter:
1126
1186
  )
1127
1187
  now = time.monotonic()
1128
1188
  if tally.exhausted(now) or not self._wait_before_retry(retry.delay_for(tally.count - 1, resp)):
1129
- self._note_ridden_through(tally.count)
1189
+ self._note_ridden_through(tally.count - ridden_before)
1130
1190
  raise ExportRetriesExhausted(f"{method} {path} gave up after {tally.describe(now)}: {e}") from e
1131
1191
 
1132
1192
  def _note_status(self, response: httpx.Response) -> None:
@@ -1184,10 +1244,21 @@ class BatchExporter:
1184
1244
  trace_starts: list[tuple[str, dict[str, Any]]] = []
1185
1245
  trace_finalizes: list[tuple[str, dict[str, Any]]] = []
1186
1246
  spans_by_trace: dict[str, list[dict[str, Any]]] = {}
1247
+ # Bodies that declare a program by name, as (name, trace id, body), so the
1248
+ # names can be resolved and the ids written before anything compares or sends
1249
+ # them. A finalize body restates no creation field and carries no id, but it
1250
+ # and a full trace body are a trace's last, so both end the trace's decision.
1251
+ named_bodies: list[tuple[str, str, dict[str, Any]]] = []
1252
+ ended_traces: list[str] = []
1187
1253
 
1188
1254
  for item in batch:
1189
1255
  if item.item_type == "trace":
1190
- traces_to_send.append(cast("TraceRecord", item.data).to_api_dict())
1256
+ body = cast("TraceRecord", item.data).to_api_dict()
1257
+ traces_to_send.append(body)
1258
+ if item.declared_name is not None:
1259
+ trace_id = str(body.get("trace_id"))
1260
+ named_bodies.append((item.declared_name, trace_id, body))
1261
+ ended_traces.append(trace_id)
1191
1262
  elif item.item_type == "encoded_trace":
1192
1263
  encoded_traces.append(cast("EncodedTraceBody", item.data))
1193
1264
  elif item.item_type == "eval_run":
@@ -1205,11 +1276,16 @@ class BatchExporter:
1205
1276
  elif item.item_type == "trace_start":
1206
1277
  trace = cast("TraceRecord", item.data)
1207
1278
  trace_id = str(trace.trace_id)
1208
- trace_starts.append((trace_id, trace.to_start_dict()))
1279
+ body = trace.to_start_dict()
1280
+ trace_starts.append((trace_id, body))
1281
+ if item.declared_name is not None:
1282
+ named_bodies.append((item.declared_name, trace_id, body))
1209
1283
  elif item.item_type == "trace_finalize":
1210
1284
  trace = cast("TraceRecord", item.data)
1211
1285
  trace_id = str(trace.trace_id)
1212
1286
  trace_finalizes.append((trace_id, trace.to_finalize_dict()))
1287
+ if item.declared_name is not None:
1288
+ ended_traces.append(trace_id)
1213
1289
  elif item.item_type == "span":
1214
1290
  span_dict = cast("SpanRecord", item.data).to_api_dict()
1215
1291
  trace_id = str(span_dict.get("trace_id") or "")
@@ -1218,6 +1294,9 @@ class BatchExporter:
1218
1294
  continue
1219
1295
  spans_by_trace.setdefault(trace_id, []).append(span_dict)
1220
1296
 
1297
+ if named_bodies or ended_traces:
1298
+ self._stamp_program_ids(named_bodies, ended_traces)
1299
+
1221
1300
  # Coalesce start + batch within one flush: if a trace's full
1222
1301
  # batch trace is in this same flush, sending it upserts the row to its
1223
1302
  # final status directly (serve traceUpsertSQL, ON CONFLICT on
@@ -1329,6 +1408,72 @@ class BatchExporter:
1329
1408
  errored,
1330
1409
  )
1331
1410
 
1411
+ # ------------------------------------------------------------------
1412
+ # Declared program names
1413
+ # ------------------------------------------------------------------
1414
+
1415
+ def _stamp_program_ids(self, named_bodies: list[tuple[str, str, dict[str, Any]]], ended_traces: list[str]) -> None:
1416
+ """Resolve the batch's declared names, then write their ids into its bodies.
1417
+
1418
+ Runs on the export thread, so the batch waits for the resolve and the
1419
+ producer never does. A body that already declares an id keeps it: inside a
1420
+ worker, a deployment's traces carry its id from the start.
1421
+
1422
+ The batch's names share one retry budget, so an outage delays the batch by one
1423
+ budget however many new names it holds. Once the budget is spent, the names not
1424
+ yet asked stay unsettled for the next batch.
1425
+ """
1426
+ waiting = [name for name, _, body in named_bodies if "program_id" not in body]
1427
+ tally = retry.TransientTally()
1428
+ for name in self._program_names.unsettled(waiting):
1429
+ if tally.exhausted(time.monotonic()):
1430
+ self._warn_program_name(name, "the batch's retry budget is spent; later traces will try again")
1431
+ continue
1432
+ self._resolve_program_name(name, tally)
1433
+ for name, trace_id, body in named_bodies:
1434
+ self._program_names.stamp(name, trace_id, body)
1435
+ for trace_id in ended_traces:
1436
+ self._program_names.trace_ended(trace_id)
1437
+
1438
+ def _resolve_program_name(self, name: str, tally: retry.TransientTally) -> None:
1439
+ """Ask serve for a name's program id, and settle the name unless serve was unreachable.
1440
+
1441
+ The request has no body. A 429, a 5xx or an unreachable server is retried on
1442
+ the batch's shared budget (see `_may_retry_a_name_resolve`), and a spent budget
1443
+ leaves the name unsettled for the next batch. A refusal settles the name with
1444
+ no id. Each name warns once.
1445
+ """
1446
+ client = self._require_client()
1447
+ path = "/api/v1/programs/by-name/" + urllib.parse.quote(name, safe="")
1448
+ try:
1449
+ response = self._send_retrying(
1450
+ "PUT", path, lambda: client.request("PUT", path), retryable=_may_retry_a_name_resolve, tally=tally
1451
+ )
1452
+ except httpx.HTTPStatusError as e:
1453
+ status = e.response.status_code
1454
+ self._program_names.settle(name, None)
1455
+ if status in (404, 405):
1456
+ self._warn_program_name(name, "the server has no program names")
1457
+ else:
1458
+ self._warn_program_name(name, f"the server refused it with {status}: {_error_text(e.response)}")
1459
+ return
1460
+ except Exception as e:
1461
+ self._warn_program_name(name, f"the server gave no answer ({e}); later traces will try again")
1462
+ return
1463
+ try:
1464
+ program_id = response.json().get("program_id")
1465
+ except (ValueError, AttributeError):
1466
+ program_id = None
1467
+ if not isinstance(program_id, str) or not program_id:
1468
+ self._program_names.settle(name, None)
1469
+ self._warn_program_name(name, "the server's answer had no program id")
1470
+ return
1471
+ self._program_names.settle(name, program_id)
1472
+
1473
+ def _warn_program_name(self, name: str, reason: str) -> None:
1474
+ if self._program_names.first_warning(name):
1475
+ logger.warning("cmpnd: traces of program %r are sent without a program id: %s.", name, reason)
1476
+
1332
1477
  # ------------------------------------------------------------------
1333
1478
  # Individual send methods (run in thread pool)
1334
1479
  # ------------------------------------------------------------------
@@ -0,0 +1,90 @@
1
+ """The program ids serve gave for the program names this process declared.
2
+
3
+ A trace can declare the name of its program instead of its id. The name never goes on
4
+ the wire: the transport asks serve for the name's program id with
5
+ ``PUT /api/v1/programs/by-name/{name}``, once per name per process, and writes the id
6
+ into each trace body before it sends it. This module keeps the answers. The transport
7
+ makes the requests, because they ride its client and its retry budget.
8
+
9
+ An answer is kept for the life of the process. A name that serve later points at
10
+ another program reaches a process only when the process starts again, and a renamed
11
+ program's old name keeps resolving to it.
12
+
13
+ A name settles in one of two ways. Serve gives an id, or serve refuses the name, as a
14
+ server without program names does with ``405``. A refused name sends its traces without
15
+ an id for the rest of the process. A name that could not be resolved because serve was
16
+ unreachable stays unsettled, and the transport tries it again for later traces.
17
+
18
+ A trace keeps the decision its first body carried. Serve refuses a trace whose started
19
+ fact changes between sends, so a trace whose start went without an id during an outage
20
+ sends every later body without one too, even after the name resolves.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import threading
26
+ from collections.abc import Iterable
27
+ from typing import Any
28
+
29
+
30
+ class ProgramNames:
31
+ """Resolved and refused names, and the id each unfinished trace was sent with."""
32
+
33
+ def __init__(self) -> None:
34
+ self._lock = threading.Lock()
35
+ # Settled names: the id serve gave, or None for a name serve refused.
36
+ self._ids: dict[str, str | None] = {}
37
+ # Traces whose first body went out while their name was unsettled, keyed by
38
+ # trace id. They keep sending without an id until they end.
39
+ self._sent_without_id: set[str] = set()
40
+ self._warned: set[str] = set()
41
+
42
+ def after_fork(self) -> None:
43
+ """Keep the answers in a forked child, with a lock the child can take.
44
+
45
+ The ids are still right in the child, so it does not ask again. A lock that
46
+ another thread held at the fork stays held in the child, so it is replaced.
47
+ """
48
+ self._lock = threading.Lock()
49
+
50
+ def unsettled(self, names: Iterable[str]) -> list[str]:
51
+ """The names among ``names`` that have no answer yet, each once, in order."""
52
+ with self._lock:
53
+ return list(dict.fromkeys(name for name in names if name not in self._ids))
54
+
55
+ def settle(self, name: str, program_id: str | None) -> None:
56
+ """Record serve's answer for a name: its id, or None when serve refused it."""
57
+ with self._lock:
58
+ self._ids[name] = program_id
59
+
60
+ def first_warning(self, name: str) -> bool:
61
+ """True the first time this is asked for a name, so each name warns once."""
62
+ with self._lock:
63
+ if name in self._warned:
64
+ return False
65
+ self._warned.add(name)
66
+ return True
67
+
68
+ def stamp(self, name: str, trace_id: str, body: dict[str, Any]) -> None:
69
+ """Write the name's id into a trace start or full trace body.
70
+
71
+ The id goes at the top level and in the started fact, where the builder puts
72
+ an id it was given. A body that already declares an id keeps it.
73
+ """
74
+ if "program_id" in body:
75
+ return
76
+ with self._lock:
77
+ program_id = None if trace_id in self._sent_without_id else self._ids.get(name)
78
+ if program_id is None and name not in self._ids:
79
+ self._sent_without_id.add(trace_id)
80
+ if program_id is None:
81
+ return
82
+ body["program_id"] = program_id
83
+ started = body.get("started_fact")
84
+ if isinstance(started, dict):
85
+ started["program_id"] = program_id
86
+
87
+ def trace_ended(self, trace_id: str) -> None:
88
+ """Forget a trace's decision once its last body is built."""
89
+ with self._lock:
90
+ self._sent_without_id.discard(trace_id)
@@ -49,7 +49,13 @@ class SpanRecord(Protocol):
49
49
 
50
50
 
51
51
  class TraceRecord(Protocol):
52
- """A trace, in each of the three shapes the contract's routes take."""
52
+ """A trace, in each of the three shapes the contract's routes take.
53
+
54
+ A trace may also have a ``declared_name`` attribute: the name of its program, which
55
+ the transport resolves to a program id before it sends the trace (see
56
+ `program_names`). The member is optional, so it is not declared here, and the
57
+ transport reads it with ``getattr``.
58
+ """
53
59
 
54
60
  @property
55
61
  def trace_id(self) -> Any: ...
@@ -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.0"
8
+ version = "0.42.2"
9
9
  description = "DSPy observability and deployment SDK for cmpnd"
10
10
  readme = "README.md"
11
11
  requires-python = ">=3.11"
@@ -100,11 +100,11 @@ def test_newer_local_dspy_warns_about_the_worker_compatibility_boundary(monkeypa
100
100
  monkeypatch.setattr(
101
101
  authored,
102
102
  "_dependency_versions",
103
- lambda: {"python": "3.14.0", "dspy": "3.4.0", "cmpnd": "0.26.0"},
103
+ lambda: {"python": "3.14.0", "dspy": "3.5.0", "cmpnd": "0.26.0"},
104
104
  )
105
105
  monkeypatch.setattr(authored, "_check_bundle", lambda *_args, **_kwargs: authored.ProgramFacts.minimal())
106
106
 
107
- with pytest.warns(RuntimeWarning, match="newer than the worker's 3.3.1 compatibility baseline"):
107
+ with pytest.warns(RuntimeWarning, match="newer than the worker's 3.4.1 compatibility baseline"):
108
108
  authored.prepare_stdin(b"program = object()\n")
109
109
 
110
110