cmpnd 0.27.2__tar.gz → 0.27.3__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 (335) hide show
  1. {cmpnd-0.27.2 → cmpnd-0.27.3}/PKG-INFO +1 -1
  2. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/__init__.py +5 -0
  3. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/_authored_import.py +0 -1
  4. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/deployment.py +3 -47
  5. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/mcp_servers.py +12 -1
  6. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/packaging.py +128 -23
  7. {cmpnd-0.27.2 → cmpnd-0.27.3}/docs/sdk-instrumentation.md +17 -0
  8. {cmpnd-0.27.2 → cmpnd-0.27.3}/pyproject.toml +1 -1
  9. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/test_traces_read.py +19 -5
  10. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/integration/hosted_mcp_tool_optimize_probe.py +66 -8
  11. cmpnd-0.27.3/tests/stress/_readiness.py +139 -0
  12. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/stress/test_optimize_loop_wasm.py +53 -25
  13. cmpnd-0.27.3/tests/stress/test_readiness.py +301 -0
  14. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/stress/test_trace_search.py +25 -10
  15. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_deploy_mcp_tool_references.py +53 -25
  16. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_packaging_mcp_tools.py +100 -1
  17. {cmpnd-0.27.2 → cmpnd-0.27.3}/uv.lock +1 -1
  18. {cmpnd-0.27.2 → cmpnd-0.27.3}/.gitignore +0 -0
  19. {cmpnd-0.27.2 → cmpnd-0.27.3}/AGENTS.md +0 -0
  20. {cmpnd-0.27.2 → cmpnd-0.27.3}/CLAUDE.md +0 -0
  21. {cmpnd-0.27.2 → cmpnd-0.27.3}/README.md +0 -0
  22. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/_assessments.py +0 -0
  23. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/_exporter_pool.py +0 -0
  24. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/_gepa_patch.py +0 -0
  25. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/_http.py +0 -0
  26. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/_program_patch.py +0 -0
  27. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/_rlm_patch.py +0 -0
  28. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/_timespec.py +0 -0
  29. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/_traces.py +0 -0
  30. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/_valsets.py +0 -0
  31. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/authored.py +0 -0
  32. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/callback.py +0 -0
  33. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/cli/__init__.py +0 -0
  34. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/cli/api_client.py +0 -0
  35. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/cli/commands/__init__.py +0 -0
  36. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/cli/commands/admin.py +0 -0
  37. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/cli/commands/assessments.py +0 -0
  38. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/cli/commands/auth.py +0 -0
  39. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/cli/commands/datasets.py +0 -0
  40. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/cli/commands/deployments.py +0 -0
  41. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/cli/commands/evals.py +0 -0
  42. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/cli/commands/login.py +0 -0
  43. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/cli/commands/optimizations.py +0 -0
  44. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/cli/commands/org.py +0 -0
  45. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/cli/commands/programs.py +0 -0
  46. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/cli/commands/provider_credentials.py +0 -0
  47. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/cli/commands/signatures.py +0 -0
  48. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/cli/commands/tool_servers.py +0 -0
  49. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/cli/commands/top.py +0 -0
  50. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/cli/commands/traces.py +0 -0
  51. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/cli/commands/valsets.py +0 -0
  52. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/cli/credentials.py +0 -0
  53. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/cli/deprecation.py +0 -0
  54. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/cli/pagination.py +0 -0
  55. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/cli/shell.py +0 -0
  56. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/cli/trace_render.py +0 -0
  57. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/cli/tree_help.py +0 -0
  58. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/cli/verbs.py +0 -0
  59. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/client.py +0 -0
  60. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/configuration.py +0 -0
  61. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/context.py +0 -0
  62. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/dataset_sync.py +0 -0
  63. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/datasets.py +0 -0
  64. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/decorators.py +0 -0
  65. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/eval_handler.py +0 -0
  66. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/execution.py +0 -0
  67. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/exporter.py +0 -0
  68. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/exporter_http.py +0 -0
  69. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/helpers.py +0 -0
  70. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/identity.py +0 -0
  71. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/imaging.py +0 -0
  72. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/ir/__init__.py +0 -0
  73. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/ir/decode.py +0 -0
  74. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/ir/encode.py +0 -0
  75. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/ir/hash.py +0 -0
  76. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/ir/program.py +0 -0
  77. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/ir/reflection.py +0 -0
  78. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/ir/types.py +0 -0
  79. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/ir/xxh64.py +0 -0
  80. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/lm_configuration.py +0 -0
  81. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/models.py +0 -0
  82. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/optimization.py +0 -0
  83. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/optimizers/__init__.py +0 -0
  84. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/optimizers/gepa.py +0 -0
  85. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/optimizers/gepa_callback.py +0 -0
  86. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/optimizers/resume.py +0 -0
  87. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/optimizers/tracker.py +0 -0
  88. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/retry.py +0 -0
  89. {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/traces.py +0 -0
  90. {cmpnd-0.27.2 → cmpnd-0.27.3}/docs/2026-04-02-code-review.md +0 -0
  91. {cmpnd-0.27.2 → cmpnd-0.27.3}/docs/cli.md +0 -0
  92. {cmpnd-0.27.2 → cmpnd-0.27.3}/docs/client.md +0 -0
  93. {cmpnd-0.27.2 → cmpnd-0.27.3}/docs/custom-proposer.md +0 -0
  94. {cmpnd-0.27.2 → cmpnd-0.27.3}/docs/plans/2026-04-05-train-val-dataset-capture-design.md +0 -0
  95. {cmpnd-0.27.2 → cmpnd-0.27.3}/docs/plans/2026-04-05-train-val-dataset-capture.md +0 -0
  96. {cmpnd-0.27.2 → cmpnd-0.27.3}/docs/reading-traces.md +0 -0
  97. {cmpnd-0.27.2 → cmpnd-0.27.3}/examples/generalizing_proposer.py +0 -0
  98. {cmpnd-0.27.2 → cmpnd-0.27.3}/examples/local_ollama.py +0 -0
  99. {cmpnd-0.27.2 → cmpnd-0.27.3}/ingest/LINEAGE.md +0 -0
  100. {cmpnd-0.27.2 → cmpnd-0.27.3}/ingest/README.md +0 -0
  101. {cmpnd-0.27.2 → cmpnd-0.27.3}/ingest/cmpnd_ingest/__init__.py +0 -0
  102. {cmpnd-0.27.2 → cmpnd-0.27.3}/ingest/cmpnd_ingest/bodies.py +0 -0
  103. {cmpnd-0.27.2 → cmpnd-0.27.3}/ingest/cmpnd_ingest/exporter_http.py +0 -0
  104. {cmpnd-0.27.2 → cmpnd-0.27.3}/ingest/cmpnd_ingest/protocols.py +0 -0
  105. {cmpnd-0.27.2 → cmpnd-0.27.3}/ingest/cmpnd_ingest/py.typed +0 -0
  106. {cmpnd-0.27.2 → cmpnd-0.27.3}/ingest/cmpnd_ingest/request_log.py +0 -0
  107. {cmpnd-0.27.2 → cmpnd-0.27.3}/ingest/cmpnd_ingest/retry.py +0 -0
  108. {cmpnd-0.27.2 → cmpnd-0.27.3}/ingest/pyproject.toml +0 -0
  109. {cmpnd-0.27.2 → cmpnd-0.27.3}/scripts/audit-sdist.sh +0 -0
  110. {cmpnd-0.27.2 → cmpnd-0.27.3}/scripts/capture_review_run.py +0 -0
  111. {cmpnd-0.27.2 → cmpnd-0.27.3}/scripts/check_release_artifacts.py +0 -0
  112. {cmpnd-0.27.2 → cmpnd-0.27.3}/scripts/fixtures/review_run.json.gz +0 -0
  113. {cmpnd-0.27.2 → cmpnd-0.27.3}/scripts/gepa_resume_bug.py +0 -0
  114. {cmpnd-0.27.2 → cmpnd-0.27.3}/scripts/release-stress.sh +0 -0
  115. {cmpnd-0.27.2 → cmpnd-0.27.3}/scripts/require_version_bump.py +0 -0
  116. {cmpnd-0.27.2 → cmpnd-0.27.3}/scripts/seed_review_run.py +0 -0
  117. {cmpnd-0.27.2 → cmpnd-0.27.3}/scripts/test-in-linux.sh +0 -0
  118. {cmpnd-0.27.2 → cmpnd-0.27.3}/stubs/dspy/__init__.pyi +0 -0
  119. {cmpnd-0.27.2 → cmpnd-0.27.3}/stubs/dspy/primitives/__init__.pyi +0 -0
  120. {cmpnd-0.27.2 → cmpnd-0.27.3}/stubs/dspy/primitives/prediction.pyi +0 -0
  121. {cmpnd-0.27.2 → cmpnd-0.27.3}/stubs/dspy/utils/__init__.pyi +0 -0
  122. {cmpnd-0.27.2 → cmpnd-0.27.3}/stubs/dspy/utils/callback.pyi +0 -0
  123. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/__init__.py +0 -0
  124. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/_exporter_helpers.py +0 -0
  125. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/_fake_clock.py +0 -0
  126. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/_identity_helpers.py +0 -0
  127. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/conftest.py +0 -0
  128. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/__init__.py +0 -0
  129. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/committee_task.py +0 -0
  130. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/conftest.py +0 -0
  131. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/data/committee_train.ndjson.gz +0 -0
  132. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/data/committee_val.ndjson.gz +0 -0
  133. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/fake_lm.py +0 -0
  134. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/helpers.py +0 -0
  135. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/target_capabilities.py +0 -0
  136. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/test_assessments_cli.py +0 -0
  137. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/test_capture_prompts.py +0 -0
  138. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/test_composite_module.py +0 -0
  139. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/test_dataset_auto_creation.py +0 -0
  140. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/test_deploy_execute.py +0 -0
  141. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/test_deploy_programs.py +0 -0
  142. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/test_edge_cases.py +0 -0
  143. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/test_example_hash.py +0 -0
  144. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/test_gepa_optimization.py +0 -0
  145. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/test_interpreter_spans.py +0 -0
  146. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/test_lm_accuracy.py +0 -0
  147. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/test_module_tracing.py +0 -0
  148. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/test_optimization_lineage.py +0 -0
  149. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/test_optimization_metric_identity.py +0 -0
  150. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/test_optimize_failures.py +0 -0
  151. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/test_optimize_loop.py +0 -0
  152. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/test_optimize_review_committee.py +0 -0
  153. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/test_optimize_server_owned_completion.py +0 -0
  154. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/test_parallel_export.py +0 -0
  155. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/test_project_attribution.py +0 -0
  156. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/test_react_tool_spans.py +0 -0
  157. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/test_save_load.py +0 -0
  158. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/test_trace_structure.py +0 -0
  159. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/test_user_attribution.py +0 -0
  160. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/test_valsets_cli.py +0 -0
  161. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/identity_vectors/01_predict_qa.json +0 -0
  162. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/identity_vectors/02_predict_renamed_field.json +0 -0
  163. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/identity_vectors/03_chain_of_thought_qa.json +0 -0
  164. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/identity_vectors/04_predict_richer_types.json +0 -0
  165. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/identity_vectors/05_composite_two_predicts.json +0 -0
  166. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/identity_vectors/06_multichaincomparison.json +0 -0
  167. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/identity_vectors/07_program_of_thought.json +0 -0
  168. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/identity_vectors/08_opaque_module.json +0 -0
  169. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/identity_vectors/09_predict_pydantic_field.json +0 -0
  170. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/identity_vectors/10_react.json +0 -0
  171. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/identity_vectors/11_refine.json +0 -0
  172. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/identity_vectors/12_retrieve.json +0 -0
  173. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/identity_vectors/13_knn.json +0 -0
  174. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/identity_vectors/14_parallel.json +0 -0
  175. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/identity_vectors/README.md +0 -0
  176. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/ingest_conformance.py +0 -0
  177. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/integration/__init__.py +0 -0
  178. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/integration/authored_optimize/judge.py +0 -0
  179. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/integration/authored_optimize/optimize_hosted.py +0 -0
  180. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/integration/conftest.py +0 -0
  181. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/integration/hosted_optimize_probe.py +0 -0
  182. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/integration/optimization_run_probe.py +0 -0
  183. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/integration/test_cli_deploy_smoketest.py +0 -0
  184. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/integration/test_logs_renders_traces.py +0 -0
  185. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/integration/test_optimization_run.py +0 -0
  186. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/integration/test_optimizing_progress.py +0 -0
  187. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/integration/test_unhappy_path.py +0 -0
  188. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/load/__init__.py +0 -0
  189. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/load/__main__.py +0 -0
  190. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/load/drain.py +0 -0
  191. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/load/ingest.py +0 -0
  192. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/load/latency_transport.py +0 -0
  193. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/load/rlm_shape.py +0 -0
  194. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/load/sweep.py +0 -0
  195. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/load/test_rlm_shape.py +0 -0
  196. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/parity/__init__.py +0 -0
  197. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/parity/conftest.py +0 -0
  198. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/parity/seed_parity.py +0 -0
  199. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/parity/test_auth.py +0 -0
  200. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/parity/test_datasets.py +0 -0
  201. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/parity/test_deployments.py +0 -0
  202. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/parity/test_evals.py +0 -0
  203. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/parity/test_health.py +0 -0
  204. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/parity/test_modules.py +0 -0
  205. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/parity/test_optimizations.py +0 -0
  206. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/parity/test_programs.py +0 -0
  207. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/parity/test_signatures.py +0 -0
  208. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/parity/test_trace_streaming.py +0 -0
  209. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/parity/test_traces.py +0 -0
  210. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/stress/__init__.py +0 -0
  211. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/stress/_provider.py +0 -0
  212. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/stress/_reliability.py +0 -0
  213. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/stress/_result.py +0 -0
  214. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/stress/mint_bedrock_token.py +0 -0
  215. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/stress/test_deploy_programs.py +0 -0
  216. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/stress/test_multi_client_stress.py +0 -0
  217. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/stress/test_optimize_client_committee.py +0 -0
  218. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/stress/test_optimize_loop.py +0 -0
  219. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/stress/test_optimize_stress.py +0 -0
  220. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/stress/test_provider_roles.py +0 -0
  221. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/stress/test_real_provider_twins.py +0 -0
  222. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/stress/test_reliability.py +0 -0
  223. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/stress/test_reliability_classify.py +0 -0
  224. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/stress/test_rlm_wasm.py +0 -0
  225. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_assessments_api.py +0 -0
  226. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_authored.py +0 -0
  227. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_authored_bundle_collisions.py +0 -0
  228. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_authored_metric_contract.py +0 -0
  229. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_authored_named_models.py +0 -0
  230. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_authored_optimize.py +0 -0
  231. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_callback.py +0 -0
  232. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_candidate_lineage.py +0 -0
  233. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_cli/__init__.py +0 -0
  234. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_cli/conftest.py +0 -0
  235. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_cli/test_admin.py +0 -0
  236. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_cli/test_api_client.py +0 -0
  237. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_cli/test_assessments.py +0 -0
  238. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_cli/test_auth.py +0 -0
  239. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_cli/test_credentials.py +0 -0
  240. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_cli/test_datasets.py +0 -0
  241. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_cli/test_deployments.py +0 -0
  242. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_cli/test_evals.py +0 -0
  243. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_cli/test_login.py +0 -0
  244. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_cli/test_main.py +0 -0
  245. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_cli/test_optimizations.py +0 -0
  246. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_cli/test_org.py +0 -0
  247. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_cli/test_pagination.py +0 -0
  248. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_cli/test_parity.py +0 -0
  249. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_cli/test_programs.py +0 -0
  250. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_cli/test_provider_credentials.py +0 -0
  251. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_cli/test_schema_sample.py +0 -0
  252. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_cli/test_shell.py +0 -0
  253. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_cli/test_signatures.py +0 -0
  254. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_cli/test_tool_servers.py +0 -0
  255. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_cli/test_top.py +0 -0
  256. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_cli/test_traces.py +0 -0
  257. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_cli/test_traces_search_wire.py +0 -0
  258. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_cli/test_tree_help.py +0 -0
  259. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_cli/test_valsets.py +0 -0
  260. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_client.py +0 -0
  261. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_clustering/__init__.py +0 -0
  262. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_clustering/conftest.py +0 -0
  263. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_clustering/test_edge_cases.py +0 -0
  264. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_clustering/test_hash_consistency.py +0 -0
  265. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_clustering/test_type_conversion.py +0 -0
  266. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_cmp551_grouping_identity.py +0 -0
  267. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_configuration.py +0 -0
  268. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_context.py +0 -0
  269. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_context_propagation.py +0 -0
  270. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_dataset_polling.py +0 -0
  271. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_dataset_sync.py +0 -0
  272. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_datasets.py +0 -0
  273. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_decorators.py +0 -0
  274. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_deploy.py +0 -0
  275. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_eval.py +0 -0
  276. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_eval_example_attribution.py +0 -0
  277. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_eval_start.py +0 -0
  278. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_eval_trace_grouping.py +0 -0
  279. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_exception_paths.py +0 -0
  280. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_execute.py +0 -0
  281. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_exporter.py +0 -0
  282. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_exporter_queue.py +0 -0
  283. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_exporter_real.py +0 -0
  284. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_fake_lm.py +0 -0
  285. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_gepa_callback.py +0 -0
  286. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_gepa_integration.py +0 -0
  287. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_gepa_resume.py +0 -0
  288. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_identity.py +0 -0
  289. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_imaging.py +0 -0
  290. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_import_weight.py +0 -0
  291. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_ingest_conformance.py +0 -0
  292. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_ingest_second_producer.py +0 -0
  293. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_integration.py +0 -0
  294. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_interpreter_callback.py +0 -0
  295. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_ir_encoder_properties.py +0 -0
  296. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_ir_golden_vectors.py +0 -0
  297. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_ir_roundtrip.py +0 -0
  298. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_lm_configuration.py +0 -0
  299. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_lm_usage_shapes.py +0 -0
  300. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_mcp_tools.py +0 -0
  301. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_models.py +0 -0
  302. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_optimization_progress.py +0 -0
  303. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_optimization_start.py +0 -0
  304. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_optimize.py +0 -0
  305. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_optimize_budget.py +0 -0
  306. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_optimize_lift.py +0 -0
  307. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_optimize_lms.py +0 -0
  308. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_optimize_proposer.py +0 -0
  309. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_optimize_reflection_lm.py +0 -0
  310. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_optimize_terminal_failure.py +0 -0
  311. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_packaging.py +0 -0
  312. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_packaging_callable_source.py +0 -0
  313. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_packaging_program_source.py +0 -0
  314. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_poll_until.py +0 -0
  315. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_program_lineage_persist.py +0 -0
  316. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_project_payload.py +0 -0
  317. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_reflector_properties.py +0 -0
  318. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_reflector_smoke.py +0 -0
  319. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_release_stress_script.py +0 -0
  320. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_repro_reactv2_bugs.py +0 -0
  321. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_require_version_bump.py +0 -0
  322. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_retry.py +0 -0
  323. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_reversible_failures.py +0 -0
  324. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_rlm_patch.py +0 -0
  325. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_schemas.py +0 -0
  326. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_seed_review_run.py +0 -0
  327. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_span_sent_once.py +0 -0
  328. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_trace_render.py +0 -0
  329. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_traces_read.py +0 -0
  330. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_tracked_gepa.py +0 -0
  331. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_tracker_enrich.py +0 -0
  332. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_truncation.py +0 -0
  333. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_valsets_api.py +0 -0
  334. {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_xxh64.py +0 -0
  335. {cmpnd-0.27.2 → cmpnd-0.27.3}/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.3
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
@@ -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:
@@ -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
@@ -722,29 +722,11 @@ def _resolve_an_mcp_tool(
722
722
 
723
723
 
724
724
  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
- """
725
+ """Generate a callable declaration with the registered tool's pinned signature."""
743
726
  body_lines = [f"def {signature.name}({signature.rendered}) -> dict:"]
744
727
  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(" )")
728
+ body_lines.append(" raise RuntimeError('this MCP tool requires a worker with registered tool support')")
729
+ body_lines.append(f"{signature.name} = __import__('cmpnd')._mcp_tool({signature.name})")
748
730
  return "\n".join(body_lines)
749
731
 
750
732
 
@@ -761,7 +743,7 @@ def _docstring_lines(description: str) -> list[str]:
761
743
  lines.append(f" {line}".rstrip())
762
744
  lines.append("")
763
745
  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.")
746
+ lines.append(" through a scoped capability without exposing a credential to the program.")
765
747
  lines.append("")
766
748
  lines.append(' Returns an MCP tool result: {"content": [{"type": "text", "text": ...}, ...],')
767
749
  lines.append(' "isError": bool}. Read the text parts.')
@@ -796,6 +778,43 @@ def module_tools(module: dspy.Module) -> dict[str, typing.Any]:
796
778
  return dict(tools) if isinstance(tools, dict) else {}
797
779
 
798
780
 
781
+ def declared_module_tools(module: dspy.Module) -> dict[str, typing.Any]:
782
+ """Collect tools throughout a module and refuse ambiguous worker routing names."""
783
+ import dspy
784
+
785
+ from .mcp_servers import is_bare_mcp_tool, mcp_tool_reference
786
+
787
+ collected: dict[str, typing.Any] = {}
788
+ pending: list[typing.Any] = [module]
789
+ visited: set[int] = set()
790
+ while pending:
791
+ member = pending.pop()
792
+ if id(member) in visited:
793
+ continue
794
+ visited.add(id(member))
795
+ if isinstance(member, dict):
796
+ pending.extend(member.values())
797
+ continue
798
+ if isinstance(member, (list, tuple)):
799
+ pending.extend(member)
800
+ continue
801
+ if not isinstance(member, dspy.Module):
802
+ continue
803
+ pending.extend(vars(member).values())
804
+ for name, tool in module_tools(member).items():
805
+ if is_bare_mcp_tool(tool):
806
+ _resolve_an_mcp_tool(name, tool, {})
807
+ previous = collected.get(name)
808
+ if previous is not None and previous is not tool:
809
+ old_ref = mcp_tool_reference(previous)
810
+ new_ref = mcp_tool_reference(tool)
811
+ if old_ref is not None or new_ref is not None:
812
+ if old_ref != new_ref:
813
+ raise ValueError(f"tool name {name!r} refers to different tools in this program")
814
+ collected[name] = tool
815
+ return collected
816
+
817
+
799
818
  def generate_program_source(
800
819
  module: dspy.Module,
801
820
  mcp_tools: Mapping[tuple[str, str], typing.Any] | None = None,
@@ -942,10 +961,13 @@ def generate_program_source(
942
961
  tool_names: list[str] = []
943
962
  mcp_declarations: list[dict[str, typing.Any]] = []
944
963
  pinned_mcp_tools = mcp_tools or {}
945
- tools_dict = module_tools(module)
964
+ tools_dict = declared_module_tools(module)
946
965
  if tools_dict:
966
+ from dspy import Tool
967
+
947
968
  from . import mcp_servers
948
969
 
970
+ mcp_bindings: list[tuple[Any, str]] = []
949
971
  for name, tool in tools_dict.items():
950
972
  if name == "finish":
951
973
  continue
@@ -957,6 +979,7 @@ def generate_program_source(
957
979
  # worker maps that name to the token it holds.
958
980
  signature = mcp_servers.synthesized_signature(pinned)
959
981
  lines.append(_generate_mcp_tool_stub(pinned, signature))
982
+ mcp_bindings.append((mcp_servers.mcp_tool_reference(tool), f"dspy.Tool({signature.name})"))
960
983
  mcp_declarations.append(
961
984
  {
962
985
  "tool": signature.name,
@@ -972,6 +995,85 @@ def generate_program_source(
972
995
  _extract_callable_tree(func)
973
996
  tool_names.append(name)
974
997
 
998
+ # A composed constructor refers to its Python bindings, which need not share
999
+ # the registered routing name. Rebuild those bindings from pinned declarations
1000
+ # without carrying the live MCP session or credential into generated source.
1001
+ if is_user_module:
1002
+ bindings: dict[str, str] = {}
1003
+
1004
+ def function_binding(func: Any) -> str:
1005
+ if not inspect.isfunction(func) or not func.__name__.isidentifier():
1006
+ raise ValueError("Cannot reconstruct this tool callable; deploy the authored source file")
1007
+ _extract_callable_tree(func)
1008
+ if func.__name__ not in emitted_sources:
1009
+ raise ValueError("Cannot recover tool source; deploy the authored source file")
1010
+ return func.__name__
1011
+
1012
+ def contains_mcp(value: Any, seen: set[int]) -> bool:
1013
+ if id(value) in seen:
1014
+ return False
1015
+ seen.add(id(value))
1016
+ if mcp_servers.mcp_tool_reference(value) is not None:
1017
+ return True
1018
+ members = value.values() if isinstance(value, dict) else value
1019
+ return isinstance(value, (list, tuple, dict)) and any(contains_mcp(item, seen) for item in members)
1020
+
1021
+ def binding_source(value: Any, ancestors: frozenset[int]) -> str:
1022
+ if id(value) in ancestors:
1023
+ raise ValueError("Cannot reconstruct cyclic MCP tool bindings; deploy the authored source file")
1024
+ ancestors = ancestors | {id(value)}
1025
+ reference = mcp_servers.mcp_tool_reference(value)
1026
+ if reference is not None:
1027
+ for declared, source in mcp_bindings:
1028
+ if reference == declared:
1029
+ return source
1030
+ raise ValueError(
1031
+ "MCP tool binding is not declared by this program; deploy the authored source file"
1032
+ )
1033
+ if isinstance(value, list):
1034
+ return "[" + ", ".join(binding_source(item, ancestors) for item in value) + "]"
1035
+ if isinstance(value, tuple):
1036
+ return "(" + "".join(binding_source(item, ancestors) + ", " for item in value) + ")"
1037
+ if isinstance(value, dict):
1038
+ if not all(isinstance(key, str) for key in value):
1039
+ raise ValueError("MCP tool dictionaries require string keys; deploy the authored source file")
1040
+ return (
1041
+ "{"
1042
+ + ", ".join(f"{key!r}: {binding_source(item, ancestors)}" for key, item in value.items())
1043
+ + "}"
1044
+ )
1045
+ if isinstance(value, Tool):
1046
+ name = function_binding(value.func)
1047
+ if not _is_literal_constant(value.args):
1048
+ raise ValueError("Cannot reconstruct tool arguments; deploy the authored source file")
1049
+ return f"dspy.Tool({name}, name={value.name!r}, desc={value.desc!r}, args={value.args!r})"
1050
+ if callable(value):
1051
+ return function_binding(value)
1052
+ if _is_literal_constant(value):
1053
+ return repr(value)
1054
+ raise ValueError("Cannot reconstruct a binding containing MCP tools; deploy the authored source file")
1055
+
1056
+ for method_name in ("__init__", "forward"):
1057
+ method = getattr(cls, method_name, None)
1058
+ if not inspect.isfunction(method):
1059
+ continue
1060
+ closure = inspect.getclosurevars(method)
1061
+ for name, value in {**closure.globals, **closure.nonlocals}.items():
1062
+ if not contains_mcp(value, set()):
1063
+ continue
1064
+ rendered = binding_source(value, frozenset())
1065
+ if name in bindings and bindings[name] != rendered:
1066
+ raise ValueError(
1067
+ f"MCP tool binding {name!r} has conflicting values; deploy the authored source file"
1068
+ )
1069
+ bindings[name] = rendered
1070
+ if bindings:
1071
+ # Evaluate every declaration before rebinding any name: an author's
1072
+ # alias can itself be another registered tool's routing name.
1073
+ names = ", ".join(bindings)
1074
+ values = ", ".join(bindings.values())
1075
+ lines.append(f"({names},) = ({values},)")
1076
+
975
1077
  # Reconstruct the synthetic ProgramSignature class up-front (when that
976
1078
  # path applies) so its annotation imports land in sig_imports *before*
977
1079
  # we emit the import block below. The class text itself is appended
@@ -1009,6 +1111,9 @@ def generate_program_source(
1009
1111
  extra_kwargs = ""
1010
1112
  if tool_names:
1011
1113
  extra_kwargs = f", tools=[{', '.join(tool_names)}]"
1114
+ max_iters = getattr(module, "max_iters", None)
1115
+ if not is_user_module and isinstance(max_iters, int):
1116
+ extra_kwargs += f", max_iters={max_iters!r}"
1012
1117
 
1013
1118
  module_assigned = False
1014
1119
  if sig is not None:
@@ -605,3 +605,20 @@ A new hand-rolled serializer on any of those paths reopens the hole. `tests/test
605
605
  - [`./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
606
  - The `cmpnd-testing` skill + [`../AGENTS.md`](../AGENTS.md) § "Running Tests & Checks" — patterns for writing callback tests against real databases, including `clear_context()` usage.
607
607
  - [`serve/DB.md`](../../serve/DB.md) — the Postgres schema these payloads land in.
608
+
609
+ ### Registered MCP tools in ReAct
610
+
611
+ `cmpnd.mcp_tools` declares registered tools for ReAct and RLM, including modules
612
+ that contain them. Deployment resolves those declarations against the organization's
613
+ registry and pins their schemas. Bare MCP session closures cannot be packaged.
614
+ Conflicting tools with the same routing name are refused.
615
+ Generated composed programs retain global and closure aliases for registered tools,
616
+ including aliases stored in lists, tuples, or dictionaries. The packager rebuilds
617
+ these bindings from pinned declarations without copying live MCP sessions.
618
+ Generated mixed-tool bindings require ordinary tools to be named Python functions.
619
+ Use authored-source deployment for partials, callable instances, lambdas, or bound methods.
620
+
621
+ The WASM kernel calls tools through a typed worker import. The worker holds the
622
+ scoped tokens and returns the MCP result document. Optional arguments omitted by
623
+ the program remain omitted. ReAct's tool callbacks produce the application span;
624
+ the worker adds transport measurements to that span using its correlation ID.
@@ -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.3"
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)
@@ -34,11 +34,10 @@ on it: what this lane is about is whether a tool call a program makes reaches a
34
34
  server, and nothing here depends on a model having *chosen* to make it. A lane about
35
35
  the choosing needs a real model, which is what ``--real-model`` is for.
36
36
 
37
- # The program must be an RLM
37
+ # ReAct and RLM
38
38
 
39
- The platform refuses MCP tools on anything else, so the tool-calling program a user can
40
- actually deploy is an RLM — which is why this file's shape (a demo, a REPL turn, a
41
- ``SUBMIT``) differs from its sibling's and is not gratuitous.
39
+ ``--tool-module react`` drives the outer kernel import. The default RLM path drives
40
+ its interpreter. Both use the registered tool and scoped authority on every rollout.
42
41
 
43
42
  CMPND_ENDPOINT=... CMPND_API_KEY=... CMPND_DEV_LM_API_HOST=... \\
44
43
  CMPND_TOOL_SERVER_URL=... python hosted_mcp_tool_optimize_probe.py --out result.json
@@ -50,6 +49,7 @@ import argparse
50
49
  import json
51
50
  import os
52
51
  import sys
52
+ import tempfile
53
53
  import time
54
54
  from pathlib import Path
55
55
  from typing import Any
@@ -75,6 +75,19 @@ EXAMPLES: list[dict[str, str]] = [
75
75
  # looking entirely healthy.
76
76
  TOOL = "catalog_answer"
77
77
 
78
+ # The packager replaces this runtime tool with its pinned declaration of the same name.
79
+ catalog_answer: Any = None
80
+
81
+
82
+ class Research(dspy.Module):
83
+ def __init__(self):
84
+ super().__init__()
85
+ self.solve = dspy.ReAct("question -> answer", tools=[catalog_answer], max_iters=1)
86
+
87
+ def forward(self, question):
88
+ return self.solve(question=question)
89
+
90
+
78
91
  # The litellm model both roles use, matching the sibling probe's default so one sealed
79
92
  # credential serves both lanes. Its PREFIX is what the platform files the provider
80
93
  # credential under.
@@ -118,7 +131,7 @@ class ShortAnswer(dspy.Signature):
118
131
  answer: str = dspy.OutputField()
119
132
 
120
133
 
121
- def build_program(tool_server: str, credential: str, timeout_s: float) -> dspy.Module:
134
+ def build_program(tool_server: str, credential: str, timeout_s: float, module_kind: str = "rlm") -> dspy.Module:
122
135
  """The RLM under optimization, with its tools and the turn that calls one.
123
136
 
124
137
  ``cmpnd.mcp_tools`` connects to the server FROM HERE, which is what tells the deploy
@@ -168,6 +181,21 @@ def build_program(tool_server: str, credential: str, timeout_s: float) -> dspy.M
168
181
  # tools.
169
182
  tools = [tool for tool in listed if tool.name == TOOL]
170
183
 
184
+ if module_kind == "react":
185
+ global catalog_answer
186
+ catalog_answer = tools[0]
187
+ program = Research()
188
+ program.solve.react.demos = [
189
+ dspy.Example(
190
+ question=EXAMPLES[0]["question"],
191
+ trajectory="",
192
+ next_thought="Look up the catalogue entry",
193
+ next_tool_name=TOOL,
194
+ next_tool_args={"question": EXAMPLES[0]["question"], "style": "sentence"},
195
+ )
196
+ ]
197
+ return program
198
+
171
199
  # Two turns, not one: the first runs the demo's code and submits. The second exists
172
200
  # only so a turn that failed has somewhere to be retried from, which is what turns a
173
201
  # transient refusal into a slower rollout rather than a failed one.
@@ -221,6 +249,8 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
221
249
  p = argparse.ArgumentParser(
222
250
  description="Optimize a tool-calling program through serve, the optimizer and the executor."
223
251
  )
252
+ p.add_argument("--tool-source", choices=("generated", "authored"), default="generated")
253
+ p.add_argument("--tool-module", choices=("rlm", "react"), default="rlm")
224
254
  p.add_argument("--out", required=True, help="write the ids this run produced here, as JSON")
225
255
  p.add_argument(
226
256
  "--metric-calls",
@@ -265,11 +295,39 @@ def main(argv: list[str] | None = None) -> int:
265
295
 
266
296
  cmpnd.configure(api_key=api_key, endpoint=endpoint, project_tags=["hosted-mcp-tool-optimize-probe"])
267
297
 
268
- program = build_program(args.tool_server, credential, args.tool_server_timeout)
298
+ program = build_program(args.tool_server, credential, args.tool_server_timeout, args.tool_module)
269
299
  started_at = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
270
300
 
271
- print(f"deploying an RLM calling {TOOL} on {args.tool_server} to {endpoint}", flush=True)
272
- deployed = cmpnd.deploy(program, metric=feedback_metric, timeout=600)
301
+ print(f"deploying {args.tool_module} calling {TOOL} on {args.tool_server} to {endpoint}", flush=True)
302
+ if args.tool_source == "authored":
303
+ if args.tool_module != "react":
304
+ raise ValueError("the authored tool probe uses ReAct")
305
+ from cmpnd import authored, deployment
306
+
307
+ demo = dict(getattr(program, "solve").react.demos[0])
308
+ source = "\n".join(
309
+ [
310
+ "import dspy",
311
+ "import cmpnd",
312
+ f"dspy.configure(lm=dspy.LM({MODEL!r}, api_base={os.environ['CMPND_DEV_LM_API_HOST']!r}, cache=False))",
313
+ f"tools = [tool for tool in cmpnd.mcp_tools({args.tool_server!r}) if tool.name == {TOOL!r}]",
314
+ "class Research(dspy.Module):",
315
+ " def __init__(self):",
316
+ " super().__init__()",
317
+ " self.solve = dspy.ReAct('question -> answer', tools=tools, max_iters=1)",
318
+ " def forward(self, question):",
319
+ " return self.solve(question=question)",
320
+ "program = Research()",
321
+ f"program.solve.react.demos = [dspy.Example(**{demo!r})]",
322
+ ]
323
+ )
324
+ with tempfile.TemporaryDirectory(prefix="cmpnd-react-source-") as directory:
325
+ path = Path(directory) / "research.py"
326
+ path.write_text(source)
327
+ prepared = authored.prepare_file(path, api_key=api_key, endpoint=endpoint)
328
+ deployed = deployment.deploy_bundle(prepared.bundle, prepared.facts, timeout=600)
329
+ else:
330
+ deployed = cmpnd.deploy(program, metric=feedback_metric, timeout=600)
273
331
  print(f"deployed {deployed}", flush=True)
274
332
 
275
333
  examples = [dspy.Example(**row).with_inputs("question") for row in EXAMPLES]