cmpnd 0.24.0__tar.gz → 0.25.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (319) hide show
  1. {cmpnd-0.24.0 → cmpnd-0.25.0}/PKG-INFO +4 -2
  2. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/_assessments.py +25 -4
  3. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/cli/commands/assessments.py +19 -3
  4. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/cli/commands/top.py +65 -56
  5. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/cli/shell.py +38 -32
  6. cmpnd-0.25.0/cmpnd/cli/snippet.py +52 -0
  7. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/client.py +19 -3
  8. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/mcp_servers.py +112 -19
  9. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/packaging.py +157 -2
  10. {cmpnd-0.24.0 → cmpnd-0.25.0}/docs/cli.md +30 -3
  11. {cmpnd-0.24.0 → cmpnd-0.25.0}/docs/client.md +8 -4
  12. {cmpnd-0.24.0 → cmpnd-0.25.0}/docs/sdk-instrumentation.md +69 -0
  13. {cmpnd-0.24.0 → cmpnd-0.25.0}/pyproject.toml +15 -7
  14. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/helpers.py +24 -0
  15. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/test_assessments_cli.py +19 -2
  16. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/test_valsets_cli.py +4 -1
  17. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_assessments_api.py +22 -0
  18. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_cli/test_assessments.py +45 -3
  19. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_cli/test_main.py +2 -2
  20. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_cli/test_top.py +137 -0
  21. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_client.py +23 -0
  22. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_mcp_tools.py +264 -0
  23. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_packaging_program_source.py +291 -1
  24. {cmpnd-0.24.0 → cmpnd-0.25.0}/uv.lock +6 -2
  25. {cmpnd-0.24.0 → cmpnd-0.25.0}/.gitignore +0 -0
  26. {cmpnd-0.24.0 → cmpnd-0.25.0}/AGENTS.md +0 -0
  27. {cmpnd-0.24.0 → cmpnd-0.25.0}/CLAUDE.md +0 -0
  28. {cmpnd-0.24.0 → cmpnd-0.25.0}/README.md +0 -0
  29. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/__init__.py +0 -0
  30. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/_exporter_pool.py +0 -0
  31. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/_gepa_patch.py +0 -0
  32. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/_http.py +0 -0
  33. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/_program_patch.py +0 -0
  34. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/_rlm_patch.py +0 -0
  35. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/_timespec.py +0 -0
  36. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/_traces.py +0 -0
  37. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/_valsets.py +0 -0
  38. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/callback.py +0 -0
  39. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/cli/__init__.py +0 -0
  40. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/cli/api_client.py +0 -0
  41. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/cli/commands/__init__.py +0 -0
  42. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/cli/commands/admin.py +0 -0
  43. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/cli/commands/auth.py +0 -0
  44. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/cli/commands/datasets.py +0 -0
  45. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/cli/commands/deployments.py +0 -0
  46. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/cli/commands/evals.py +0 -0
  47. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/cli/commands/login.py +0 -0
  48. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/cli/commands/optimizations.py +0 -0
  49. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/cli/commands/org.py +0 -0
  50. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/cli/commands/programs.py +0 -0
  51. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/cli/commands/provider_credentials.py +0 -0
  52. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/cli/commands/signatures.py +0 -0
  53. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/cli/commands/tool_servers.py +0 -0
  54. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/cli/commands/traces.py +0 -0
  55. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/cli/commands/valsets.py +0 -0
  56. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/cli/credentials.py +0 -0
  57. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/cli/deprecation.py +0 -0
  58. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/cli/pagination.py +0 -0
  59. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/cli/trace_render.py +0 -0
  60. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/cli/tree_help.py +0 -0
  61. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/cli/verbs.py +0 -0
  62. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/configuration.py +0 -0
  63. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/context.py +0 -0
  64. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/dataset_sync.py +0 -0
  65. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/datasets.py +0 -0
  66. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/decorators.py +0 -0
  67. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/deployment.py +0 -0
  68. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/eval_handler.py +0 -0
  69. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/execution.py +0 -0
  70. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/exporter.py +0 -0
  71. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/exporter_http.py +0 -0
  72. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/helpers.py +0 -0
  73. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/identity.py +0 -0
  74. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/imaging.py +0 -0
  75. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/ir/__init__.py +0 -0
  76. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/ir/decode.py +0 -0
  77. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/ir/encode.py +0 -0
  78. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/ir/hash.py +0 -0
  79. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/ir/program.py +0 -0
  80. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/ir/reflection.py +0 -0
  81. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/ir/types.py +0 -0
  82. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/ir/xxh64.py +0 -0
  83. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/models.py +0 -0
  84. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/optimization.py +0 -0
  85. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/optimizers/__init__.py +0 -0
  86. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/optimizers/gepa.py +0 -0
  87. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/optimizers/gepa_callback.py +0 -0
  88. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/optimizers/resume.py +0 -0
  89. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/optimizers/tracker.py +0 -0
  90. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/retry.py +0 -0
  91. {cmpnd-0.24.0 → cmpnd-0.25.0}/cmpnd/traces.py +0 -0
  92. {cmpnd-0.24.0 → cmpnd-0.25.0}/docs/2026-04-02-code-review.md +0 -0
  93. {cmpnd-0.24.0 → cmpnd-0.25.0}/docs/custom-proposer.md +0 -0
  94. {cmpnd-0.24.0 → cmpnd-0.25.0}/docs/plans/2026-04-05-train-val-dataset-capture-design.md +0 -0
  95. {cmpnd-0.24.0 → cmpnd-0.25.0}/docs/plans/2026-04-05-train-val-dataset-capture.md +0 -0
  96. {cmpnd-0.24.0 → cmpnd-0.25.0}/docs/reading-traces.md +0 -0
  97. {cmpnd-0.24.0 → cmpnd-0.25.0}/examples/generalizing_proposer.py +0 -0
  98. {cmpnd-0.24.0 → cmpnd-0.25.0}/examples/local_ollama.py +0 -0
  99. {cmpnd-0.24.0 → cmpnd-0.25.0}/ingest/LINEAGE.md +0 -0
  100. {cmpnd-0.24.0 → cmpnd-0.25.0}/ingest/README.md +0 -0
  101. {cmpnd-0.24.0 → cmpnd-0.25.0}/ingest/cmpnd_ingest/__init__.py +0 -0
  102. {cmpnd-0.24.0 → cmpnd-0.25.0}/ingest/cmpnd_ingest/bodies.py +0 -0
  103. {cmpnd-0.24.0 → cmpnd-0.25.0}/ingest/cmpnd_ingest/exporter_http.py +0 -0
  104. {cmpnd-0.24.0 → cmpnd-0.25.0}/ingest/cmpnd_ingest/protocols.py +0 -0
  105. {cmpnd-0.24.0 → cmpnd-0.25.0}/ingest/cmpnd_ingest/py.typed +0 -0
  106. {cmpnd-0.24.0 → cmpnd-0.25.0}/ingest/cmpnd_ingest/request_log.py +0 -0
  107. {cmpnd-0.24.0 → cmpnd-0.25.0}/ingest/cmpnd_ingest/retry.py +0 -0
  108. {cmpnd-0.24.0 → cmpnd-0.25.0}/ingest/pyproject.toml +0 -0
  109. {cmpnd-0.24.0 → cmpnd-0.25.0}/scripts/audit-sdist.sh +0 -0
  110. {cmpnd-0.24.0 → cmpnd-0.25.0}/scripts/capture_review_run.py +0 -0
  111. {cmpnd-0.24.0 → cmpnd-0.25.0}/scripts/check_release_artifacts.py +0 -0
  112. {cmpnd-0.24.0 → cmpnd-0.25.0}/scripts/fixtures/review_run.json.gz +0 -0
  113. {cmpnd-0.24.0 → cmpnd-0.25.0}/scripts/gepa_resume_bug.py +0 -0
  114. {cmpnd-0.24.0 → cmpnd-0.25.0}/scripts/require_version_bump.py +0 -0
  115. {cmpnd-0.24.0 → cmpnd-0.25.0}/scripts/seed_review_run.py +0 -0
  116. {cmpnd-0.24.0 → cmpnd-0.25.0}/scripts/test-in-linux.sh +0 -0
  117. {cmpnd-0.24.0 → cmpnd-0.25.0}/stubs/dspy/__init__.pyi +0 -0
  118. {cmpnd-0.24.0 → cmpnd-0.25.0}/stubs/dspy/primitives/__init__.pyi +0 -0
  119. {cmpnd-0.24.0 → cmpnd-0.25.0}/stubs/dspy/primitives/prediction.pyi +0 -0
  120. {cmpnd-0.24.0 → cmpnd-0.25.0}/stubs/dspy/utils/__init__.pyi +0 -0
  121. {cmpnd-0.24.0 → cmpnd-0.25.0}/stubs/dspy/utils/callback.pyi +0 -0
  122. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/__init__.py +0 -0
  123. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/_exporter_helpers.py +0 -0
  124. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/_fake_clock.py +0 -0
  125. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/_identity_helpers.py +0 -0
  126. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/conftest.py +0 -0
  127. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/__init__.py +0 -0
  128. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/committee_task.py +0 -0
  129. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/conftest.py +0 -0
  130. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/data/committee_train.ndjson.gz +0 -0
  131. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/data/committee_val.ndjson.gz +0 -0
  132. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/fake_lm.py +0 -0
  133. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/target_capabilities.py +0 -0
  134. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/test_capture_prompts.py +0 -0
  135. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/test_composite_module.py +0 -0
  136. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/test_dataset_auto_creation.py +0 -0
  137. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/test_deploy_execute.py +0 -0
  138. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/test_deploy_programs.py +0 -0
  139. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/test_edge_cases.py +0 -0
  140. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/test_example_hash.py +0 -0
  141. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/test_gepa_optimization.py +0 -0
  142. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/test_interpreter_spans.py +0 -0
  143. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/test_lm_accuracy.py +0 -0
  144. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/test_module_tracing.py +0 -0
  145. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/test_optimization_lineage.py +0 -0
  146. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/test_optimization_metric_identity.py +0 -0
  147. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/test_optimize_failures.py +0 -0
  148. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/test_optimize_loop.py +0 -0
  149. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/test_optimize_review_committee.py +0 -0
  150. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/test_optimize_server_owned_completion.py +0 -0
  151. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/test_parallel_export.py +0 -0
  152. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/test_project_attribution.py +0 -0
  153. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/test_react_tool_spans.py +0 -0
  154. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/test_save_load.py +0 -0
  155. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/test_trace_structure.py +0 -0
  156. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/test_traces_read.py +0 -0
  157. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/e2e/test_user_attribution.py +0 -0
  158. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/identity_vectors/01_predict_qa.json +0 -0
  159. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/identity_vectors/02_predict_renamed_field.json +0 -0
  160. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/identity_vectors/03_chain_of_thought_qa.json +0 -0
  161. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/identity_vectors/04_predict_richer_types.json +0 -0
  162. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/identity_vectors/05_composite_two_predicts.json +0 -0
  163. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/identity_vectors/06_multichaincomparison.json +0 -0
  164. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/identity_vectors/07_program_of_thought.json +0 -0
  165. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/identity_vectors/08_opaque_module.json +0 -0
  166. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/identity_vectors/09_predict_pydantic_field.json +0 -0
  167. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/identity_vectors/10_react.json +0 -0
  168. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/identity_vectors/11_refine.json +0 -0
  169. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/identity_vectors/12_retrieve.json +0 -0
  170. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/identity_vectors/13_knn.json +0 -0
  171. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/identity_vectors/14_parallel.json +0 -0
  172. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/identity_vectors/README.md +0 -0
  173. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/ingest_conformance.py +0 -0
  174. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/integration/__init__.py +0 -0
  175. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/integration/conftest.py +0 -0
  176. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/integration/hosted_optimize_probe.py +0 -0
  177. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/integration/optimization_run_probe.py +0 -0
  178. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/integration/test_cli_deploy_smoketest.py +0 -0
  179. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/integration/test_logs_renders_traces.py +0 -0
  180. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/integration/test_optimization_run.py +0 -0
  181. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/integration/test_optimizing_progress.py +0 -0
  182. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/integration/test_unhappy_path.py +0 -0
  183. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/load/__init__.py +0 -0
  184. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/load/__main__.py +0 -0
  185. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/load/drain.py +0 -0
  186. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/load/ingest.py +0 -0
  187. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/load/latency_transport.py +0 -0
  188. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/load/rlm_shape.py +0 -0
  189. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/load/sweep.py +0 -0
  190. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/load/test_rlm_shape.py +0 -0
  191. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/parity/__init__.py +0 -0
  192. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/parity/conftest.py +0 -0
  193. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/parity/seed_parity.py +0 -0
  194. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/parity/test_auth.py +0 -0
  195. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/parity/test_datasets.py +0 -0
  196. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/parity/test_deployments.py +0 -0
  197. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/parity/test_evals.py +0 -0
  198. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/parity/test_health.py +0 -0
  199. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/parity/test_modules.py +0 -0
  200. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/parity/test_optimizations.py +0 -0
  201. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/parity/test_programs.py +0 -0
  202. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/parity/test_signatures.py +0 -0
  203. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/parity/test_trace_streaming.py +0 -0
  204. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/parity/test_traces.py +0 -0
  205. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/stress/__init__.py +0 -0
  206. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/stress/_provider.py +0 -0
  207. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/stress/_reliability.py +0 -0
  208. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/stress/_result.py +0 -0
  209. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/stress/mint_bedrock_token.py +0 -0
  210. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/stress/test_deploy_programs.py +0 -0
  211. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/stress/test_multi_client_stress.py +0 -0
  212. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/stress/test_optimize_client_committee.py +0 -0
  213. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/stress/test_optimize_loop.py +0 -0
  214. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/stress/test_optimize_loop_wasm.py +0 -0
  215. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/stress/test_optimize_stress.py +0 -0
  216. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/stress/test_provider_roles.py +0 -0
  217. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/stress/test_real_provider_twins.py +0 -0
  218. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/stress/test_reliability.py +0 -0
  219. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/stress/test_reliability_classify.py +0 -0
  220. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/stress/test_rlm_wasm.py +0 -0
  221. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/stress/test_trace_search.py +0 -0
  222. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_callback.py +0 -0
  223. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_candidate_lineage.py +0 -0
  224. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_cli/__init__.py +0 -0
  225. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_cli/conftest.py +0 -0
  226. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_cli/test_admin.py +0 -0
  227. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_cli/test_api_client.py +0 -0
  228. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_cli/test_auth.py +0 -0
  229. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_cli/test_credentials.py +0 -0
  230. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_cli/test_datasets.py +0 -0
  231. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_cli/test_deployments.py +0 -0
  232. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_cli/test_evals.py +0 -0
  233. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_cli/test_login.py +0 -0
  234. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_cli/test_optimizations.py +0 -0
  235. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_cli/test_org.py +0 -0
  236. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_cli/test_pagination.py +0 -0
  237. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_cli/test_parity.py +0 -0
  238. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_cli/test_programs.py +0 -0
  239. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_cli/test_provider_credentials.py +0 -0
  240. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_cli/test_schema_sample.py +0 -0
  241. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_cli/test_shell.py +0 -0
  242. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_cli/test_signatures.py +0 -0
  243. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_cli/test_tool_servers.py +0 -0
  244. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_cli/test_traces.py +0 -0
  245. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_cli/test_traces_search_wire.py +0 -0
  246. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_cli/test_tree_help.py +0 -0
  247. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_cli/test_valsets.py +0 -0
  248. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_clustering/__init__.py +0 -0
  249. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_clustering/conftest.py +0 -0
  250. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_clustering/test_edge_cases.py +0 -0
  251. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_clustering/test_hash_consistency.py +0 -0
  252. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_clustering/test_type_conversion.py +0 -0
  253. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_cmp551_grouping_identity.py +0 -0
  254. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_configuration.py +0 -0
  255. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_context.py +0 -0
  256. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_context_propagation.py +0 -0
  257. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_dataset_polling.py +0 -0
  258. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_dataset_sync.py +0 -0
  259. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_datasets.py +0 -0
  260. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_decorators.py +0 -0
  261. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_deploy.py +0 -0
  262. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_deploy_mcp_tool_references.py +0 -0
  263. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_eval.py +0 -0
  264. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_eval_example_attribution.py +0 -0
  265. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_eval_start.py +0 -0
  266. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_eval_trace_grouping.py +0 -0
  267. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_exception_paths.py +0 -0
  268. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_execute.py +0 -0
  269. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_exporter.py +0 -0
  270. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_exporter_queue.py +0 -0
  271. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_exporter_real.py +0 -0
  272. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_fake_lm.py +0 -0
  273. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_gepa_callback.py +0 -0
  274. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_gepa_integration.py +0 -0
  275. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_gepa_resume.py +0 -0
  276. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_identity.py +0 -0
  277. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_imaging.py +0 -0
  278. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_import_weight.py +0 -0
  279. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_ingest_conformance.py +0 -0
  280. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_ingest_second_producer.py +0 -0
  281. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_integration.py +0 -0
  282. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_interpreter_callback.py +0 -0
  283. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_ir_encoder_properties.py +0 -0
  284. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_ir_golden_vectors.py +0 -0
  285. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_ir_roundtrip.py +0 -0
  286. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_lm_usage_shapes.py +0 -0
  287. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_models.py +0 -0
  288. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_optimization_progress.py +0 -0
  289. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_optimization_start.py +0 -0
  290. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_optimize.py +0 -0
  291. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_optimize_budget.py +0 -0
  292. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_optimize_lift.py +0 -0
  293. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_optimize_lms.py +0 -0
  294. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_optimize_proposer.py +0 -0
  295. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_optimize_reflection_lm.py +0 -0
  296. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_optimize_terminal_failure.py +0 -0
  297. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_packaging.py +0 -0
  298. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_packaging_callable_source.py +0 -0
  299. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_packaging_mcp_tools.py +0 -0
  300. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_poll_until.py +0 -0
  301. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_program_lineage_persist.py +0 -0
  302. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_project_payload.py +0 -0
  303. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_reflector_properties.py +0 -0
  304. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_reflector_smoke.py +0 -0
  305. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_repro_reactv2_bugs.py +0 -0
  306. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_require_version_bump.py +0 -0
  307. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_retry.py +0 -0
  308. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_reversible_failures.py +0 -0
  309. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_rlm_patch.py +0 -0
  310. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_schemas.py +0 -0
  311. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_seed_review_run.py +0 -0
  312. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_trace_render.py +0 -0
  313. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_traces_read.py +0 -0
  314. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_tracked_gepa.py +0 -0
  315. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_tracker_enrich.py +0 -0
  316. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_truncation.py +0 -0
  317. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_valsets_api.py +0 -0
  318. {cmpnd-0.24.0 → cmpnd-0.25.0}/tests/test_xxh64.py +0 -0
  319. {cmpnd-0.24.0 → cmpnd-0.25.0}/todo.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cmpnd
