cmpnd 0.27.2__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.2 → cmpnd-0.27.4}/PKG-INFO +1 -1
  2. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/__init__.py +5 -0
  3. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/_authored_import.py +0 -1
  4. cmpnd-0.27.4/cmpnd/_runtime_tools.py +39 -0
  5. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/deployment.py +3 -47
  6. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/ir/reflection.py +12 -3
  7. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/mcp_servers.py +12 -1
  8. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/packaging.py +171 -36
  9. {cmpnd-0.27.2 → cmpnd-0.27.4}/docs/cli.md +8 -6
  10. {cmpnd-0.27.2 → cmpnd-0.27.4}/docs/sdk-instrumentation.md +32 -1
  11. {cmpnd-0.27.2 → cmpnd-0.27.4}/pyproject.toml +1 -1
  12. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/test_traces_read.py +19 -5
  13. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/integration/hosted_mcp_tool_optimize_probe.py +66 -8
  14. cmpnd-0.27.4/tests/stress/_readiness.py +139 -0
  15. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/stress/test_optimize_loop_wasm.py +53 -25
  16. cmpnd-0.27.4/tests/stress/test_readiness.py +301 -0
  17. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/stress/test_trace_search.py +25 -10
  18. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_deploy_mcp_tool_references.py +53 -25
  19. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_exporter.py +5 -3
  20. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_packaging_mcp_tools.py +203 -2
  21. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_reflector_properties.py +55 -0
  22. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_reflector_smoke.py +14 -1
  23. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_retry.py +1 -0
  24. cmpnd-0.27.4/tests/test_runtime_tools.py +47 -0
  25. {cmpnd-0.27.2 → cmpnd-0.27.4}/uv.lock +1 -1
  26. {cmpnd-0.27.2 → cmpnd-0.27.4}/.gitignore +0 -0
  27. {cmpnd-0.27.2 → cmpnd-0.27.4}/AGENTS.md +0 -0
  28. {cmpnd-0.27.2 → cmpnd-0.27.4}/CLAUDE.md +0 -0
  29. {cmpnd-0.27.2 → cmpnd-0.27.4}/README.md +0 -0
  30. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/_assessments.py +0 -0
  31. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/_exporter_pool.py +0 -0
  32. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/_gepa_patch.py +0 -0
  33. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/_http.py +0 -0
  34. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/_program_patch.py +0 -0
  35. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/_rlm_patch.py +0 -0
  36. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/_timespec.py +0 -0
  37. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/_traces.py +0 -0
  38. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/_valsets.py +0 -0
  39. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/authored.py +0 -0
  40. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/callback.py +0 -0
  41. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/cli/__init__.py +0 -0
  42. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/cli/api_client.py +0 -0
  43. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/cli/commands/__init__.py +0 -0
  44. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/cli/commands/admin.py +0 -0
  45. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/cli/commands/assessments.py +0 -0
  46. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/cli/commands/auth.py +0 -0
  47. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/cli/commands/datasets.py +0 -0
  48. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/cli/commands/deployments.py +0 -0
  49. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/cli/commands/evals.py +0 -0
  50. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/cli/commands/login.py +0 -0
  51. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/cli/commands/optimizations.py +0 -0
  52. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/cli/commands/org.py +0 -0
  53. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/cli/commands/programs.py +0 -0
  54. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/cli/commands/provider_credentials.py +0 -0
  55. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/cli/commands/signatures.py +0 -0
  56. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/cli/commands/tool_servers.py +0 -0
  57. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/cli/commands/top.py +0 -0
  58. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/cli/commands/traces.py +0 -0
  59. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/cli/commands/valsets.py +0 -0
  60. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/cli/credentials.py +0 -0
  61. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/cli/deprecation.py +0 -0
  62. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/cli/pagination.py +0 -0
  63. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/cli/shell.py +0 -0
  64. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/cli/trace_render.py +0 -0
  65. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/cli/tree_help.py +0 -0
  66. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/cli/verbs.py +0 -0
  67. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/client.py +0 -0
  68. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/configuration.py +0 -0
  69. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/context.py +0 -0
  70. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/dataset_sync.py +0 -0
  71. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/datasets.py +0 -0
  72. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/decorators.py +0 -0
  73. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/eval_handler.py +0 -0
  74. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/execution.py +0 -0
  75. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/exporter.py +0 -0
  76. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/exporter_http.py +0 -0
  77. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/helpers.py +0 -0
  78. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/identity.py +0 -0
  79. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/imaging.py +0 -0
  80. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/ir/__init__.py +0 -0
  81. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/ir/decode.py +0 -0
  82. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/ir/encode.py +0 -0
  83. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/ir/hash.py +0 -0
  84. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/ir/program.py +0 -0
  85. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/ir/types.py +0 -0
  86. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/ir/xxh64.py +0 -0
  87. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/lm_configuration.py +0 -0
  88. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/models.py +0 -0
  89. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/optimization.py +0 -0
  90. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/optimizers/__init__.py +0 -0
  91. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/optimizers/gepa.py +0 -0
  92. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/optimizers/gepa_callback.py +0 -0
  93. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/optimizers/resume.py +0 -0
  94. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/optimizers/tracker.py +0 -0
  95. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/retry.py +0 -0
  96. {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/traces.py +0 -0
  97. {cmpnd-0.27.2 → cmpnd-0.27.4}/docs/2026-04-02-code-review.md +0 -0
  98. {cmpnd-0.27.2 → cmpnd-0.27.4}/docs/client.md +0 -0
  99. {cmpnd-0.27.2 → cmpnd-0.27.4}/docs/custom-proposer.md +0 -0
  100. {cmpnd-0.27.2 → cmpnd-0.27.4}/docs/plans/2026-04-05-train-val-dataset-capture-design.md +0 -0
  101. {cmpnd-0.27.2 → cmpnd-0.27.4}/docs/plans/2026-04-05-train-val-dataset-capture.md +0 -0
  102. {cmpnd-0.27.2 → cmpnd-0.27.4}/docs/reading-traces.md +0 -0
  103. {cmpnd-0.27.2 → cmpnd-0.27.4}/examples/generalizing_proposer.py +0 -0
  104. {cmpnd-0.27.2 → cmpnd-0.27.4}/examples/local_ollama.py +0 -0
  105. {cmpnd-0.27.2 → cmpnd-0.27.4}/ingest/LINEAGE.md +0 -0
  106. {cmpnd-0.27.2 → cmpnd-0.27.4}/ingest/README.md +0 -0
  107. {cmpnd-0.27.2 → cmpnd-0.27.4}/ingest/cmpnd_ingest/__init__.py +0 -0
  108. {cmpnd-0.27.2 → cmpnd-0.27.4}/ingest/cmpnd_ingest/bodies.py +0 -0
  109. {cmpnd-0.27.2 → cmpnd-0.27.4}/ingest/cmpnd_ingest/exporter_http.py +0 -0
  110. {cmpnd-0.27.2 → cmpnd-0.27.4}/ingest/cmpnd_ingest/protocols.py +0 -0
  111. {cmpnd-0.27.2 → cmpnd-0.27.4}/ingest/cmpnd_ingest/py.typed +0 -0
  112. {cmpnd-0.27.2 → cmpnd-0.27.4}/ingest/cmpnd_ingest/request_log.py +0 -0
  113. {cmpnd-0.27.2 → cmpnd-0.27.4}/ingest/cmpnd_ingest/retry.py +0 -0
  114. {cmpnd-0.27.2 → cmpnd-0.27.4}/ingest/pyproject.toml +0 -0
  115. {cmpnd-0.27.2 → cmpnd-0.27.4}/scripts/audit-sdist.sh +0 -0
  116. {cmpnd-0.27.2 → cmpnd-0.27.4}/scripts/capture_review_run.py +0 -0
  117. {cmpnd-0.27.2 → cmpnd-0.27.4}/scripts/check_release_artifacts.py +0 -0
  118. {cmpnd-0.27.2 → cmpnd-0.27.4}/scripts/fixtures/review_run.json.gz +0 -0
  119. {cmpnd-0.27.2 → cmpnd-0.27.4}/scripts/gepa_resume_bug.py +0 -0
  120. {cmpnd-0.27.2 → cmpnd-0.27.4}/scripts/release-stress.sh +0 -0
  121. {cmpnd-0.27.2 → cmpnd-0.27.4}/scripts/require_version_bump.py +0 -0
  122. {cmpnd-0.27.2 → cmpnd-0.27.4}/scripts/seed_review_run.py +0 -0
  123. {cmpnd-0.27.2 → cmpnd-0.27.4}/scripts/test-in-linux.sh +0 -0
  124. {cmpnd-0.27.2 → cmpnd-0.27.4}/stubs/dspy/__init__.pyi +0 -0
  125. {cmpnd-0.27.2 → cmpnd-0.27.4}/stubs/dspy/primitives/__init__.pyi +0 -0
  126. {cmpnd-0.27.2 → cmpnd-0.27.4}/stubs/dspy/primitives/prediction.pyi +0 -0
  127. {cmpnd-0.27.2 → cmpnd-0.27.4}/stubs/dspy/utils/__init__.pyi +0 -0
  128. {cmpnd-0.27.2 → cmpnd-0.27.4}/stubs/dspy/utils/callback.pyi +0 -0
  129. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/__init__.py +0 -0
  130. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/_exporter_helpers.py +0 -0
  131. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/_fake_clock.py +0 -0
  132. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/_identity_helpers.py +0 -0
  133. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/conftest.py +0 -0
  134. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/__init__.py +0 -0
  135. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/committee_task.py +0 -0
  136. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/conftest.py +0 -0
  137. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/data/committee_train.ndjson.gz +0 -0
  138. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/data/committee_val.ndjson.gz +0 -0
  139. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/fake_lm.py +0 -0
  140. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/helpers.py +0 -0
  141. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/target_capabilities.py +0 -0
  142. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/test_assessments_cli.py +0 -0
  143. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/test_capture_prompts.py +0 -0
  144. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/test_composite_module.py +0 -0
  145. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/test_dataset_auto_creation.py +0 -0
  146. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/test_deploy_execute.py +0 -0
  147. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/test_deploy_programs.py +0 -0
  148. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/test_edge_cases.py +0 -0
  149. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/test_example_hash.py +0 -0
  150. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/test_gepa_optimization.py +0 -0
  151. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/test_interpreter_spans.py +0 -0
  152. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/test_lm_accuracy.py +0 -0
  153. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/test_module_tracing.py +0 -0
  154. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/test_optimization_lineage.py +0 -0
  155. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/test_optimization_metric_identity.py +0 -0
  156. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/test_optimize_failures.py +0 -0
  157. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/test_optimize_loop.py +0 -0
  158. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/test_optimize_review_committee.py +0 -0
  159. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/test_optimize_server_owned_completion.py +0 -0
  160. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/test_parallel_export.py +0 -0
  161. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/test_project_attribution.py +0 -0
  162. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/test_react_tool_spans.py +0 -0
  163. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/test_save_load.py +0 -0
  164. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/test_trace_structure.py +0 -0
  165. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/test_user_attribution.py +0 -0
  166. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/test_valsets_cli.py +0 -0
  167. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/identity_vectors/01_predict_qa.json +0 -0
  168. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/identity_vectors/02_predict_renamed_field.json +0 -0
  169. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/identity_vectors/03_chain_of_thought_qa.json +0 -0
  170. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/identity_vectors/04_predict_richer_types.json +0 -0
  171. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/identity_vectors/05_composite_two_predicts.json +0 -0
  172. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/identity_vectors/06_multichaincomparison.json +0 -0
  173. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/identity_vectors/07_program_of_thought.json +0 -0
  174. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/identity_vectors/08_opaque_module.json +0 -0
  175. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/identity_vectors/09_predict_pydantic_field.json +0 -0
  176. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/identity_vectors/10_react.json +0 -0
  177. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/identity_vectors/11_refine.json +0 -0
  178. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/identity_vectors/12_retrieve.json +0 -0
  179. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/identity_vectors/13_knn.json +0 -0
  180. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/identity_vectors/14_parallel.json +0 -0
  181. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/identity_vectors/README.md +0 -0
  182. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/ingest_conformance.py +0 -0
  183. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/integration/__init__.py +0 -0
  184. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/integration/authored_optimize/judge.py +0 -0
  185. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/integration/authored_optimize/optimize_hosted.py +0 -0
  186. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/integration/conftest.py +0 -0
  187. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/integration/hosted_optimize_probe.py +0 -0
  188. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/integration/optimization_run_probe.py +0 -0
  189. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/integration/test_cli_deploy_smoketest.py +0 -0
  190. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/integration/test_logs_renders_traces.py +0 -0
  191. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/integration/test_optimization_run.py +0 -0
  192. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/integration/test_optimizing_progress.py +0 -0
  193. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/integration/test_unhappy_path.py +0 -0
  194. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/load/__init__.py +0 -0
  195. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/load/__main__.py +0 -0
  196. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/load/drain.py +0 -0
  197. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/load/ingest.py +0 -0
  198. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/load/latency_transport.py +0 -0
  199. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/load/rlm_shape.py +0 -0
  200. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/load/sweep.py +0 -0
  201. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/load/test_rlm_shape.py +0 -0
  202. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/parity/__init__.py +0 -0
  203. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/parity/conftest.py +0 -0
  204. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/parity/seed_parity.py +0 -0
  205. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/parity/test_auth.py +0 -0
  206. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/parity/test_datasets.py +0 -0
  207. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/parity/test_deployments.py +0 -0
  208. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/parity/test_evals.py +0 -0
  209. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/parity/test_health.py +0 -0
  210. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/parity/test_modules.py +0 -0
  211. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/parity/test_optimizations.py +0 -0
  212. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/parity/test_programs.py +0 -0
  213. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/parity/test_signatures.py +0 -0
  214. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/parity/test_trace_streaming.py +0 -0
  215. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/parity/test_traces.py +0 -0
  216. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/stress/__init__.py +0 -0
  217. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/stress/_provider.py +0 -0
  218. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/stress/_reliability.py +0 -0
  219. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/stress/_result.py +0 -0
  220. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/stress/mint_bedrock_token.py +0 -0
  221. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/stress/test_deploy_programs.py +0 -0
  222. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/stress/test_multi_client_stress.py +0 -0
  223. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/stress/test_optimize_client_committee.py +0 -0
  224. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/stress/test_optimize_loop.py +0 -0
  225. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/stress/test_optimize_stress.py +0 -0
  226. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/stress/test_provider_roles.py +0 -0
  227. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/stress/test_real_provider_twins.py +0 -0
  228. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/stress/test_reliability.py +0 -0
  229. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/stress/test_reliability_classify.py +0 -0
  230. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/stress/test_rlm_wasm.py +0 -0
  231. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_assessments_api.py +0 -0
  232. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_authored.py +0 -0
  233. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_authored_bundle_collisions.py +0 -0
  234. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_authored_metric_contract.py +0 -0
  235. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_authored_named_models.py +0 -0
  236. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_authored_optimize.py +0 -0
  237. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_callback.py +0 -0
  238. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_candidate_lineage.py +0 -0
  239. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_cli/__init__.py +0 -0
  240. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_cli/conftest.py +0 -0
  241. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_cli/test_admin.py +0 -0
  242. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_cli/test_api_client.py +0 -0
  243. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_cli/test_assessments.py +0 -0
  244. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_cli/test_auth.py +0 -0
  245. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_cli/test_credentials.py +0 -0
  246. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_cli/test_datasets.py +0 -0
  247. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_cli/test_deployments.py +0 -0
  248. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_cli/test_evals.py +0 -0
  249. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_cli/test_login.py +0 -0
  250. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_cli/test_main.py +0 -0
  251. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_cli/test_optimizations.py +0 -0
  252. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_cli/test_org.py +0 -0
  253. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_cli/test_pagination.py +0 -0
  254. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_cli/test_parity.py +0 -0
  255. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_cli/test_programs.py +0 -0
  256. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_cli/test_provider_credentials.py +0 -0
  257. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_cli/test_schema_sample.py +0 -0
  258. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_cli/test_shell.py +0 -0
  259. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_cli/test_signatures.py +0 -0
  260. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_cli/test_tool_servers.py +0 -0
  261. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_cli/test_top.py +0 -0
  262. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_cli/test_traces.py +0 -0
  263. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_cli/test_traces_search_wire.py +0 -0
  264. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_cli/test_tree_help.py +0 -0
  265. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_cli/test_valsets.py +0 -0
  266. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_client.py +0 -0
  267. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_clustering/__init__.py +0 -0
  268. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_clustering/conftest.py +0 -0
  269. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_clustering/test_edge_cases.py +0 -0
  270. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_clustering/test_hash_consistency.py +0 -0
  271. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_clustering/test_type_conversion.py +0 -0
  272. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_cmp551_grouping_identity.py +0 -0
  273. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_configuration.py +0 -0
  274. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_context.py +0 -0
  275. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_context_propagation.py +0 -0
  276. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_dataset_polling.py +0 -0
  277. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_dataset_sync.py +0 -0
  278. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_datasets.py +0 -0
  279. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_decorators.py +0 -0
  280. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_deploy.py +0 -0
  281. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_eval.py +0 -0
  282. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_eval_example_attribution.py +0 -0
  283. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_eval_start.py +0 -0
  284. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_eval_trace_grouping.py +0 -0
  285. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_exception_paths.py +0 -0
  286. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_execute.py +0 -0
  287. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_exporter_queue.py +0 -0
  288. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_exporter_real.py +0 -0
  289. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_fake_lm.py +0 -0
  290. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_gepa_callback.py +0 -0
  291. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_gepa_integration.py +0 -0
  292. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_gepa_resume.py +0 -0
  293. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_identity.py +0 -0
  294. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_imaging.py +0 -0
  295. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_import_weight.py +0 -0
  296. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_ingest_conformance.py +0 -0
  297. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_ingest_second_producer.py +0 -0
  298. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_integration.py +0 -0
  299. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_interpreter_callback.py +0 -0
  300. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_ir_encoder_properties.py +0 -0
  301. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_ir_golden_vectors.py +0 -0
  302. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_ir_roundtrip.py +0 -0
  303. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_lm_configuration.py +0 -0
  304. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_lm_usage_shapes.py +0 -0
  305. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_mcp_tools.py +0 -0
  306. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_models.py +0 -0
  307. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_optimization_progress.py +0 -0
  308. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_optimization_start.py +0 -0
  309. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_optimize.py +0 -0
  310. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_optimize_budget.py +0 -0
  311. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_optimize_lift.py +0 -0
  312. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_optimize_lms.py +0 -0
  313. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_optimize_proposer.py +0 -0
  314. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_optimize_reflection_lm.py +0 -0
  315. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_optimize_terminal_failure.py +0 -0
  316. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_packaging.py +0 -0
  317. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_packaging_callable_source.py +0 -0
  318. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_packaging_program_source.py +0 -0
  319. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_poll_until.py +0 -0
  320. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_program_lineage_persist.py +0 -0
  321. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_project_payload.py +0 -0
  322. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_release_stress_script.py +0 -0
  323. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_repro_reactv2_bugs.py +0 -0
  324. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_require_version_bump.py +0 -0
  325. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_reversible_failures.py +0 -0
  326. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_rlm_patch.py +0 -0
  327. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_schemas.py +0 -0
  328. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_seed_review_run.py +0 -0
  329. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_span_sent_once.py +0 -0
  330. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_trace_render.py +0 -0
  331. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_traces_read.py +0 -0
  332. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_tracked_gepa.py +0 -0
  333. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_tracker_enrich.py +0 -0
  334. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_truncation.py +0 -0
  335. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_valsets_api.py +0 -0
  336. {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_xxh64.py +0 -0
  337. {cmpnd-0.27.2 → 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.2
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>
@@ -214,3 +214,8 @@ def de_instrument() -> None:
214
214
  _gepa_patch.unpatch_gepa()
215
215
  _program_patch.unpatch_program_state()
216
216
  _rlm_patch.unpatch_rlm_tools()
217
+
218
+
219
+ def _mcp_tool(declaration): # pyright: ignore[reportUnusedFunction] -- generated bundles call this hook
220
+ """Keep generated declarations importable outside a worker without granting access."""
221
+ return declaration
@@ -122,7 +122,6 @@ def _program_facts(
122
122
  from .authored import ProgramFacts
123
123
 
124
124
  references = deployment._mcp_tool_references(program)
125
- deployment._refuse_mcp_tools_outside_rlm(program, references)
126
125
  pinned: dict[tuple[str, str], Any] = {}
127
126
  if references:
128
127
  import httpx
@@ -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
@@ -250,7 +250,7 @@ def _mcp_tool_references(module) -> list[Any]:
250
250
  """Every MCP reference the program's tools carry, deduplicated, in a stable order."""
251
251
  references: list[Any] = []
252
252
  seen: set[tuple[str, str]] = set()
253
- for tool in packaging.module_tools(module).values():
253
+ for tool in packaging.declared_module_tools(module).values():
254
254
  reference = mcp_servers.mcp_tool_reference(tool)
255
255
  if reference is None:
256
256
  continue
@@ -262,48 +262,6 @@ def _mcp_tool_references(module) -> list[Any]:
262
262
  return references
263
263
 
264
264
 
265
- def _refuse_mcp_tools_outside_rlm(module, references: list[Any]) -> None:
266
- """Refuse a program that declares MCP tools and is not a `dspy.RLM`.
267
-
268
- **At deploy, because the alternative arrives mid-turn in a deployed program.** The
269
- worker answers an MCP-backed tool where `dspy.RLM`'s inner interpreter parks waiting
270
- for one; that is the only place it can intervene without the kernel holding a token it
271
- must never see. A `dspy.ReAct` program calls its tools from kernel Python directly, so
272
- the call reaches the `def` the packager generated and raises — loud, but at run time,
273
- in front of whoever is reading the answer rather than whoever can change the program.
274
-
275
- **A state check and not a name check.** `isinstance` against the live class is what
276
- makes a subclass, a rename and a re-export all answer correctly; matching
277
- `type(module).__name__` against a list would pass the first and refuse the last. It is
278
- the reason this lives here and not in serve: the create body's `module_class` is a
279
- display string a client fills in, and a guard over it would fail open in both
280
- directions.
281
-
282
- Absent `dspy.RLM` refuses too, following `cmpnd.callback`'s idiom. A dspy with no RLM
283
- is a dspy with no path that can serve these tools at all, so refusing is the honest
284
- answer rather than a gap.
285
- """
286
- if not references:
287
- return
288
- # Deferred, as every other dspy use in this module is: a deploy of a program with no
289
- # MCP tools returns above and never pays for the import.
290
- import dspy
291
-
292
- if isinstance(module, getattr(dspy, "RLM", type(None))):
293
- return
294
- names = ", ".join(sorted(reference.tool_name for reference in references))
295
- noun = "the MCP tool" if len(references) == 1 else "the MCP tools"
296
- raise ValueError(
297
- f"{type(module).__name__} declares {noun} {names}, and only dspy.RLM can call "
298
- "them once deployed. The platform answers an MCP tool call underneath the sandbox, "
299
- "at the point where RLM's interpreter waits for a tool result, and no other module "
300
- "reaches that point — its tool call runs inside the sandbox, where the call would "
301
- f"reach a stub and raise. Either wrap this program's work in dspy.RLM, or give "
302
- f"{type(module).__name__} ordinary Python tools, which deploy as source and run in "
303
- "the sandbox."
304
- )
305
-
306
-
307
265
  def _server_identity(url: str) -> str:
308
266
  """A tool server's URL in the one form two spellings of it compare equal in.
309
267
 
@@ -381,7 +339,7 @@ def _resolve_mcp_tool_references(client: httpx.Client, config, references: list[
381
339
  server_url=reference.server_url,
382
340
  tool_name=reference.tool_name,
383
341
  schema_hash=str(tool.get("schema_hash") or ""),
384
- input_schema=tool.get("input_schema") or {},
342
+ input_schema=mcp_servers.registry_schema(tool),
385
343
  description=str(tool.get("description") or ""),
386
344
  )
387
345
  return pinned
@@ -424,10 +382,8 @@ def deploy(module, metric=None, timeout=300, metadata=None):
424
382
  # Resolve the program's MCP tool references against the org's registry, before
425
383
  # anything is packaged and before a deployment row exists. A program with no MCP
426
384
  # tools asks the registry nothing.
385
+ # Validate declarations before contacting the registry or creating a deployment.
427
386
  references = _mcp_tool_references(module)
428
- # Before the round trip, because it needs nothing from the registry: a module that
429
- # cannot call these tools at all is refused whatever the registry says about them.
430
- _refuse_mcp_tools_outside_rlm(module, references)
431
387
  pinned_mcp_tools: dict[tuple[str, str], Any] = {}
432
388
  if references:
433
389
  with httpx.Client(timeout=httpx.Timeout(_TIMEOUT_SECONDS, connect=_CONNECT_TIMEOUT_SECONDS)) as client:
@@ -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")
@@ -696,7 +696,7 @@ def _registered_tools(url: str, timeout_s: float) -> list[PinnedTool] | None | o
696
696
  server_url=url,
697
697
  tool_name=str(tool.get("name", "")),
698
698
  schema_hash=str(tool.get("schema_hash", "")),
699
- input_schema=tool.get("input_schema") or {},
699
+ input_schema=registry_schema(tool),
700
700
  description=str(tool.get("description", "")),
701
701
  )
702
702
  for tool in (detail.json() or {}).get("tools") or []
@@ -704,6 +704,17 @@ def _registered_tools(url: str, timeout_s: float) -> list[PinnedTool] | None | o
704
704
  ]
705
705
 
706
706
 
707
+ def registry_schema(tool: Mapping[str, Any]) -> Mapping[str, Any]:
708
+ """Require the registry's pinned schema before constructing a callable declaration."""
709
+ schema = tool.get("input_schema")
710
+ if not isinstance(schema, dict):
711
+ raise ValueError(
712
+ f"the registry could not supply the pinned schema for tool {tool.get('name')!r}; "
713
+ "restore the tool registry and refresh the server before deploying"
714
+ )
715
+ return schema
716
+
717
+
707
718
  def _tool_from_pinned(pinned: PinnedTool, reference: McpToolReference) -> dspy.Tool:
708
719
  """Build the local declaration DSPy needs without a live MCP session."""
709
720
  import dspy
@@ -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
 
@@ -722,29 +724,11 @@ def _resolve_an_mcp_tool(
722
724
 
723
725
 
724
726
  def _generate_mcp_tool_stub(pinned: typing.Any, signature: typing.Any) -> str:
725
- """The `def` a resolved MCP tool becomes in the generated program.
726
-
727
- A declaration plus a signature, and a body that is a refusal. The deployed program
728
- runs in the kernel, which has no MCP client and must never hold a credential, so the
729
- worker intercepts the call before this function's body runs. Reaching the body means
730
- no tool token was dispatched for it, and a body that returned something instead would
731
- answer the model with a lie.
732
-
733
- The signature is synthesized rather than `*args, **kwargs` because `dspy.RLM` builds
734
- each callable stub and each prompt entry from `inspect.signature(tool.func)` — a tool
735
- the model cannot see a signature for is a tool it cannot call.
736
-
737
- The docstring's reader is the model, which is why it carries two generated sentences
738
- after the server's own description. The first says who answers the call. The second
739
- states the shape the result comes back as, because a deployed call returns the
740
- protocol's own result object while a LOCAL run through DSPy's conversion flattens it
741
- to text — so a model told only what the server says would read the dict as a string.
742
- """
727
+ """Generate a callable declaration with the registered tool's pinned signature."""
743
728
  body_lines = [f"def {signature.name}({signature.rendered}) -> dict:"]
744
729
  body_lines.extend(_docstring_lines(pinned.description))
745
- body_lines.append(" raise RuntimeError(")
746
- body_lines.append(f' "{signature.name} is served by an MCP tool server and cannot run in this process"')
747
- body_lines.append(" )")
730
+ body_lines.append(" raise RuntimeError('this MCP tool requires a worker with registered tool support')")
731
+ body_lines.append(f"{signature.name} = __import__('cmpnd')._mcp_tool({signature.name})")
748
732
  return "\n".join(body_lines)
749
733
 
750
734
 
@@ -761,7 +745,7 @@ def _docstring_lines(description: str) -> list[str]:
761
745
  lines.append(f" {line}".rstrip())
762
746
  lines.append("")
763
747
  lines.append(" Served by an MCP tool server registered to this org. The worker answers this call")
764
- lines.append(" before the body runs; reaching the body means no tool token was dispatched for it.")
748
+ lines.append(" through a scoped capability without exposing a credential to the program.")
765
749
  lines.append("")
766
750
  lines.append(' Returns an MCP tool result: {"content": [{"type": "text", "text": ...}, ...],')
767
751
  lines.append(' "isError": bool}. Read the text parts.')
@@ -796,9 +780,54 @@ def module_tools(module: dspy.Module) -> dict[str, typing.Any]:
796
780
  return dict(tools) if isinstance(tools, dict) else {}
797
781
 
798
782
 
783
+ def declared_module_tools(module: dspy.Module) -> dict[str, typing.Any]:
784
+ """Collect tools throughout a module and refuse ambiguous worker routing names."""
785
+ import dspy
786
+
787
+ from .mcp_servers import is_bare_mcp_tool, mcp_tool_reference
788
+
789
+ collected: dict[str, typing.Any] = {}
790
+ pending: list[typing.Any] = [module]
791
+ visited: set[int] = set()
792
+ while pending:
793
+ member = pending.pop()
794
+ if id(member) in visited:
795
+ continue
796
+ visited.add(id(member))
797
+ if isinstance(member, dict):
798
+ pending.extend(member.values())
799
+ continue
800
+ if isinstance(member, (list, tuple)):
801
+ pending.extend(member)
802
+ continue
803
+ if not isinstance(member, dspy.Module):
804
+ continue
805
+ pending.extend(vars(member).values())
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
+ )
813
+ if is_bare_mcp_tool(tool):
814
+ _resolve_an_mcp_tool(name, tool, {})
815
+ previous = collected.get(name)
816
+ if previous is not None and previous is not tool:
817
+ old_ref = mcp_tool_reference(previous)
818
+ new_ref = mcp_tool_reference(tool)
819
+ if old_ref is not None or new_ref is not None:
820
+ if old_ref != new_ref:
821
+ raise ValueError(f"tool name {name!r} refers to different tools in this program")
822
+ collected[name] = tool
823
+ return collected
824
+
825
+
799
826
  def generate_program_source(
800
827
  module: dspy.Module,
801
828
  mcp_tools: Mapping[tuple[str, str], typing.Any] | None = None,
829
+ *,
830
+ _declarations: list[dict[str, typing.Any]] | None = None,
802
831
  ) -> str:
803
832
  """Return raw Python source for module.
804
833
 
@@ -942,10 +971,19 @@ def generate_program_source(
942
971
  tool_names: list[str] = []
943
972
  mcp_declarations: list[dict[str, typing.Any]] = []
944
973
  pinned_mcp_tools = mcp_tools or {}
945
- tools_dict = module_tools(module)
974
+ tools_dict = declared_module_tools(module)
946
975
  if tools_dict:
976
+ from dspy import Tool
977
+
947
978
  from . import mcp_servers
948
979
 
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
+
949
987
  for name, tool in tools_dict.items():
950
988
  if name == "finish":
951
989
  continue
@@ -955,23 +993,108 @@ def generate_program_source(
955
993
  # instead of a body — see [_generate_mcp_tool_stub]. The `def`'s name is
956
994
  # the MCP tool's own, because the declaration names the tool and the
957
995
  # worker maps that name to the token it holds.
958
- signature = mcp_servers.synthesized_signature(pinned)
959
- lines.append(_generate_mcp_tool_stub(pinned, signature))
960
- mcp_declarations.append(
961
- {
962
- "tool": signature.name,
963
- "server": pinned.server_url,
964
- "schema_hash": pinned.schema_hash,
965
- "parameters": signature.parameter_names,
966
- }
967
- )
968
- 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))
969
1012
  continue
970
1013
  func = getattr(tool, "func", None)
971
1014
  if func is not None:
972
1015
  _extract_callable_tree(func)
973
1016
  tool_names.append(name)
974
1017
 
1018
+ # A composed constructor refers to its Python bindings, which need not share
1019
+ # the registered routing name. Rebuild those bindings from pinned declarations
1020
+ # without carrying the live MCP session or credential into generated source.
1021
+ if is_user_module:
1022
+ bindings: dict[str, str] = {}
1023
+
1024
+ def function_binding(func: Any) -> str:
1025
+ if not inspect.isfunction(func) or not func.__name__.isidentifier():
1026
+ raise ValueError("Cannot reconstruct this tool callable; deploy the authored source file")
1027
+ _extract_callable_tree(func)
1028
+ if func.__name__ not in emitted_sources:
1029
+ raise ValueError("Cannot recover tool source; deploy the authored source file")
1030
+ return func.__name__
1031
+
1032
+ def contains_mcp(value: Any, seen: set[int]) -> bool:
1033
+ if id(value) in seen:
1034
+ return False
1035
+ seen.add(id(value))
1036
+ if mcp_servers.mcp_tool_reference(value) is not None:
1037
+ return True
1038
+ members = value.values() if isinstance(value, dict) else value
1039
+ return isinstance(value, (list, tuple, dict)) and any(contains_mcp(item, seen) for item in members)
1040
+
1041
+ def binding_source(value: Any, ancestors: frozenset[int]) -> str:
1042
+ if id(value) in ancestors:
1043
+ raise ValueError("Cannot reconstruct cyclic MCP tool bindings; deploy the authored source file")
1044
+ ancestors = ancestors | {id(value)}
1045
+ reference = mcp_servers.mcp_tool_reference(value)
1046
+ if reference is not None:
1047
+ for declared, route in mcp_bindings:
1048
+ if reference == declared:
1049
+ alias = str(getattr(value, "name", route))
1050
+ return mcp_tool_expression(route, alias, wrap=True)
1051
+ raise ValueError(
1052
+ "MCP tool binding is not declared by this program; deploy the authored source file"
1053
+ )
1054
+ if isinstance(value, list):
1055
+ return "[" + ", ".join(binding_source(item, ancestors) for item in value) + "]"
1056
+ if isinstance(value, tuple):
1057
+ return "(" + "".join(binding_source(item, ancestors) + ", " for item in value) + ")"
1058
+ if isinstance(value, dict):
1059
+ if not all(isinstance(key, str) for key in value):
1060
+ raise ValueError("MCP tool dictionaries require string keys; deploy the authored source file")
1061
+ return (
1062
+ "{"
1063
+ + ", ".join(f"{key!r}: {binding_source(item, ancestors)}" for key, item in value.items())
1064
+ + "}"
1065
+ )
1066
+ if isinstance(value, Tool):
1067
+ name = function_binding(value.func)
1068
+ if not _is_literal_constant(value.args):
1069
+ raise ValueError("Cannot reconstruct tool arguments; deploy the authored source file")
1070
+ return f"dspy.Tool({name}, name={value.name!r}, desc={value.desc!r}, args={value.args!r})"
1071
+ if callable(value):
1072
+ return function_binding(value)
1073
+ if _is_literal_constant(value):
1074
+ return repr(value)
1075
+ raise ValueError("Cannot reconstruct a binding containing MCP tools; deploy the authored source file")
1076
+
1077
+ for method_name in ("__init__", "forward"):
1078
+ method = getattr(cls, method_name, None)
1079
+ if not inspect.isfunction(method):
1080
+ continue
1081
+ closure = inspect.getclosurevars(method)
1082
+ for name, value in {**closure.globals, **closure.nonlocals}.items():
1083
+ if not contains_mcp(value, set()):
1084
+ continue
1085
+ rendered = binding_source(value, frozenset())
1086
+ if name in bindings and bindings[name] != rendered:
1087
+ raise ValueError(
1088
+ f"MCP tool binding {name!r} has conflicting values; deploy the authored source file"
1089
+ )
1090
+ bindings[name] = rendered
1091
+ if bindings:
1092
+ # Evaluate every declaration before rebinding any name: an author's
1093
+ # alias can itself be another registered tool's routing name.
1094
+ names = ", ".join(bindings)
1095
+ values = ", ".join(bindings.values())
1096
+ lines.append(f"({names},) = ({values},)")
1097
+
975
1098
  # Reconstruct the synthetic ProgramSignature class up-front (when that
976
1099
  # path applies) so its annotation imports land in sig_imports *before*
977
1100
  # we emit the import block below. The class text itself is appended
@@ -1009,6 +1132,9 @@ def generate_program_source(
1009
1132
  extra_kwargs = ""
1010
1133
  if tool_names:
1011
1134
  extra_kwargs = f", tools=[{', '.join(tool_names)}]"
1135
+ max_iters = getattr(module, "max_iters", None)
1136
+ if not is_user_module and isinstance(max_iters, int):
1137
+ extra_kwargs += f", max_iters={max_iters!r}"
1012
1138
 
1013
1139
  module_assigned = False
1014
1140
  if sig is not None:
@@ -1073,6 +1199,8 @@ def generate_program_source(
1073
1199
  # decide whether an empty list means "none" or "not resolved".
1074
1200
  if mcp_declarations:
1075
1201
  lines.append(f"{MCP_TOOLS_DECLARATION} = {mcp_declarations!r}")
1202
+ if _declarations is not None:
1203
+ _declarations.extend(mcp_declarations)
1076
1204
 
1077
1205
  return "\n\n".join(lines)
1078
1206
 
@@ -1241,7 +1369,12 @@ def generate_source_zip(
1241
1369
  """
1242
1370
  import dspy
1243
1371
 
1244
- 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
+ )
1245
1378
 
1246
1379
  metric_filename = None
1247
1380
  metric_source = None
@@ -1254,7 +1387,7 @@ def generate_source_zip(
1254
1387
 
1255
1388
  lm = _extract_lm_object(module)
1256
1389
  declarations = lm_configuration.resolve({"default-lm": lm} if lm is not None else {}, require_all=False)
1257
- manifest = {
1390
+ manifest: dict[str, typing.Any] = {
1258
1391
  "lm_capabilities": declarations,
1259
1392
  "dependency_versions": {
1260
1393
  "python": platform.python_version(),
@@ -1263,6 +1396,8 @@ def generate_source_zip(
1263
1396
  "program": "program_source.py",
1264
1397
  "metric": metric_filename,
1265
1398
  }
1399
+ if tool_declarations:
1400
+ manifest["tools"] = tool_declarations
1266
1401
 
1267
1402
  buf = io.BytesIO()
1268
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
@@ -605,3 +613,26 @@ A new hand-rolled serializer on any of those paths reopens the hole. `tests/test
605
613
  - [`./optimization-tracking.md`](../../docs/optimization-tracking.md) — how `auto_instrument`'s GEPA patch and `cmpnd/optimizers/tracker.py` turn a `GEPA.compile` call into an `OptimizationRun` with linked evals.
606
614
  - The `cmpnd-testing` skill + [`../AGENTS.md`](../AGENTS.md) § "Running Tests & Checks" — patterns for writing callback tests against real databases, including `clear_context()` usage.
607
615
  - [`serve/DB.md`](../../serve/DB.md) — the Postgres schema these payloads land in.
616
+
617
+ ### Registered MCP tools in ReAct
618
+
619
+ `cmpnd.mcp_tools` declares registered tools for ReAct and RLM, including modules
620
+ that contain them. Deployment resolves those declarations against the organization's
621
+ registry and pins their schemas. Bare MCP session closures cannot be packaged.
622
+ Conflicting tools with the same routing name are refused.
623
+ Generated composed programs retain global and closure aliases for registered tools,
624
+ including aliases stored in lists, tuples, or dictionaries. The packager rebuilds
625
+ these bindings from pinned declarations without copying live MCP sessions.
626
+ Generated mixed-tool bindings require ordinary tools to be named Python functions.
627
+ Use authored-source deployment for partials, callable instances, lambdas, or bound methods.
628
+
629
+ The WASM kernel calls tools through a typed worker import. The worker holds the
630
+ scoped tokens and returns the MCP result document. Optional arguments omitted by
631
+ the program remain omitted. ReAct's tool callbacks produce the application span;
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.2"
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"
@@ -9,6 +9,8 @@ mismatch a mocked transport cannot. Skipped unless CMPND_E2E_API_KEY is set
9
9
 
10
10
  from __future__ import annotations
11
11
 
12
+ import time
13
+
12
14
  import dspy
13
15
  import pytest
14
16
 
@@ -26,16 +28,28 @@ class TestTracesReadE2E:
26
28
  assert trace_id is not None, "no trace captured"
27
29
  program_hash = server_client.get_trace(trace_id)["trace"]["program_signature_hash"]
28
30
 
29
- # list by the program that produced it finds the trace.
30
- page = cmpnd.traces.list(hash=str(program_hash), limit=50, api_key=CMPND_E2E_API_KEY, endpoint=E2E_ENDPOINT)
31
- assert trace_id in [t["trace_id"] for t in page["traces"]]
32
-
33
- # show returns the trace and its spans.
31
+ # Detail reads the ingested source without waiting for collection projection.
34
32
  detail = cmpnd.traces.show(trace_id, api_key=CMPND_E2E_API_KEY, endpoint=E2E_ENDPOINT)
35
33
  assert detail["trace"]["trace_id"] == trace_id
36
34
  assert isinstance(detail["spans"], list)
37
35
  assert len(detail["spans"]) >= 1
38
36
 
37
+ # Collection publication is asynchronous. Allow its 30-second idle
38
+ # cooldown plus processing time, polling the same trace rather than
39
+ # rerunning the program and restarting the freshness window.
40
+ deadline = time.monotonic() + 90
41
+ while True:
42
+ page = cmpnd.traces.list(hash=str(program_hash), limit=50, api_key=CMPND_E2E_API_KEY, endpoint=E2E_ENDPOINT)
43
+ listed_ids = [t["trace_id"] for t in page["traces"]]
44
+ if trace_id in listed_ids:
45
+ break
46
+ remaining = deadline - time.monotonic()
47
+ assert remaining > 0, (
48
+ f"Trace {trace_id} is readable in detail but absent from its program's collection after 90s; "
49
+ f"program={program_hash}, last listed trace IDs={listed_ids}"
50
+ )
51
+ time.sleep(min(2, remaining))
52
+
39
53
  def test_show_missing_trace_raises(self):
40
54
  with pytest.raises(cmpnd.ApiError):
41
55
  cmpnd.traces.show("00000000-0000-0000-0000-000000000000", api_key=CMPND_E2E_API_KEY, endpoint=E2E_ENDPOINT)