cmpnd 0.27.3__tar.gz → 0.27.4__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 (337) hide show
  1. {cmpnd-0.27.3 → cmpnd-0.27.4}/PKG-INFO +1 -1
  2. cmpnd-0.27.4/cmpnd/_runtime_tools.py +39 -0
  3. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/ir/reflection.py +12 -3
  4. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/packaging.py +46 -16
  5. {cmpnd-0.27.3 → cmpnd-0.27.4}/docs/cli.md +8 -6
  6. {cmpnd-0.27.3 → cmpnd-0.27.4}/docs/sdk-instrumentation.md +15 -1
  7. {cmpnd-0.27.3 → cmpnd-0.27.4}/pyproject.toml +1 -1
  8. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_exporter.py +5 -3
  9. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_packaging_mcp_tools.py +103 -1
  10. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_reflector_properties.py +55 -0
  11. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_reflector_smoke.py +14 -1
  12. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_retry.py +1 -0
  13. cmpnd-0.27.4/tests/test_runtime_tools.py +47 -0
  14. {cmpnd-0.27.3 → cmpnd-0.27.4}/uv.lock +1 -1
  15. {cmpnd-0.27.3 → cmpnd-0.27.4}/.gitignore +0 -0
  16. {cmpnd-0.27.3 → cmpnd-0.27.4}/AGENTS.md +0 -0
  17. {cmpnd-0.27.3 → cmpnd-0.27.4}/CLAUDE.md +0 -0
  18. {cmpnd-0.27.3 → cmpnd-0.27.4}/README.md +0 -0
  19. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/__init__.py +0 -0
  20. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/_assessments.py +0 -0
  21. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/_authored_import.py +0 -0
  22. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/_exporter_pool.py +0 -0
  23. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/_gepa_patch.py +0 -0
  24. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/_http.py +0 -0
  25. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/_program_patch.py +0 -0
  26. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/_rlm_patch.py +0 -0
  27. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/_timespec.py +0 -0
  28. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/_traces.py +0 -0
  29. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/_valsets.py +0 -0
  30. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/authored.py +0 -0
  31. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/callback.py +0 -0
  32. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/cli/__init__.py +0 -0
  33. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/cli/api_client.py +0 -0
  34. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/cli/commands/__init__.py +0 -0
  35. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/cli/commands/admin.py +0 -0
  36. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/cli/commands/assessments.py +0 -0
  37. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/cli/commands/auth.py +0 -0
  38. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/cli/commands/datasets.py +0 -0
  39. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/cli/commands/deployments.py +0 -0
  40. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/cli/commands/evals.py +0 -0
  41. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/cli/commands/login.py +0 -0
  42. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/cli/commands/optimizations.py +0 -0
  43. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/cli/commands/org.py +0 -0
  44. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/cli/commands/programs.py +0 -0
  45. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/cli/commands/provider_credentials.py +0 -0
  46. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/cli/commands/signatures.py +0 -0
  47. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/cli/commands/tool_servers.py +0 -0
  48. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/cli/commands/top.py +0 -0
  49. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/cli/commands/traces.py +0 -0
  50. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/cli/commands/valsets.py +0 -0
  51. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/cli/credentials.py +0 -0
  52. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/cli/deprecation.py +0 -0
  53. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/cli/pagination.py +0 -0
  54. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/cli/shell.py +0 -0
  55. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/cli/trace_render.py +0 -0
  56. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/cli/tree_help.py +0 -0
  57. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/cli/verbs.py +0 -0
  58. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/client.py +0 -0
  59. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/configuration.py +0 -0
  60. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/context.py +0 -0
  61. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/dataset_sync.py +0 -0
  62. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/datasets.py +0 -0
  63. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/decorators.py +0 -0
  64. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/deployment.py +0 -0
  65. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/eval_handler.py +0 -0
  66. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/execution.py +0 -0
  67. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/exporter.py +0 -0
  68. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/exporter_http.py +0 -0
  69. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/helpers.py +0 -0
  70. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/identity.py +0 -0
  71. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/imaging.py +0 -0
  72. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/ir/__init__.py +0 -0
  73. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/ir/decode.py +0 -0
  74. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/ir/encode.py +0 -0
  75. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/ir/hash.py +0 -0
  76. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/ir/program.py +0 -0
  77. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/ir/types.py +0 -0
  78. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/ir/xxh64.py +0 -0
  79. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/lm_configuration.py +0 -0
  80. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/mcp_servers.py +0 -0
  81. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/models.py +0 -0
  82. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/optimization.py +0 -0
  83. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/optimizers/__init__.py +0 -0
  84. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/optimizers/gepa.py +0 -0
  85. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/optimizers/gepa_callback.py +0 -0
  86. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/optimizers/resume.py +0 -0
  87. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/optimizers/tracker.py +0 -0
  88. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/retry.py +0 -0
  89. {cmpnd-0.27.3 → cmpnd-0.27.4}/cmpnd/traces.py +0 -0
  90. {cmpnd-0.27.3 → cmpnd-0.27.4}/docs/2026-04-02-code-review.md +0 -0
  91. {cmpnd-0.27.3 → cmpnd-0.27.4}/docs/client.md +0 -0
  92. {cmpnd-0.27.3 → cmpnd-0.27.4}/docs/custom-proposer.md +0 -0
  93. {cmpnd-0.27.3 → cmpnd-0.27.4}/docs/plans/2026-04-05-train-val-dataset-capture-design.md +0 -0
  94. {cmpnd-0.27.3 → cmpnd-0.27.4}/docs/plans/2026-04-05-train-val-dataset-capture.md +0 -0
  95. {cmpnd-0.27.3 → cmpnd-0.27.4}/docs/reading-traces.md +0 -0
  96. {cmpnd-0.27.3 → cmpnd-0.27.4}/examples/generalizing_proposer.py +0 -0
  97. {cmpnd-0.27.3 → cmpnd-0.27.4}/examples/local_ollama.py +0 -0
  98. {cmpnd-0.27.3 → cmpnd-0.27.4}/ingest/LINEAGE.md +0 -0
  99. {cmpnd-0.27.3 → cmpnd-0.27.4}/ingest/README.md +0 -0
  100. {cmpnd-0.27.3 → cmpnd-0.27.4}/ingest/cmpnd_ingest/__init__.py +0 -0
  101. {cmpnd-0.27.3 → cmpnd-0.27.4}/ingest/cmpnd_ingest/bodies.py +0 -0
  102. {cmpnd-0.27.3 → cmpnd-0.27.4}/ingest/cmpnd_ingest/exporter_http.py +0 -0
  103. {cmpnd-0.27.3 → cmpnd-0.27.4}/ingest/cmpnd_ingest/protocols.py +0 -0
  104. {cmpnd-0.27.3 → cmpnd-0.27.4}/ingest/cmpnd_ingest/py.typed +0 -0
  105. {cmpnd-0.27.3 → cmpnd-0.27.4}/ingest/cmpnd_ingest/request_log.py +0 -0
  106. {cmpnd-0.27.3 → cmpnd-0.27.4}/ingest/cmpnd_ingest/retry.py +0 -0
  107. {cmpnd-0.27.3 → cmpnd-0.27.4}/ingest/pyproject.toml +0 -0
  108. {cmpnd-0.27.3 → cmpnd-0.27.4}/scripts/audit-sdist.sh +0 -0
  109. {cmpnd-0.27.3 → cmpnd-0.27.4}/scripts/capture_review_run.py +0 -0
  110. {cmpnd-0.27.3 → cmpnd-0.27.4}/scripts/check_release_artifacts.py +0 -0
  111. {cmpnd-0.27.3 → cmpnd-0.27.4}/scripts/fixtures/review_run.json.gz +0 -0
  112. {cmpnd-0.27.3 → cmpnd-0.27.4}/scripts/gepa_resume_bug.py +0 -0
  113. {cmpnd-0.27.3 → cmpnd-0.27.4}/scripts/release-stress.sh +0 -0
  114. {cmpnd-0.27.3 → cmpnd-0.27.4}/scripts/require_version_bump.py +0 -0
  115. {cmpnd-0.27.3 → cmpnd-0.27.4}/scripts/seed_review_run.py +0 -0
  116. {cmpnd-0.27.3 → cmpnd-0.27.4}/scripts/test-in-linux.sh +0 -0
  117. {cmpnd-0.27.3 → cmpnd-0.27.4}/stubs/dspy/__init__.pyi +0 -0
  118. {cmpnd-0.27.3 → cmpnd-0.27.4}/stubs/dspy/primitives/__init__.pyi +0 -0
  119. {cmpnd-0.27.3 → cmpnd-0.27.4}/stubs/dspy/primitives/prediction.pyi +0 -0
  120. {cmpnd-0.27.3 → cmpnd-0.27.4}/stubs/dspy/utils/__init__.pyi +0 -0
  121. {cmpnd-0.27.3 → cmpnd-0.27.4}/stubs/dspy/utils/callback.pyi +0 -0
  122. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/__init__.py +0 -0
  123. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/_exporter_helpers.py +0 -0
  124. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/_fake_clock.py +0 -0
  125. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/_identity_helpers.py +0 -0
  126. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/conftest.py +0 -0
  127. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/e2e/__init__.py +0 -0
  128. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/e2e/committee_task.py +0 -0
  129. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/e2e/conftest.py +0 -0
  130. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/e2e/data/committee_train.ndjson.gz +0 -0
  131. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/e2e/data/committee_val.ndjson.gz +0 -0
  132. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/e2e/fake_lm.py +0 -0
  133. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/e2e/helpers.py +0 -0
  134. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/e2e/target_capabilities.py +0 -0
  135. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/e2e/test_assessments_cli.py +0 -0
  136. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/e2e/test_capture_prompts.py +0 -0
  137. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/e2e/test_composite_module.py +0 -0
  138. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/e2e/test_dataset_auto_creation.py +0 -0
  139. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/e2e/test_deploy_execute.py +0 -0
  140. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/e2e/test_deploy_programs.py +0 -0
  141. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/e2e/test_edge_cases.py +0 -0
  142. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/e2e/test_example_hash.py +0 -0
  143. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/e2e/test_gepa_optimization.py +0 -0
  144. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/e2e/test_interpreter_spans.py +0 -0
  145. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/e2e/test_lm_accuracy.py +0 -0
  146. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/e2e/test_module_tracing.py +0 -0
  147. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/e2e/test_optimization_lineage.py +0 -0
  148. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/e2e/test_optimization_metric_identity.py +0 -0
  149. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/e2e/test_optimize_failures.py +0 -0
  150. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/e2e/test_optimize_loop.py +0 -0
  151. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/e2e/test_optimize_review_committee.py +0 -0
  152. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/e2e/test_optimize_server_owned_completion.py +0 -0
  153. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/e2e/test_parallel_export.py +0 -0
  154. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/e2e/test_project_attribution.py +0 -0
  155. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/e2e/test_react_tool_spans.py +0 -0
  156. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/e2e/test_save_load.py +0 -0
  157. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/e2e/test_trace_structure.py +0 -0
  158. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/e2e/test_traces_read.py +0 -0
  159. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/e2e/test_user_attribution.py +0 -0
  160. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/e2e/test_valsets_cli.py +0 -0
  161. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/identity_vectors/01_predict_qa.json +0 -0
  162. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/identity_vectors/02_predict_renamed_field.json +0 -0
  163. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/identity_vectors/03_chain_of_thought_qa.json +0 -0
  164. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/identity_vectors/04_predict_richer_types.json +0 -0
  165. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/identity_vectors/05_composite_two_predicts.json +0 -0
  166. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/identity_vectors/06_multichaincomparison.json +0 -0
  167. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/identity_vectors/07_program_of_thought.json +0 -0
  168. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/identity_vectors/08_opaque_module.json +0 -0
  169. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/identity_vectors/09_predict_pydantic_field.json +0 -0
  170. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/identity_vectors/10_react.json +0 -0
  171. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/identity_vectors/11_refine.json +0 -0
  172. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/identity_vectors/12_retrieve.json +0 -0
  173. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/identity_vectors/13_knn.json +0 -0
  174. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/identity_vectors/14_parallel.json +0 -0
  175. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/identity_vectors/README.md +0 -0
  176. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/ingest_conformance.py +0 -0
  177. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/integration/__init__.py +0 -0
  178. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/integration/authored_optimize/judge.py +0 -0
  179. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/integration/authored_optimize/optimize_hosted.py +0 -0
  180. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/integration/conftest.py +0 -0
  181. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/integration/hosted_mcp_tool_optimize_probe.py +0 -0
  182. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/integration/hosted_optimize_probe.py +0 -0
  183. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/integration/optimization_run_probe.py +0 -0
  184. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/integration/test_cli_deploy_smoketest.py +0 -0
  185. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/integration/test_logs_renders_traces.py +0 -0
  186. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/integration/test_optimization_run.py +0 -0
  187. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/integration/test_optimizing_progress.py +0 -0
  188. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/integration/test_unhappy_path.py +0 -0
  189. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/load/__init__.py +0 -0
  190. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/load/__main__.py +0 -0
  191. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/load/drain.py +0 -0
  192. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/load/ingest.py +0 -0
  193. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/load/latency_transport.py +0 -0
  194. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/load/rlm_shape.py +0 -0
  195. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/load/sweep.py +0 -0
  196. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/load/test_rlm_shape.py +0 -0
  197. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/parity/__init__.py +0 -0
  198. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/parity/conftest.py +0 -0
  199. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/parity/seed_parity.py +0 -0
  200. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/parity/test_auth.py +0 -0
  201. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/parity/test_datasets.py +0 -0
  202. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/parity/test_deployments.py +0 -0
  203. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/parity/test_evals.py +0 -0
  204. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/parity/test_health.py +0 -0
  205. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/parity/test_modules.py +0 -0
  206. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/parity/test_optimizations.py +0 -0
  207. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/parity/test_programs.py +0 -0
  208. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/parity/test_signatures.py +0 -0
  209. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/parity/test_trace_streaming.py +0 -0
  210. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/parity/test_traces.py +0 -0
  211. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/stress/__init__.py +0 -0
  212. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/stress/_provider.py +0 -0
  213. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/stress/_readiness.py +0 -0
  214. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/stress/_reliability.py +0 -0
  215. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/stress/_result.py +0 -0
  216. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/stress/mint_bedrock_token.py +0 -0
  217. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/stress/test_deploy_programs.py +0 -0
  218. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/stress/test_multi_client_stress.py +0 -0
  219. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/stress/test_optimize_client_committee.py +0 -0
  220. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/stress/test_optimize_loop.py +0 -0
  221. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/stress/test_optimize_loop_wasm.py +0 -0
  222. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/stress/test_optimize_stress.py +0 -0
  223. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/stress/test_provider_roles.py +0 -0
  224. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/stress/test_readiness.py +0 -0
  225. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/stress/test_real_provider_twins.py +0 -0
  226. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/stress/test_reliability.py +0 -0
  227. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/stress/test_reliability_classify.py +0 -0
  228. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/stress/test_rlm_wasm.py +0 -0
  229. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/stress/test_trace_search.py +0 -0
  230. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_assessments_api.py +0 -0
  231. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_authored.py +0 -0
  232. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_authored_bundle_collisions.py +0 -0
  233. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_authored_metric_contract.py +0 -0
  234. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_authored_named_models.py +0 -0
  235. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_authored_optimize.py +0 -0
  236. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_callback.py +0 -0
  237. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_candidate_lineage.py +0 -0
  238. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_cli/__init__.py +0 -0
  239. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_cli/conftest.py +0 -0
  240. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_cli/test_admin.py +0 -0
  241. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_cli/test_api_client.py +0 -0
  242. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_cli/test_assessments.py +0 -0
  243. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_cli/test_auth.py +0 -0
  244. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_cli/test_credentials.py +0 -0
  245. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_cli/test_datasets.py +0 -0
  246. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_cli/test_deployments.py +0 -0
  247. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_cli/test_evals.py +0 -0
  248. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_cli/test_login.py +0 -0
  249. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_cli/test_main.py +0 -0
  250. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_cli/test_optimizations.py +0 -0
  251. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_cli/test_org.py +0 -0
  252. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_cli/test_pagination.py +0 -0
  253. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_cli/test_parity.py +0 -0
  254. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_cli/test_programs.py +0 -0
  255. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_cli/test_provider_credentials.py +0 -0
  256. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_cli/test_schema_sample.py +0 -0
  257. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_cli/test_shell.py +0 -0
  258. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_cli/test_signatures.py +0 -0
  259. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_cli/test_tool_servers.py +0 -0
  260. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_cli/test_top.py +0 -0
  261. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_cli/test_traces.py +0 -0
  262. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_cli/test_traces_search_wire.py +0 -0
  263. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_cli/test_tree_help.py +0 -0
  264. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_cli/test_valsets.py +0 -0
  265. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_client.py +0 -0
  266. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_clustering/__init__.py +0 -0
  267. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_clustering/conftest.py +0 -0
  268. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_clustering/test_edge_cases.py +0 -0
  269. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_clustering/test_hash_consistency.py +0 -0
  270. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_clustering/test_type_conversion.py +0 -0
  271. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_cmp551_grouping_identity.py +0 -0
  272. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_configuration.py +0 -0
  273. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_context.py +0 -0
  274. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_context_propagation.py +0 -0
  275. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_dataset_polling.py +0 -0
  276. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_dataset_sync.py +0 -0
  277. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_datasets.py +0 -0
  278. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_decorators.py +0 -0
  279. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_deploy.py +0 -0
  280. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_deploy_mcp_tool_references.py +0 -0
  281. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_eval.py +0 -0
  282. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_eval_example_attribution.py +0 -0
  283. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_eval_start.py +0 -0
  284. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_eval_trace_grouping.py +0 -0
  285. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_exception_paths.py +0 -0
  286. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_execute.py +0 -0
  287. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_exporter_queue.py +0 -0
  288. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_exporter_real.py +0 -0
  289. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_fake_lm.py +0 -0
  290. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_gepa_callback.py +0 -0
  291. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_gepa_integration.py +0 -0
  292. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_gepa_resume.py +0 -0
  293. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_identity.py +0 -0
  294. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_imaging.py +0 -0
  295. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_import_weight.py +0 -0
  296. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_ingest_conformance.py +0 -0
  297. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_ingest_second_producer.py +0 -0
  298. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_integration.py +0 -0
  299. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_interpreter_callback.py +0 -0
  300. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_ir_encoder_properties.py +0 -0
  301. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_ir_golden_vectors.py +0 -0
  302. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_ir_roundtrip.py +0 -0
  303. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_lm_configuration.py +0 -0
  304. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_lm_usage_shapes.py +0 -0
  305. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_mcp_tools.py +0 -0
  306. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_models.py +0 -0
  307. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_optimization_progress.py +0 -0
  308. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_optimization_start.py +0 -0
  309. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_optimize.py +0 -0
  310. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_optimize_budget.py +0 -0
  311. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_optimize_lift.py +0 -0
  312. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_optimize_lms.py +0 -0
  313. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_optimize_proposer.py +0 -0
  314. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_optimize_reflection_lm.py +0 -0
  315. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_optimize_terminal_failure.py +0 -0
  316. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_packaging.py +0 -0
  317. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_packaging_callable_source.py +0 -0
  318. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_packaging_program_source.py +0 -0
  319. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_poll_until.py +0 -0
  320. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_program_lineage_persist.py +0 -0
  321. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_project_payload.py +0 -0
  322. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_release_stress_script.py +0 -0
  323. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_repro_reactv2_bugs.py +0 -0
  324. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_require_version_bump.py +0 -0
  325. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_reversible_failures.py +0 -0
  326. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_rlm_patch.py +0 -0
  327. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_schemas.py +0 -0
  328. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_seed_review_run.py +0 -0
  329. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_span_sent_once.py +0 -0
  330. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_trace_render.py +0 -0
  331. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_traces_read.py +0 -0
  332. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_tracked_gepa.py +0 -0
  333. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_tracker_enrich.py +0 -0
  334. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_truncation.py +0 -0
  335. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_valsets_api.py +0 -0
  336. {cmpnd-0.27.3 → cmpnd-0.27.4}/tests/test_xxh64.py +0 -0
  337. {cmpnd-0.27.3 → cmpnd-0.27.4}/todo.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cmpnd