3
- Version: 0.24.0
3
+ Version: 0.25.0
4
4
  Summary: DSPy observability and deployment SDK for cmpnd
5
5
  Project-URL: Homepage, https://cmpnd.ai
6
6
  Author-email: cmpnd <hello@cmpnd.ai>
@@ -22,6 +22,7 @@ Requires-Dist: httpx>=0.27.0
22
22
  Requires-Dist: ipython>=8.20
23
23
  Requires-Dist: openai<2
24
24
  Requires-Dist: pydantic>=2.0
25
+ Requires-Dist: typing-extensions>=4.1.0
25
26
  Requires-Dist: xxhash>=3.0.0
26
27
  Provides-Extra: dev
27
28
  Requires-Dist: basedpyright>=1.39; extra == 'dev'
@@ -41,7 +42,8 @@ Requires-Dist: cmpnd-fake-provider; (python_version >= '3.11') and extra == 'fak
41
42
  Provides-Extra: images
42
43
  Requires-Dist: pillow>=10.0; extra == 'images'
43
44
  Provides-Extra: mcp
44
- Requires-Dist: mcp<2,>=1.9; extra == 'mcp'
45
+ Requires-Dist: dspy>=3.3.1; extra == 'mcp'
46
+ Requires-Dist: mcp>=1.9; extra == 'mcp'
45
47
  Description-Content-Type: text/markdown
46
48
 
47
49
  # cmpnd
@@ -46,15 +46,31 @@ def create(
46
46
 
47
47
 
48
48
  def list(
49
- program: str, *, limit: int = 50, offset: int = 0, api_key: str | None = None, endpoint: str | None = None
49
+ program: str | None = None,
50
+ *,
51
+ trace_id: str | None = None,
52
+ limit: int = 50,
53
+ offset: int = 0,
54
+ api_key: str | None = None,
55
+ endpoint: str | None = None,
50
56
  ) -> dict[str, Any]:
51
- """List a program's assessments, newest first.
57
+ """List assessments by program or by trace, newest first.
52
58
 
53
- Returns ``{"assessments": [...], "total": N, "limit", "offset", "has_more"}``.
59
+ Pass exactly one of ``program`` (every assessment of that program) or
60
+ ``trace_id`` (only that trace's assessments). Returns
61
+ ``{"assessments": [...], "total": N, "limit", "offset", "has_more"}``.
54
62
  ``limit`` (max rows) and ``offset`` (skip this many) page the full set — the
55
63
  server clamps ``limit`` to at most 200.
56
64
  """
57
- params = {"program": program, "limit": str(limit), "offset": str(offset)}
65
+ if program is not None and trace_id is not None:
66
+ raise ValueError("pass exactly one of program or trace_id")
67
+ if trace_id is not None:
68
+ scope: dict[str, str] = {"trace_id": trace_id}
69
+ elif program is not None:
70
+ scope = {"program": program}
71
+ else:
72
+ raise ValueError("pass exactly one of program or trace_id")
73
+ params = {**scope, "limit": str(limit), "offset": str(offset)}
58
74
  return _client(api_key, endpoint).get("/api/v1/assessments", params=params)
59
75
 
60
76
 
@@ -63,6 +79,11 @@ def get(assessment_id: str, *, api_key: str | None = None, endpoint: str | None
63
79
  return _client(api_key, endpoint).get(f"/api/v1/assessments/{assessment_id}")
64
80
 
65
81
 
82
+ def delete(assessment_id: str, *, api_key: str | None = None, endpoint: str | None = None) -> None:
83
+ """Delete one assessment. Raises ``ApiError`` (404) if it does not exist."""
84
+ _client(api_key, endpoint).delete(f"/api/v1/assessments/{assessment_id}")
85
+
86
+
66
87
  def _client(api_key: str | None, endpoint: str | None) -> _http.ApiClient:
67
88
  cfg = configuration.get_config()
68
89
  resolved_key = api_key or (cfg.api_key if cfg else None) or os.getenv("CMPND_API_KEY")
@@ -44,8 +44,14 @@ def register(subparsers: _SubParsersAction[ArgumentParser]) -> None:
44
44
  )
45
45
  create.set_defaults(func=handle_create)
46
46
 
47
- ls = sub.add_parser("list", help="List a program's assessments")
48
- ls.add_argument("program", help="Program signature hash (16-hex, as `cmpnd signatures list` shows)")
47
+ ls = sub.add_parser("list", help="List assessments by program or by trace")
48
+ scope = ls.add_mutually_exclusive_group(required=True)
49
+ scope.add_argument(
50
+ "program",
51
+ nargs="?",
52
+ help="Program signature hash (16-hex, as `cmpnd signatures list` shows)",
53
+ )
54
+ scope.add_argument("--trace", dest="trace_id", metavar="TRACE_ID", help="A single trace's assessments")
49
55
  pagination.add_args(ls, with_page_aliases=False, default_limit=50)
50
56
  ls.set_defaults(func=handle_list)
51
57
 
@@ -53,6 +59,10 @@ def register(subparsers: _SubParsersAction[ArgumentParser]) -> None:
53
59
  show.add_argument("id", help="Assessment id")
54
60
  show.set_defaults(func=handle_show)
55
61
 
62
+ delete = sub.add_parser("delete", help="Delete one assessment")
63
+ delete.add_argument("id", help="Assessment id")
64
+ delete.set_defaults(func=handle_delete)
65
+
56
66
 
57
67
  def handle_create(client: api_client.ApiClient, args: Any) -> Any:
58
68
  body: dict[str, Any] = {"trace_id": args.trace_id, "conclusion": args.conclusion}
@@ -64,9 +74,15 @@ def handle_create(client: api_client.ApiClient, args: Any) -> Any:
64
74
 
65
75
 
66
76
  def handle_list(client: api_client.ApiClient, args: Any) -> Any:
67
- params = {"program": args.program, **pagination.wire(args, "limit_offset", as_str=True)}
77
+ scope = {"trace_id": args.trace_id} if args.trace_id else {"program": args.program}
78
+ params = {**scope, **pagination.wire(args, "limit_offset", as_str=True)}
68
79
  return client.get("/api/v1/assessments", params=params)
69
80
 
70
81
 
71
82
  def handle_show(client: api_client.ApiClient, args: Any) -> Any:
72
83
  return client.get(f"/api/v1/assessments/{args.id}")
84
+
85
+
86
+ def handle_delete(client: api_client.ApiClient, args: Any) -> Any:
87
+ client.delete(f"/api/v1/assessments/{args.id}")
88
+ return {"deleted": args.id}
@@ -15,7 +15,7 @@ from argparse import ArgumentParser, ArgumentTypeError, _SubParsersAction
15
15
  from datetime import datetime, timezone
16
16
  from typing import Any
17
17
 
18
- from .. import api_client, pagination
18
+ from .. import api_client, pagination, snippet
19
19
  from . import signatures
20
20
 
21
21
  # --------------------------------------------------------------------------
@@ -782,30 +782,35 @@ def cmd_deploy(client: api_client.ApiClient, args: Any) -> None:
782
782
 
783
783
  configuration.configure(api_key=client._api_key, endpoint=client._endpoint)
784
784
 
785
- namespace: dict[str, Any] = {"__name__": "__cmpnd_deploy__", "__file__": source_label}
786
- try:
787
- exec(compile(source, source_label, "exec"), namespace)
788
- except Exception as exc:
789
- raise api_client.CliError(f"Snippet raised {type(exc).__name__}: {exc}")
790
-
791
- program = namespace.get("program")
792
- if program is None:
793
- # Fallback: pick the last dspy.Module instance defined.
785
+ # The whole packaging step stays inside the registration, not just the exec:
786
+ # both the annotations DSPy resolves and the class sources the packager
787
+ # inlines are read back out of `sys.modules` after the snippet has run.
788
+ with snippet.as_module("__cmpnd_deploy__", source_label) as module:
789
+ namespace: dict[str, Any] = module.__dict__
794
790
  try:
795
- import dspy
796
- except ImportError:
797
- raise api_client.CliError("Snippet did not bind `program`, and dspy isn't importable to scan for one.")
798
- candidates = [v for v in namespace.values() if isinstance(v, dspy.Module)]
799
- if not candidates:
800
- raise api_client.CliError(
801
- "Snippet did not bind `program` to a dspy.Module instance. Add e.g. `program = dspy.Predict(...)`."
802
- )
803
- program = candidates[-1]
791
+ exec(compile(source, source_label, "exec"), namespace)
792
+ except Exception as exc:
793
+ raise api_client.CliError(f"Snippet raised {type(exc).__name__}: {exc}")
794
+
795
+ program = namespace.get("program")
796
+ if program is None:
797
+ # Fallback: pick the last dspy.Module instance defined.
798
+ try:
799
+ import dspy
800
+ except ImportError:
801
+ raise api_client.CliError("Snippet did not bind `program`, and dspy isn't importable to scan for one.")
802
+ candidates = [v for v in namespace.values() if isinstance(v, dspy.Module)]
803
+ if not candidates:
804
+ raise api_client.CliError(
805
+ "Snippet did not bind `program` to a dspy.Module instance. Add e.g. `program = dspy.Predict(...)`."
806
+ )
807
+ program = candidates[-1]
808
+
809
+ metadata = _parse_deploy_metadata(getattr(args, "metadata", None))
804
810
 
805
- metadata = _parse_deploy_metadata(getattr(args, "metadata", None))
811
+ print(_c("deploying…", "dim"), file=sys.stderr)
812
+ deployed = deployment.deploy(program, metadata=metadata)
806
813
 
807
- print(_c("deploying…", "dim"), file=sys.stderr)
808
- deployed = deployment.deploy(program, metadata=metadata)
809
814
  deployment_id = deployed.deployment_id
810
815
  print(deployment_id)
811
816
 
@@ -882,42 +887,46 @@ def cmd_optimize(client: api_client.ApiClient, args: Any) -> Any:
882
887
 
883
888
  configuration.configure(api_key=client._api_key, endpoint=client._endpoint)
884
889
 
885
- namespace: dict[str, Any] = {"__name__": "__cmpnd_optimize__", "__file__": source_label}
886
- try:
887
- exec(compile(source, source_label, "exec"), namespace)
888
- except Exception as exc:
889
- raise api_client.CliError(f"Snippet raised {type(exc).__name__}: {exc}")
890
+ # Registered for the whole run, not just the exec: optimize deploys the
891
+ # program, and packaging reads the snippet's classes back through
892
+ # `sys.modules`. See `cmpnd.cli.snippet`.
893
+ with snippet.as_module("__cmpnd_optimize__", source_label) as module:
894
+ namespace: dict[str, Any] = module.__dict__
895
+ try:
896
+ exec(compile(source, source_label, "exec"), namespace)
897
+ except Exception as exc:
898
+ raise api_client.CliError(f"Snippet raised {type(exc).__name__}: {exc}")
890
899
 
891
- missing = [name for name in ("program", "trainset", "valset", "metric") if namespace.get(name) is None]
892
- if missing:
893
- raise api_client.CliError(
894
- f"Snippet must bind {missing} — e.g. `program = dspy.Predict('q -> a')`, "
895
- "`trainset = [...]`, `valset = [...]`, "
896
- "`def metric(gold, pred, trace, pred_name, pred_trace): ...` returning "
897
- "`dspy.Prediction(score=, feedback=)`."
898
- )
900
+ missing = [name for name in ("program", "trainset", "valset", "metric") if namespace.get(name) is None]
901
+ if missing:
902
+ raise api_client.CliError(
903
+ f"Snippet must bind {missing} — e.g. `program = dspy.Predict('q -> a')`, "
904
+ "`trainset = [...]`, `valset = [...]`, "
905
+ "`def metric(gold, pred, trace, pred_name, pred_trace): ...` returning "
906
+ "`dspy.Prediction(score=, feedback=)`."
907
+ )
899
908
 
900
- print(_c(f"optimizing with {args.optimizer}…", "dim"), file=sys.stderr)
901
- try:
902
- result = optimization.optimize(
903
- namespace["program"],
904
- optimizer=args.optimizer,
905
- trainset=namespace["trainset"],
906
- valset=namespace["valset"],
907
- metric=namespace["metric"],
908
- deployment_id=getattr(args, "deployment_id", None),
909
- # The flag is named for what a command line can carry — a model name,
910
- # never an LM object — and the parameter for what the Python surface
911
- # accepts. Each layer's name says what that layer takes.
912
- reflection_lm=getattr(args, "reflection_model", None),
913
- max_metric_calls=getattr(args, "max_metric_calls", None),
914
- auto=getattr(args, "auto", None),
915
- max_steps=getattr(args, "max_steps", 10),
916
- patience=getattr(args, "patience", 2),
917
- seed=getattr(args, "seed", 0),
918
- )
919
- except (optimization.OptimizeError, optimization.OptimizeFailed, ValueError) as exc:
920
- raise api_client.CliError(str(exc))
909
+ print(_c(f"optimizing with {args.optimizer}…", "dim"), file=sys.stderr)
910
+ try:
911
+ result = optimization.optimize(
912
+ namespace["program"],
913
+ optimizer=args.optimizer,
914
+ trainset=namespace["trainset"],
915
+ valset=namespace["valset"],
916
+ metric=namespace["metric"],
917
+ deployment_id=getattr(args, "deployment_id", None),
918
+ # The flag is named for what a command line can carry — a model name,
919
+ # never an LM object — and the parameter for what the Python surface
920
+ # accepts. Each layer's name says what that layer takes.
921
+ reflection_lm=getattr(args, "reflection_model", None),
922
+ max_metric_calls=getattr(args, "max_metric_calls", None),
923
+ auto=getattr(args, "auto", None),
924
+ max_steps=getattr(args, "max_steps", 10),
925
+ patience=getattr(args, "patience", 2),
926
+ seed=getattr(args, "seed", 0),
927
+ )
928
+ except (optimization.OptimizeError, optimization.OptimizeFailed, ValueError) as exc:
929
+ raise api_client.CliError(str(exc))
921
930
 
922
931
  if getattr(args, "json", False):
923
932
  print(
@@ -33,6 +33,7 @@ from types import SimpleNamespace
33
33
  from typing import Any, Callable
34
34
 
35
35
  from . import api_client, commands, credentials, verbs
36
+ from .snippet import as_module
36
37
 
37
38
  # --------------------------------------------------------------------------
38
39
  # Config + dismissible-tips marker
@@ -508,38 +509,43 @@ def _make_namespace(client: api_client.ApiClient, endpoint: str) -> dict[str, An
508
509
 
509
510
  configuration.configure(api_key=client._api_key, endpoint=client._endpoint)
510
511
 
511
- namespace: dict[str, Any] = {"__name__": "__cmpnd_deploy__", "__file__": label}
512
- try:
513
- exec(compile(snippet, label, "exec"), namespace)
514
- except SystemExit as exc:
515
- # IPython catches SystemExit internally; the stdlib code.interact
516
- # fallback would re-raise and kill the whole REPL. Convert to a
517
- # plain exception so behavior is uniform across REPL flavors.
518
- # The snippet runs with __name__ == "__cmpnd_deploy__", so a
519
- # standard `if __name__ == "__main__":` guard already prevents
520
- # this — call that out so users know how to fix.
521
- raise RuntimeError(
522
- f"Snippet called sys.exit({exc.code!r}). Remove the call or guard it "
523
- 'with `if __name__ == "__main__":` — the deploy runner sets '
524
- "__name__ to '__cmpnd_deploy__', so a __main__ guard short-circuits "
525
- "in deploy but still works when the file is run directly."
526
- ) from None
527
-
528
- program = namespace.get("program")
529
- if program is None:
530
- import dspy
531
-
532
- candidates = [v for v in namespace.values() if isinstance(v, dspy.Module)]
533
- if not candidates:
534
- raise ValueError(
535
- "Snippet did not bind `program` to a dspy.Module instance "
536
- "(e.g. `program = dspy.Predict('q -> a')`)."
537
- )
538
- program = candidates[-1]
539
-
540
- sys.stderr.write(_c("deploying…\n", "dim"))
541
- sys.stderr.flush()
542
- deployed = deployment.deploy(program)
512
+ # Registered for the whole deploy, not just the exec: the annotations
513
+ # DSPy resolves and the class sources the packager inlines are both read
514
+ # back through `sys.modules`. See `cmpnd.cli.snippet`.
515
+ with as_module("__cmpnd_deploy__", label) as module:
516
+ namespace: dict[str, Any] = module.__dict__
517
+ try:
518
+ exec(compile(snippet, label, "exec"), namespace)
519
+ except SystemExit as exc:
520
+ # IPython catches SystemExit internally; the stdlib code.interact
521
+ # fallback would re-raise and kill the whole REPL. Convert to a
522
+ # plain exception so behavior is uniform across REPL flavors.
523
+ # The snippet runs with __name__ == "__cmpnd_deploy__", so a
524
+ # standard `if __name__ == "__main__":` guard already prevents
525
+ # this — call that out so users know how to fix.
526
+ raise RuntimeError(
527
+ f"Snippet called sys.exit({exc.code!r}). Remove the call or guard it "
528
+ 'with `if __name__ == "__main__":` — the deploy runner sets '
529
+ "__name__ to '__cmpnd_deploy__', so a __main__ guard short-circuits "
530
+ "in deploy but still works when the file is run directly."
531
+ ) from None
532
+
533
+ program = namespace.get("program")
534
+ if program is None:
535
+ import dspy
536
+
537
+ candidates = [v for v in namespace.values() if isinstance(v, dspy.Module)]
538
+ if not candidates:
539
+ raise ValueError(
540
+ "Snippet did not bind `program` to a dspy.Module instance "
541
+ "(e.g. `program = dspy.Predict('q -> a')`)."
542
+ )
543
+ program = candidates[-1]
544
+
545
+ sys.stderr.write(_c("deploying…\n", "dim"))
546
+ sys.stderr.flush()
547
+ deployed = deployment.deploy(program)
548
+
543
549
  record = client.get(f"/api/v1/deployments/{deployed.deployment_id}")
544
550
  deps.refresh() # new deployment becomes visible in the registry
545
551
  return Deployment(client, record)
@@ -0,0 +1,52 @@
1
+ """Run a user's deploy/optimize snippet as a real, importable module.
2
+
3
+ A snippet exec'd into a bare dict is not reachable through ``sys.modules``, and
4
+ two things the snippet's own program depends on look it up there:
5
+
6
+ * **Deferred annotations.** With ``from __future__ import annotations`` a field's
7
+ type is stored as the string ``"Genre"``. Whatever resolves it later — DSPy
8
+ rebuilding a signature for ``ChainOfThought``, pydantic rebuilding a model —
9
+ reads the defining module's namespace out of ``sys.modules``. With no module
10
+ there the string stays a ``ForwardRef`` and the deploy is refused with "Field
11
+ types must be types".
12
+ * **Source recovery.** The packager inlines a user-defined annotation type by
13
+ copying its source, and ``inspect`` finds a class's file through
14
+ ``sys.modules[cls.__module__].__file__``. Without the module the type silently
15
+ degrades to a stdlib stand-in.
16
+
17
+ So the snippet gets a module object, registered for as long as the command needs
18
+ it and removed afterwards.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import contextlib
24
+ import sys
25
+ import types
26
+ from collections.abc import Generator
27
+
28
+
29
+ @contextlib.contextmanager
30
+ def as_module(module_name: str, label: str) -> Generator[types.ModuleType]:
31
+ """Yield a module for a snippet to exec into, registered under ``module_name``.
32
+
33
+ ``label`` is the snippet's origin — a path, or ``<stdin>`` — and becomes the
34
+ module's ``__file__``, which is what lets ``inspect`` recover the source of a
35
+ class the snippet declares.
36
+
37
+ The registration is undone on the way out, whether the snippet raised or not,
38
+ so a REPL session or a second command in the same process never inherits the
39
+ previous snippet's module. Anything already registered under the name is put
40
+ back rather than dropped.
41
+ """
42
+ module = types.ModuleType(module_name)
43
+ module.__file__ = label
44
+ displaced = sys.modules.get(module_name)
45
+ sys.modules[module_name] = module
46
+ try:
47
+ yield module
48
+ finally:
49
+ if displaced is None:
50
+ sys.modules.pop(module_name, None)
51
+ else:
52
+ sys.modules[module_name] = displaced
@@ -182,11 +182,22 @@ class Client:
182
182
  endpoint=self._config.endpoint,
183
183
  )
184
184
 
185
- def list_assessments(self, program: str, *, limit: int = 50, offset: int = 0) -> list[dict[str, Any]]:
186
- """A program's assessments, newest first (paged by limit/offset)."""
185
+ def list_assessments(
186
+ self, program: str | None = None, *, trace_id: str | None = None, limit: int = 50, offset: int = 0
187
+ ) -> list[dict[str, Any]]:
188
+ """Assessments newest first (paged by limit/offset).
189
+
190
+ Pass exactly one of ``program`` (all of that program's assessments) or
191
+ ``trace_id`` (one trace's assessments).
192
+ """
187
193
  self._check_active()
188
194
  page = _assessments.list(
189
- program, limit=limit, offset=offset, api_key=self._config.api_key, endpoint=self._config.endpoint
195
+ program,
196
+ trace_id=trace_id,
197
+ limit=limit,
198
+ offset=offset,
199
+ api_key=self._config.api_key,
200
+ endpoint=self._config.endpoint,
190
201
  )
191
202
  return page.get("assessments", [])
192
203
 
@@ -195,6 +206,11 @@ class Client:
195
206
  self._check_active()
196
207
  return _assessments.get(assessment_id, api_key=self._config.api_key, endpoint=self._config.endpoint)
197
208
 
209
+ def delete_assessment(self, assessment_id: str) -> None:
210
+ """Delete one assessment. Raises if it does not exist."""
211
+ self._check_active()
212
+ _assessments.delete(assessment_id, api_key=self._config.api_key, endpoint=self._config.endpoint)
213
+
198
214
  def create_valset(self, program: str, *, name: str | None = None) -> dict[str, Any]:
199
215
  """Create a program-owned valset; name auto-assigned when omitted."""
200
216
  self._check_active()
@@ -52,8 +52,8 @@ import atexit
52
52
  import keyword
53
53
  import logging
54
54
  import threading
55
- from collections.abc import Callable, Coroutine, Mapping
56
- from contextlib import AsyncExitStack
55
+ from collections.abc import AsyncGenerator, Callable, Coroutine, Mapping
56
+ from contextlib import AsyncExitStack, asynccontextmanager
57
57
  from dataclasses import dataclass, field
58
58
  from typing import TYPE_CHECKING, Any, ClassVar
59
59
  from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
@@ -530,35 +530,126 @@ class _SessionHost:
530
530
  self._loop.call_soon_threadsafe(self._loop.stop)
531
531
 
532
532
 
533
+ _MCP_IS_NOT_INSTALLED = (
534
+ "cmpnd.mcp_tools needs the MCP client library, which is an optional extra. "
535
+ "Install it with `pip install 'cmpnd[mcp]'` (or `uv add 'cmpnd[mcp]'`)."
536
+ )
537
+
538
+ # How long a tool call may take to be answered on the server-sent stream. The stream
539
+ # is meant to idle between messages, and the older client took this as a separate
540
+ # `sse_read_timeout` argument that cmpnd left at its default. The newer client folds
541
+ # every timeout into one httpx client, so cmpnd has to name it to keep `timeout_s`
542
+ # from cutting a long-running tool call short. It is NOT what bounds discovery —
543
+ # `_connect` holds the caller's `timeout_s` over initialize and the tool listing,
544
+ # because a budget that governs only the connect phase is no budget at all.
545
+ _TOOL_CALL_READ_TIMEOUT_S = 300.0
546
+
547
+
548
+ def _installed_mcp_version() -> str:
549
+ """The installed MCP client library's version, for a message that has to name it."""
550
+ from importlib import metadata
551
+
552
+ try:
553
+ return metadata.version("mcp")
554
+ except Exception:
555
+ return "unknown version"
556
+
557
+
558
+ @asynccontextmanager
559
+ async def _streamable_http_streams(
560
+ url: str, headers: Mapping[str, str], timeout_s: float
561
+ ) -> AsyncGenerator[tuple[Any, Any]]:
562
+ """Yield the read and write streams of a streamable-HTTP session to `url`.
563
+
564
+ Two generations of the MCP client library reach the same transport by different
565
+ shapes, and this is where the difference stops: `streamable_http_client` takes a
566
+ prebuilt httpx client, `streamablehttp_client` takes `headers` and `timeout`, and
567
+ the older one yields a third value (a session-id getter) that nothing here reads.
568
+ Which spelling exists is not a reliable guide to which shape it has — a client
569
+ library carrying both names yields three values from either — so the streams are
570
+ taken by position rather than unpacked.
571
+
572
+ Raises `ImportError` naming what to install when the library is absent, and a
573
+ different one naming the installed version when it is present but exposes neither
574
+ transport.
575
+ """
576
+ try:
577
+ import mcp.client.streamable_http as transport # pyright: ignore[reportMissingImports]
578
+ except ImportError as exc:
579
+ raise ImportError(_MCP_IS_NOT_INSTALLED) from exc
580
+
581
+ with_prebuilt_client = getattr(transport, "streamable_http_client", None)
582
+ if with_prebuilt_client is not None:
583
+ httpx_module = getattr(transport, "httpx2", None) or getattr(transport, "httpx", None)
584
+ timeout: Any = timeout_s
585
+ if httpx_module is not None:
586
+ timeout = httpx_module.Timeout(timeout_s, read=_TOOL_CALL_READ_TIMEOUT_S)
587
+ http_client = transport.create_mcp_http_client(headers=dict(headers), timeout=timeout)
588
+ # A client the transport did not create is a client the transport does not
589
+ # close, so it is closed here — after the session that uses it.
590
+ async with http_client, with_prebuilt_client(url, http_client=http_client) as streams:
591
+ yield streams[0], streams[1]
592
+ return
593
+
594
+ with_headers = getattr(transport, "streamablehttp_client", None)
595
+ if with_headers is None:
596
+ raise ImportError(
597
+ f"cmpnd.mcp_tools cannot drive the installed MCP client library "
598
+ f"({_installed_mcp_version()}): mcp.client.streamable_http exposes neither "
599
+ "`streamable_http_client` nor `streamablehttp_client`, the two streamable-HTTP "
600
+ "transports cmpnd speaks. Install a version that has one of them."
601
+ )
602
+
603
+ async with with_headers(url, headers=dict(headers), timeout=timeout_s) as streams:
604
+ yield streams[0], streams[1]
605
+
606
+
533
607
  def _connect(url: str, headers: Mapping[str, str], timeout_s: float) -> tuple[Any, list[Any]]:
534
608
  """Open a session to `url` on the shared host loop and list the server's tools.
535
609
 
536
610
  The one function in this module that dials a network, kept separate so everything
537
611
  around it — the refusals, the conversion, the recording — is testable without one.
612
+
613
+ `timeout_s` bounds the whole of that: connecting, the initialize handshake and the
614
+ tool listing. It is held here rather than left to the transport because neither
615
+ client generation bounds those two calls by it — the modern one reads them on a
616
+ stream whose budget is a tool call's, and a caller asking for five seconds is
617
+ asking about the server answering at all, not about one socket read.
538
618
  """
539
619
  host = _SessionHost.shared()
540
620
 
541
621
  async def _open() -> tuple[Any, list[Any]]:
542
622
  try:
543
623
  from mcp import ClientSession # pyright: ignore[reportMissingImports]
544
- from mcp.client.streamable_http import ( # pyright: ignore[reportMissingImports]
545
- streamablehttp_client,
546
- )
547
624
  except ImportError as exc:
548
- raise ImportError(
549
- "cmpnd.mcp_tools needs the MCP client library, which is an optional extra. "
550
- "Install it with `pip install 'cmpnd[mcp]'` (or `uv add 'cmpnd[mcp]'`)."
551
- ) from exc
625
+ raise ImportError(_MCP_IS_NOT_INSTALLED) from exc
626
+
627
+ async def _discover() -> tuple[Any, list[Any], AsyncExitStack]:
628
+ # The stack is opened and, on any failure, closed inside this coroutine:
629
+ # the transport's cancel scopes must be exited by the task that entered
630
+ # them, and on timeout that task is this one rather than its caller.
631
+ stack = AsyncExitStack()
632
+ try:
633
+ read, write = await stack.enter_async_context(_streamable_http_streams(url, headers, timeout_s))
634
+ session = await stack.enter_async_context(ClientSession(read, write))
635
+ await session.initialize()
636
+ listing = await session.list_tools()
637
+ except BaseException:
638
+ await stack.aclose()
639
+ raise
640
+ return session, list(listing.tools), stack
552
641
 
553
- stack = AsyncExitStack()
554
- read, write, _ = await stack.enter_async_context(
555
- streamablehttp_client(url, headers=dict(headers), timeout=timeout_s)
556
- )
557
- session = await stack.enter_async_context(ClientSession(read, write))
558
- await session.initialize()
642
+ try:
643
+ session, tools, stack = await asyncio.wait_for(_discover(), timeout_s)
644
+ except asyncio.TimeoutError as exc:
645
+ raise TimeoutError(
646
+ f"the MCP server at {url} did not finish listing its tools within {timeout_s}s. "
647
+ "cmpnd.mcp_tools connects, initializes and reads the listing within the timeout you "
648
+ "pass it, so that a server which never answers costs you that and no more. Raise "
649
+ "timeout_s if the server is simply slow to start."
650
+ ) from exc
559
651
  host.adopt(stack)
560
- listing = await session.list_tools()
561
- return session, list(listing.tools)
652
+ return session, tools
562
653
 
563
654
  return host.run(_open())
564
655
 
@@ -586,8 +677,10 @@ def mcp_tools(
586
677
  Optional: a server with exactly one credential resolves without a name, and
587
678
  a server with two makes the deploy name one or be refused. Naming one here
588
679
  is also how you rotate a specific key rather than whichever one is there.
589
- timeout_s: How long to wait on the server and, for OAuth, on the token
590
- endpoint.
680
+ timeout_s: How long to wait for the server to answer — connecting,
681
+ initializing and listing its tools — and, for OAuth, on the token
682
+ endpoint. A tool call the returned tools make later is answered on the
683
+ session's own stream, which keeps a longer budget of its own.
591
684
 
592
685
  Returns:
593
686
  A list of `dspy.Tool`, each carrying an `McpToolReference` readable with