3
- Version: 0.27.3
3
+ Version: 0.27.4
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>
@@ -0,0 +1,39 @@
1
+ """Private provenance for tools that CMPND adds while a program runs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, TypeVar
6
+
7
+ _RUNTIME_TOOL_MARKER = "_cmpnd_runtime_tool"
8
+
9
+
10
+ class _RuntimeToolProvenance:
11
+ """An in-process token that keeps its identity through DSPy module copies."""
12
+
13
+ def __copy__(self) -> _RuntimeToolProvenance:
14
+ return self
15
+
16
+ def __deepcopy__(self, memo: dict[int, Any]) -> _RuntimeToolProvenance:
17
+ del memo
18
+ return self
19
+
20
+
21
+ _RUNTIME_TOOL_PROVENANCE = _RuntimeToolProvenance()
22
+ _T = TypeVar("_T")
23
+
24
+
25
+ def mark_runtime_tool(tool: _T) -> _T:
26
+ """Mark a platform-created tool without trusting a caller-chosen boolean."""
27
+ setattr(tool, _RUNTIME_TOOL_MARKER, _RUNTIME_TOOL_PROVENANCE)
28
+ function = getattr(tool, "func", None)
29
+ if function is not None:
30
+ setattr(function, _RUNTIME_TOOL_MARKER, _RUNTIME_TOOL_PROVENANCE)
31
+ return tool
32
+
33
+
34
+ def is_runtime_tool(tool: object) -> bool:
35
+ """Return whether this process marked the tool or its wrapped callable."""
36
+ if getattr(tool, _RUNTIME_TOOL_MARKER, None) is _RUNTIME_TOOL_PROVENANCE:
37
+ return True
38
+ function = getattr(tool, "func", None)
39
+ return getattr(function, _RUNTIME_TOOL_MARKER, None) is _RUNTIME_TOOL_PROVENANCE
@@ -14,6 +14,7 @@ from typing import Any
14
14
 
15
15
  import dspy
16
16
 
17
+ from .._runtime_tools import is_runtime_tool
17
18
  from . import program, types
18
19
 
19
20
  _PRIM_BY_PYTYPE: dict[Any, types.PrimKind] = {
@@ -123,9 +124,17 @@ def reflect(m: Any) -> program.Module:
123
124
  react_cls = _opt("ReAct")
124
125
  if react_cls is not None and isinstance(m, react_cls):
125
126
  tools = getattr(m, "tools", None) or {}
126
- # Exclude DSPy's auto-injected `finish` sentinel — identity is the
127
- # user-supplied tool set, not the runtime control-flow primitive.
128
- names = tuple(sorted(n for n in tools if n != "finish"))
127
+ # Exclude runtime control tools. DSPy supplies `finish`; a hosted worker may
128
+ # add marked platform tools that are absent from the authored program. Check
129
+ # the explicit marker rather than a name, so a user's similarly named tool
130
+ # remains part of the program's identity.
131
+ names = tuple(
132
+ sorted(
133
+ name
134
+ for name, tool in tools.items()
135
+ if name != "finish" and not (name.startswith("cmpnd_") and is_runtime_tool(tool))
136
+ )
137
+ )
129
138
  return program.ReAct(signature=project_sig(m.signature), tools=names)
130
139
 
131
140
  bestofn_cls = _opt("BestOfN")
@@ -30,6 +30,8 @@ from typing import TYPE_CHECKING, Any
30
30
 
31
31
  import typing_extensions
32
32
 
33
+ from ._runtime_tools import is_runtime_tool
34
+
33
35
  if TYPE_CHECKING:
34
36
  import dspy
35
37
 
@@ -802,6 +804,12 @@ def declared_module_tools(module: dspy.Module) -> dict[str, typing.Any]:
802
804
  continue
803
805
  pending.extend(vars(member).values())
804
806
  for name, tool in module_tools(member).items():
807
+ if name.startswith("cmpnd_") and is_runtime_tool(tool):
808
+ continue
809
+ if name.startswith("cmpnd_"):
810
+ raise ValueError(
811
+ f"tool name {name!r} uses the reserved cmpnd_ prefix; rename the tool before deploying"
812
+ )
805
813
  if is_bare_mcp_tool(tool):
806
814
  _resolve_an_mcp_tool(name, tool, {})
807
815
  previous = collected.get(name)
@@ -818,6 +826,8 @@ def declared_module_tools(module: dspy.Module) -> dict[str, typing.Any]:
818
826
  def generate_program_source(
819
827
  module: dspy.Module,
820
828
  mcp_tools: Mapping[tuple[str, str], typing.Any] | None = None,
829
+ *,
830
+ _declarations: list[dict[str, typing.Any]] | None = None,
821
831
  ) -> str:
822
832
  """Return raw Python source for module.
823
833
 
@@ -968,6 +978,12 @@ def generate_program_source(
968
978
  from . import mcp_servers
969
979
 
970
980
  mcp_bindings: list[tuple[Any, str]] = []
981
+
982
+ def mcp_tool_expression(route: str, alias: str, *, wrap: bool = False) -> str:
983
+ if wrap or alias != route:
984
+ return f"dspy.Tool({route}, name={alias!r})"
985
+ return route
986
+
971
987
  for name, tool in tools_dict.items():
972
988
  if name == "finish":
973
989
  continue
@@ -977,18 +993,22 @@ def generate_program_source(
977
993
  # instead of a body — see [_generate_mcp_tool_stub]. The `def`'s name is
978
994
  # the MCP tool's own, because the declaration names the tool and the
979
995
  # worker maps that name to the token it holds.
980
- signature = mcp_servers.synthesized_signature(pinned)
981
- lines.append(_generate_mcp_tool_stub(pinned, signature))
982
- mcp_bindings.append((mcp_servers.mcp_tool_reference(tool), f"dspy.Tool({signature.name})"))
983
- mcp_declarations.append(
984
- {
985
- "tool": signature.name,
986
- "server": pinned.server_url,
987
- "schema_hash": pinned.schema_hash,
988
- "parameters": signature.parameter_names,
989
- }
990
- )
991
- tool_names.append(signature.name)
996
+ reference = mcp_servers.mcp_tool_reference(tool)
997
+ route = next((route for declared, route in mcp_bindings if declared == reference), None)
998
+ if route is None:
999
+ signature = mcp_servers.synthesized_signature(pinned)
1000
+ route = signature.name
1001
+ lines.append(_generate_mcp_tool_stub(pinned, signature))
1002
+ mcp_bindings.append((reference, route))
1003
+ mcp_declarations.append(
1004
+ {
1005
+ "tool": route,
1006
+ "server": pinned.server_url,
1007
+ "schema_hash": pinned.schema_hash,
1008
+ "parameters": signature.parameter_names,
1009
+ }
1010
+ )
1011
+ tool_names.append(mcp_tool_expression(route, name))
992
1012
  continue
993
1013
  func = getattr(tool, "func", None)
994
1014
  if func is not None:
@@ -1024,9 +1044,10 @@ def generate_program_source(
1024
1044
  ancestors = ancestors | {id(value)}
1025
1045
  reference = mcp_servers.mcp_tool_reference(value)
1026
1046
  if reference is not None:
1027
- for declared, source in mcp_bindings:
1047
+ for declared, route in mcp_bindings:
1028
1048
  if reference == declared:
1029
- return source
1049
+ alias = str(getattr(value, "name", route))
1050
+ return mcp_tool_expression(route, alias, wrap=True)
1030
1051
  raise ValueError(
1031
1052
  "MCP tool binding is not declared by this program; deploy the authored source file"
1032
1053
  )
@@ -1178,6 +1199,8 @@ def generate_program_source(
1178
1199
  # decide whether an empty list means "none" or "not resolved".
1179
1200
  if mcp_declarations:
1180
1201
  lines.append(f"{MCP_TOOLS_DECLARATION} = {mcp_declarations!r}")
1202
+ if _declarations is not None:
1203
+ _declarations.extend(mcp_declarations)
1181
1204
 
1182
1205
  return "\n\n".join(lines)
1183
1206
 
@@ -1346,7 +1369,12 @@ def generate_source_zip(
1346
1369
  """
1347
1370
  import dspy
1348
1371
 
1349
- program_source = generate_program_source(module, mcp_tools=mcp_tools)
1372
+ tool_declarations: list[dict[str, typing.Any]] = []
1373
+ program_source = generate_program_source(
1374
+ module,
1375
+ mcp_tools=mcp_tools,
1376
+ _declarations=tool_declarations,
1377
+ )
1350
1378
 
1351
1379
  metric_filename = None
1352
1380
  metric_source = None
@@ -1359,7 +1387,7 @@ def generate_source_zip(
1359
1387
 
1360
1388
  lm = _extract_lm_object(module)
1361
1389
  declarations = lm_configuration.resolve({"default-lm": lm} if lm is not None else {}, require_all=False)
1362
- manifest = {
1390
+ manifest: dict[str, typing.Any] = {
1363
1391
  "lm_capabilities": declarations,
1364
1392
  "dependency_versions": {
1365
1393
  "python": platform.python_version(),
@@ -1368,6 +1396,8 @@ def generate_source_zip(
1368
1396
  "program": "program_source.py",
1369
1397
  "metric": metric_filename,
1370
1398
  }
1399
+ if tool_declarations:
1400
+ manifest["tools"] = tool_declarations
1371
1401
 
1372
1402
  buf = io.BytesIO()
1373
1403
  with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
@@ -282,12 +282,14 @@ immediately, because the credential goes with it. The confirmation says so.
282
282
  A program built with `cmpnd.mcp_tools` deploys against exactly this registry.
283
283
  `cmpnd deploy` resolves each of the program's tool references against it before
284
284
  packaging anything — one listing read plus one read per server, and none at all
285
- for a program with no MCP tools — so the bundle pins the same reduced schema the
286
- platform will validate every call against. An unregistered server and a tool the
287
- registry does not list are both refused at the terminal, naming the tool, the
288
- server and the command that fixes it; the platform refuses the same two
289
- authoritatively, plus a tool that is registered but not allow-listed and a schema
290
- that has changed since the program was built. See
285
+ for a program with no MCP tools — so the bundle records declarations built from the
286
+ current reduced schema and its hash. Serve checks that hash against the current
287
+ registry at deploy time. mcp-tools validates each call against the current stored
288
+ schema, so a refresh can change call validation after a program was built. An
289
+ unregistered server and a tool the registry does not list are both refused at the
290
+ terminal, naming the tool, the server and the command that fixes it; the platform
291
+ refuses the same two authoritatively, plus a tool that is registered but not
292
+ allow-listed and a schema that has changed since the program was built. See
291
293
  [`sdk-instrumentation.md`](sdk-instrumentation.md) for what the bundle carries in
292
294
  place of a tool's body.
293
295
 
@@ -401,7 +401,8 @@ def web_search_exa(query: str, numResults: float | None = None) -> dict:
401
401
 
402
402
 
403
403
  __cmpnd_mcp_tools__ = [{'tool': 'web_search_exa', 'server': 'https://mcp.exa.ai/mcp',
404
- 'schema_hash': '…', 'parameters': ['query', 'numResults']}]
404
+ 'schema_hash': '…',
405
+ 'parameters': ['query', 'numResults']}]
405
406
  ```
406
407
 
407
408
  The body is a refusal because the deployed program runs in the kernel, which has no MCP
@@ -425,6 +426,13 @@ annotation at all for a type name the table does not carry. A property name that
425
426
  Python identifier, or is a keyword, refuses the deploy — it is never mangled, because the
426
427
  name has to reach the MCP server unchanged.
427
428
 
429
+ The SDK uses the registry's pinned, secret-free `input_schema` to construct the generated
430
+ function signature. The source marker and generated bundle manifest retain `schema_hash`
431
+ and `parameters`. Serve checks the hash against the registry at deploy time, and the
432
+ worker uses the parameters for positional binding. Runtime schema inspection reads the
433
+ current stored schema through credentialed-egress and mcp-tools, so it does not rely on a
434
+ schema copy in the program bundle.
435
+
428
436
  ### Direct local MCP discovery supports two client generations
429
437
 
430
438
  When registry lookup falls back to a direct local connection, the SDK lists tools
@@ -622,3 +630,9 @@ The WASM kernel calls tools through a typed worker import. The worker holds the
622
630
  scoped tokens and returns the MCP result document. Optional arguments omitted by
623
631
  the program remain omitted. ReAct's tool callbacks produce the application span;
624
632
  the worker adds transport measurements to that span using its correlation ID.
633
+
634
+ The worker reserves the `cmpnd_` prefix for platform tools. Deployment refuses an
635
+ authored tool that uses this prefix and asks the author to rename it. Platform tools
636
+ that the worker adds while a program runs carry a private, copy-stable provenance token.
637
+ Only tools carrying that token stay out of the program's identity and generated bundle.
638
+ Setting a similarly named attribute does not grant platform provenance.
@@ -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.27.3"
8
+ version = "0.27.4"
9
9
  description = "DSPy observability and deployment SDK for cmpnd"
10
10
  readme = "README.md"
11
11
  requires-python = ">=3.10"
@@ -13,6 +13,7 @@ from typing import Any
13
13
  from uuid import uuid4
14
14
 
15
15
  import httpx
16
+ import pytest
16
17
 
17
18
  from cmpnd import retry
18
19
  from cmpnd.configuration import Config
@@ -1007,9 +1008,10 @@ def test_data_send_retry_exhausted_counts_error():
1007
1008
  assert exporter._dropped_count == 0
1008
1009
 
1009
1010
 
1010
- def test_data_send_does_not_retry_500():
1011
- """A 500 is the platform's to answer for — counted once, not retried."""
1012
- capture = SequenceCapture([500])
1011
+ @pytest.mark.parametrize("status", [422, 500])
1012
+ def test_data_send_does_not_retry_permanent_error(status: int):
1013
+ """The exporter counts a permanent error once and does not retry it."""
1014
+ capture = SequenceCapture([status])
1013
1015
  exporter = _make_seq_exporter(capture)
1014
1016
 
1015
1017
  exporter._flush_batch([ExportItem(item_type="trace", data=_make_trace(), timestamp=0.0)])
@@ -21,6 +21,9 @@ from __future__ import annotations
21
21
 
22
22
  import ast
23
23
  import functools
24
+ import io
25
+ import json
26
+ import zipfile
24
27
  from typing import Any
25
28
 
26
29
  import pytest
@@ -28,7 +31,8 @@ import pytest
28
31
  dspy = pytest.importorskip("dspy")
29
32
 
30
33
  from cmpnd import mcp_servers # noqa: E402
31
- from cmpnd.packaging import generate_program_source # noqa: E402
34
+ from cmpnd._runtime_tools import mark_runtime_tool # noqa: E402
35
+ from cmpnd.packaging import declared_module_tools, generate_program_source, generate_source_zip # noqa: E402
32
36
 
33
37
  global_search_tool: Any = None
34
38
  global_search_tools: Any = None
@@ -132,6 +136,7 @@ class TestTheGeneratedStub:
132
136
  assert "'tool': 'web_search_exa'" in source
133
137
  assert f"'server': {_EXA_SERVER!r}" in source
134
138
  assert f"'schema_hash': {'c' * 64!r}" in source
139
+ assert "'input_schema'" not in source
135
140
  assert "'parameters': ['query', 'numResults']" in source
136
141
 
137
142
  def test_the_declared_parameters_are_the_defs_own_in_order(self, monkeypatch):
@@ -145,6 +150,65 @@ class TestTheGeneratedStub:
145
150
  declared = namespace["__cmpnd_mcp_tools__"][0]
146
151
  stub = namespace[declared["tool"]]
147
152
  assert list(inspect.signature(stub).parameters) == declared["parameters"]
153
+ assert "input_schema" not in declared
154
+
155
+ def test_the_zip_manifest_carries_the_same_pinned_declaration(self, monkeypatch):
156
+ tools = _exa_tools(monkeypatch)
157
+ archive_bytes = generate_source_zip(
158
+ _react_with(tools[0]),
159
+ mcp_tools={(_EXA_SERVER, "web_search_exa"): _EXA_SEARCH},
160
+ )
161
+ with zipfile.ZipFile(io.BytesIO(archive_bytes)) as archive:
162
+ manifest = json.loads(archive.read("manifest.json"))
163
+ namespace: dict[str, Any] = {}
164
+ exec(compile(archive.read("program_source.py"), "<generated>", "exec"), namespace) # noqa: S102
165
+
166
+ assert manifest["tools"] == namespace["__cmpnd_mcp_tools__"]
167
+
168
+ def test_the_zip_preserves_an_alias_while_the_declaration_keeps_the_route(self, monkeypatch):
169
+ tool = _exa_tools(monkeypatch)[0]
170
+ tool.name = "fetch_pages"
171
+ archive_bytes = generate_source_zip(
172
+ _react_with(tool),
173
+ mcp_tools={(_EXA_SERVER, "web_search_exa"): _EXA_SEARCH},
174
+ )
175
+ with zipfile.ZipFile(io.BytesIO(archive_bytes)) as archive:
176
+ manifest = json.loads(archive.read("manifest.json"))
177
+ namespace: dict[str, Any] = {}
178
+ exec(compile(archive.read("program_source.py"), "<generated>", "exec"), namespace) # noqa: S102
179
+
180
+ rebuilt = namespace["module"]
181
+ assert set(rebuilt.tools) == {"fetch_pages", "finish"}
182
+ assert rebuilt.tools["fetch_pages"].func.__name__ == "web_search_exa"
183
+ assert manifest["tools"] == [
184
+ {
185
+ "tool": "web_search_exa",
186
+ "server": _EXA_SERVER,
187
+ "schema_hash": "c" * 64,
188
+ "parameters": ["query", "numResults"],
189
+ }
190
+ ]
191
+
192
+ def test_two_aliases_share_one_canonical_declaration(self, monkeypatch):
193
+ first = _exa_tools(monkeypatch)[0]
194
+ second = _exa_tools(monkeypatch)[0]
195
+ first.name = "fetch_pages"
196
+ second.name = "search_pages"
197
+ archive_bytes = generate_source_zip(
198
+ dspy.ReAct("question -> answer", tools=[first, second]),
199
+ mcp_tools={(_EXA_SERVER, "web_search_exa"): _EXA_SEARCH},
200
+ )
201
+ with zipfile.ZipFile(io.BytesIO(archive_bytes)) as archive:
202
+ manifest = json.loads(archive.read("manifest.json"))
203
+ namespace: dict[str, Any] = {}
204
+ exec(compile(archive.read("program_source.py"), "<generated>", "exec"), namespace) # noqa: S102
205
+
206
+ rebuilt = namespace["module"]
207
+ assert set(rebuilt.tools) == {"fetch_pages", "search_pages", "finish"}
208
+ assert {rebuilt.tools[name].func.__name__ for name in ("fetch_pages", "search_pages")} == {"web_search_exa"}
209
+ assert len(manifest["tools"]) == 1
210
+ assert manifest["tools"][0]["tool"] == "web_search_exa"
211
+ assert "input_schema" not in manifest["tools"][0]
148
212
 
149
213
  def test_the_generated_source_names_the_stub_in_the_tools_kwarg(self, monkeypatch):
150
214
  source = self._source(monkeypatch)
@@ -360,7 +424,45 @@ class TestPlainToolsAreUnaffected:
360
424
  assert "'tool': 'web_search_exa'" in source
361
425
  assert "'_add_at_module_scope'" not in source, "a source tool is not an MCP declaration"
362
426
 
427
+ def test_user_tool_in_the_platform_namespace_is_refused(self):
428
+ module = dspy.ReAct(
429
+ "question -> answer",
430
+ tools=[dspy.Tool(_add_at_module_scope, name="cmpnd_inspect_tool_schema")],
431
+ )
432
+ with pytest.raises(ValueError, match="cmpnd_inspect_tool_schema.*rename"):
433
+ generate_program_source(module)
434
+
435
+ def test_user_cannot_claim_platform_provenance_with_a_boolean_attribute(self):
436
+ tool = dspy.Tool(_add_at_module_scope, name="ordinary_tool")
437
+ tool._cmpnd_runtime_tool = True
438
+ module = _react_with(tool)
439
+ del module.tools[tool.name]
440
+ module.tools["cmpnd_inspect_tool_schema"] = tool
441
+
442
+ with pytest.raises(ValueError, match="cmpnd_inspect_tool_schema.*rename"):
443
+ generate_program_source(module)
444
+
445
+ def test_runtime_platform_tool_is_not_packaged(self):
446
+ module = _react_with(dspy.Tool(_add_at_module_scope))
447
+ ambient = mark_runtime_tool(dspy.Tool(_runtime_inspector_at_module_scope, name="cmpnd_inspect_tool_schema"))
448
+ module.tools[ambient.name] = ambient
449
+
450
+ assert "cmpnd_inspect_tool_schema" not in declared_module_tools(module)
451
+
452
+ def test_marked_non_platform_tool_is_still_packaged(self):
453
+ module = _react_with(dspy.Tool(_add_at_module_scope))
454
+ marked = dspy.Tool(_add_at_module_scope, name="inspect_tool_schema")
455
+ marked._cmpnd_runtime_tool = True
456
+ module.tools[marked.name] = marked
457
+
458
+ assert "inspect_tool_schema" in declared_module_tools(module)
459
+
363
460
 
364
461
  def _add_at_module_scope(x: int, y: int) -> int:
365
462
  """Add two numbers."""
366
463
  return x + y
464
+
465
+
466
+ def _runtime_inspector_at_module_scope(tool: str) -> str:
467
+ """Test-only stand-in for the worker's ambient schema inspector."""
468
+ return tool
@@ -12,6 +12,7 @@ import pytest
12
12
  from hypothesis import HealthCheck, assume, given, settings
13
13
  from hypothesis import strategies as st
14
14
 
15
+ from cmpnd._runtime_tools import mark_runtime_tool
15
16
  from cmpnd.ir import (
16
17
  ChainOfThought as IR_ChainOfThought,
17
18
  )
@@ -155,6 +156,60 @@ def test_react_tool_set_changes_hash() -> None:
155
156
  assert program_signature_hash(reflect(r1)) != program_signature_hash(reflect(r2))
156
157
 
157
158
 
159
+ def test_react_platform_tool_does_not_change_hash() -> None:
160
+ def search(query: str) -> str:
161
+ return query
162
+
163
+ def inspect_schema(tool: str) -> str:
164
+ return tool
165
+
166
+ react = dspy.ReAct("q -> a", tools=[search])
167
+ before = program_signature_hash(reflect(react))
168
+ ambient = mark_runtime_tool(dspy.Tool(inspect_schema, name="cmpnd_inspect_tool_schema"))
169
+ react.tools[ambient.name] = ambient
170
+
171
+ assert program_signature_hash(reflect(react)) == before
172
+
173
+
174
+ def test_react_user_tool_with_platform_prefix_remains_part_of_identity() -> None:
175
+ def inspect_schema(tool: str) -> str:
176
+ return tool
177
+
178
+ plain = dspy.ReAct("q -> a", tools=[])
179
+ user_tool = dspy.ReAct(
180
+ "q -> a",
181
+ tools=[dspy.Tool(inspect_schema, name="cmpnd_inspect_tool_schema")],
182
+ )
183
+
184
+ assert program_signature_hash(reflect(user_tool)) != program_signature_hash(reflect(plain))
185
+
186
+
187
+ def test_react_boolean_marker_cannot_remove_an_exposed_alias_from_identity() -> None:
188
+ def inspect_schema(tool: str) -> str:
189
+ return tool
190
+
191
+ plain = dspy.ReAct("q -> a", tools=[])
192
+ user_tool = dspy.Tool(inspect_schema, name="ordinary_tool")
193
+ user_tool._cmpnd_runtime_tool = True
194
+ forged = dspy.ReAct("q -> a", tools=[user_tool])
195
+ del forged.tools[user_tool.name]
196
+ forged.tools["cmpnd_inspect_tool_schema"] = user_tool
197
+
198
+ assert program_signature_hash(reflect(forged)) != program_signature_hash(reflect(plain))
199
+
200
+
201
+ def test_react_marked_non_platform_tool_remains_part_of_identity() -> None:
202
+ def inspect_schema(tool: str) -> str:
203
+ return tool
204
+
205
+ plain = dspy.ReAct("q -> a", tools=[])
206
+ user_tool = dspy.Tool(inspect_schema, name="inspect_tool_schema")
207
+ user_tool._cmpnd_runtime_tool = True
208
+ marked = dspy.ReAct("q -> a", tools=[user_tool])
209
+
210
+ assert program_signature_hash(reflect(marked)) != program_signature_hash(reflect(plain))
211
+
212
+
158
213
  def test_refine_distinct_from_inner() -> None:
159
214
  import cmpnd.ir
160
215
 
@@ -54,6 +54,7 @@ import dspy
54
54
  import numpy as np
55
55
  import pytest
56
56
 
57
+ from cmpnd._runtime_tools import mark_runtime_tool
57
58
  from cmpnd.ir import (
58
59
  KNN as IR_KNN,
59
60
  )
@@ -121,6 +122,17 @@ def _sample_tool(query: str) -> str:
121
122
  return query
122
123
 
123
124
 
125
+ def _inspect_tool_schema(tool: str) -> str:
126
+ return tool
127
+
128
+
129
+ def _build_react() -> dspy.ReAct:
130
+ react = dspy.ReAct("q -> a", tools=[_sample_tool])
131
+ inspector = mark_runtime_tool(dspy.Tool(_inspect_tool_schema, name="cmpnd_inspect_tool_schema"))
132
+ react.tools[inspector.name] = inspector
133
+ return react
134
+
135
+
124
136
  def _build_user_composite() -> dspy.Module:
125
137
  class _TwoPredictApp(dspy.Module):
126
138
  def __init__(self) -> None:
@@ -202,6 +214,7 @@ def _check_react(ir: Any) -> None:
202
214
  f"expected user tool in {ir.tools}"
203
215
  )
204
216
  assert "finish" not in ir.tools, "auto-injected finish sentinel leaked into identity"
217
+ assert "cmpnd_inspect_tool_schema" not in ir.tools, "ambient platform tool leaked into identity"
205
218
 
206
219
 
207
220
  def _check_best_of_n(ir: Any) -> None:
@@ -285,7 +298,7 @@ KIND_FIXTURES: tuple[KindFixture, ...] = (
285
298
  name="ReAct",
286
299
  dspy_attr="ReAct",
287
300
  ir_cls=IR_ReAct,
288
- build=lambda: dspy.ReAct("q -> a", tools=[_sample_tool]),
301
+ build=_build_react,
289
302
  required_attrs=("signature", "tools"),
290
303
  extra=_check_react,
291
304
  ),
@@ -29,6 +29,7 @@ class TestClassification:
29
29
  assert not retry.is_retryable(_resp(500, body={"error": "boom"}))
30
30
  assert not retry.is_retryable(_resp(400))
31
31
  assert not retry.is_retryable(_resp(404))
32
+ assert not retry.is_retryable(_resp(422))
32
33
  assert not retry.is_retryable(_resp(429))
33
34
 
34
35
  def test_explicit_false_is_not_retryable(self):
@@ -0,0 +1,47 @@
1
+ from __future__ import annotations
2
+
3
+ import copy
4
+
5
+ import dspy
6
+
7
+ from cmpnd._runtime_tools import is_runtime_tool, mark_runtime_tool
8
+
9
+
10
+ def test_an_arbitrary_boolean_on_a_tool_is_not_runtime_provenance() -> None:
11
+ def inspect_schema(tool: str) -> str:
12
+ return tool
13
+
14
+ tool = dspy.Tool(inspect_schema, name="cmpnd_inspect_tool_schema")
15
+ tool._cmpnd_runtime_tool = True
16
+
17
+ assert not is_runtime_tool(tool)
18
+
19
+
20
+ def test_an_arbitrary_boolean_on_a_function_is_not_runtime_provenance() -> None:
21
+ def inspect_schema(tool: str) -> str:
22
+ return tool
23
+
24
+ inspect_schema._cmpnd_runtime_tool = True
25
+
26
+ assert not is_runtime_tool(dspy.Tool(inspect_schema, name="cmpnd_inspect_tool_schema"))
27
+
28
+
29
+ def test_runtime_provenance_survives_tool_and_module_copies() -> None:
30
+ def inspect_schema(tool: str) -> str:
31
+ return tool
32
+
33
+ tool = mark_runtime_tool(dspy.Tool(inspect_schema, name="cmpnd_inspect_tool_schema"))
34
+ react = dspy.ReAct("q -> a", tools=[tool])
35
+
36
+ assert is_runtime_tool(copy.copy(tool))
37
+ assert is_runtime_tool(copy.deepcopy(tool))
38
+ assert is_runtime_tool(react.deepcopy().tools[tool.name])
39
+
40
+
41
+ def test_runtime_provenance_survives_callable_normalization() -> None:
42
+ def inspect_schema(tool: str) -> str:
43
+ return tool
44
+
45
+ marked = mark_runtime_tool(inspect_schema)
46
+
47
+ assert is_runtime_tool(dspy.Tool(marked, name="cmpnd_inspect_tool_schema"))
@@ -496,7 +496,7 @@ wheels = [
496
496
 
497
497
  [[package]]
498
498
  name = "cmpnd"
499
- version = "0.27.3"
499
+ version = "0.27.4"
500
500
  source = { editable = "." }
501
501
  dependencies = [
502
502
  { name = "dspy" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes