cmpnd 0.4.1__tar.gz → 0.4.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. {cmpnd-0.4.1 → cmpnd-0.4.2}/PKG-INFO +1 -1
  2. {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/__init__.py +6 -0
  3. cmpnd-0.4.2/cmpnd/_rlm_patch.py +134 -0
  4. {cmpnd-0.4.1 → cmpnd-0.4.2}/pyproject.toml +1 -1
  5. {cmpnd-0.4.1 → cmpnd-0.4.2}/scripts/seed_review_run.py +118 -0
  6. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/e2e/conftest.py +1 -1
  7. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/stress/test_deploy_programs.py +1 -1
  8. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/stress/test_multi_client_stress.py +1 -1
  9. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/stress/test_real_provider_twins.py +1 -1
  10. cmpnd-0.4.2/tests/test_rlm_patch.py +180 -0
  11. {cmpnd-0.4.1 → cmpnd-0.4.2}/uv.lock +1 -1
  12. {cmpnd-0.4.1 → cmpnd-0.4.2}/.gitignore +0 -0
  13. {cmpnd-0.4.1 → cmpnd-0.4.2}/CLAUDE.md +0 -0
  14. {cmpnd-0.4.1 → cmpnd-0.4.2}/README.md +0 -0
  15. {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/_gepa_patch.py +0 -0
  16. {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/_program_patch.py +0 -0
  17. {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/callback.py +0 -0
  18. {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/cli/__init__.py +0 -0
  19. {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/cli/api_client.py +0 -0
  20. {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/cli/commands/__init__.py +0 -0
  21. {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/cli/commands/admin.py +0 -0
  22. {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/cli/commands/auth.py +0 -0
  23. {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/cli/commands/datasets.py +0 -0
  24. {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/cli/commands/deployments.py +0 -0
  25. {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/cli/commands/evals.py +0 -0
  26. {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/cli/commands/login.py +0 -0
  27. {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/cli/commands/optimizations.py +0 -0
  28. {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/cli/commands/org.py +0 -0
  29. {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/cli/commands/programs.py +0 -0
  30. {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/cli/commands/top.py +0 -0
  31. {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/cli/commands/traces.py +0 -0
  32. {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/cli/credentials.py +0 -0
  33. {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/cli/shell.py +0 -0
  34. {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/cli/verbs.py +0 -0
  35. {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/configuration.py +0 -0
  36. {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/context.py +0 -0
  37. {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/dataset_sync.py +0 -0
  38. {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/datasets.py +0 -0
  39. {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/decorators.py +0 -0
  40. {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/deployment.py +0 -0
  41. {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/eval_handler.py +0 -0
  42. {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/execution.py +0 -0
  43. {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/exporter.py +0 -0
  44. {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/helpers.py +0 -0
  45. {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/identity.py +0 -0
  46. {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/ir/__init__.py +0 -0
  47. {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/ir/decode.py +0 -0
  48. {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/ir/encode.py +0 -0
  49. {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/ir/hash.py +0 -0
  50. {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/ir/program.py +0 -0
  51. {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/ir/reflection.py +0 -0
  52. {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/ir/types.py +0 -0
  53. {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/models.py +0 -0
  54. {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/optimization.py +0 -0
  55. {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/optimizers/__init__.py +0 -0
  56. {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/optimizers/gepa.py +0 -0
  57. {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/optimizers/gepa_callback.py +0 -0
  58. {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/optimizers/resume.py +0 -0
  59. {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/optimizers/tracker.py +0 -0
  60. {cmpnd-0.4.1 → cmpnd-0.4.2}/cmpnd/packaging.py +0 -0
  61. {cmpnd-0.4.1 → cmpnd-0.4.2}/docs/2026-04-02-code-review.md +0 -0
  62. {cmpnd-0.4.1 → cmpnd-0.4.2}/docs/plans/2026-04-05-train-val-dataset-capture-design.md +0 -0
  63. {cmpnd-0.4.1 → cmpnd-0.4.2}/docs/plans/2026-04-05-train-val-dataset-capture.md +0 -0
  64. {cmpnd-0.4.1 → cmpnd-0.4.2}/docs/sdk-instrumentation.md +0 -0
  65. {cmpnd-0.4.1 → cmpnd-0.4.2}/examples/local_ollama.py +0 -0
  66. {cmpnd-0.4.1 → cmpnd-0.4.2}/scripts/capture_review_run.py +0 -0
  67. {cmpnd-0.4.1 → cmpnd-0.4.2}/scripts/fixtures/review_run.json.gz +0 -0
  68. {cmpnd-0.4.1 → cmpnd-0.4.2}/scripts/gepa_resume_bug.py +0 -0
  69. {cmpnd-0.4.1 → cmpnd-0.4.2}/stubs/dspy/__init__.pyi +0 -0
  70. {cmpnd-0.4.1 → cmpnd-0.4.2}/stubs/dspy/primitives/__init__.pyi +0 -0
  71. {cmpnd-0.4.1 → cmpnd-0.4.2}/stubs/dspy/primitives/prediction.pyi +0 -0
  72. {cmpnd-0.4.1 → cmpnd-0.4.2}/stubs/dspy/utils/__init__.pyi +0 -0
  73. {cmpnd-0.4.1 → cmpnd-0.4.2}/stubs/dspy/utils/callback.pyi +0 -0
  74. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/__init__.py +0 -0
  75. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/_exporter_helpers.py +0 -0
  76. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/_identity_helpers.py +0 -0
  77. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/e2e/__init__.py +0 -0
  78. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/e2e/committee_task.py +0 -0
  79. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/e2e/data/committee_train.ndjson.gz +0 -0
  80. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/e2e/data/committee_val.ndjson.gz +0 -0
  81. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/e2e/fake_lm.py +0 -0
  82. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/e2e/helpers.py +0 -0
  83. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/e2e/test_capture_prompts.py +0 -0
  84. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/e2e/test_composite_module.py +0 -0
  85. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/e2e/test_dataset_auto_creation.py +0 -0
  86. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/e2e/test_deploy_execute.py +0 -0
  87. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/e2e/test_deploy_programs.py +0 -0
  88. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/e2e/test_edge_cases.py +0 -0
  89. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/e2e/test_example_hash.py +0 -0
  90. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/e2e/test_gepa_optimization.py +0 -0
  91. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/e2e/test_lm_accuracy.py +0 -0
  92. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/e2e/test_module_tracing.py +0 -0
  93. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/e2e/test_optimization_lineage.py +0 -0
  94. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/e2e/test_optimization_metric_identity.py +0 -0
  95. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/e2e/test_optimize_failures.py +0 -0
  96. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/e2e/test_optimize_loop.py +0 -0
  97. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/e2e/test_optimize_review_committee.py +0 -0
  98. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/e2e/test_optimize_server_owned_completion.py +0 -0
  99. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/e2e/test_parallel_export.py +0 -0
  100. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/e2e/test_project_attribution.py +0 -0
  101. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/e2e/test_react_tool_spans.py +0 -0
  102. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/e2e/test_save_load.py +0 -0
  103. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/e2e/test_trace_structure.py +0 -0
  104. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/e2e/test_user_attribution.py +0 -0
  105. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/identity_vectors/01_predict_qa.json +0 -0
  106. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/identity_vectors/02_predict_renamed_field.json +0 -0
  107. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/identity_vectors/03_chain_of_thought_qa.json +0 -0
  108. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/identity_vectors/04_predict_richer_types.json +0 -0
  109. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/identity_vectors/05_composite_two_predicts.json +0 -0
  110. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/identity_vectors/06_multichaincomparison.json +0 -0
  111. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/identity_vectors/07_program_of_thought.json +0 -0
  112. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/identity_vectors/08_opaque_module.json +0 -0
  113. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/identity_vectors/09_predict_pydantic_field.json +0 -0
  114. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/identity_vectors/10_react.json +0 -0
  115. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/identity_vectors/11_refine.json +0 -0
  116. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/identity_vectors/12_retrieve.json +0 -0
  117. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/identity_vectors/13_knn.json +0 -0
  118. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/identity_vectors/14_parallel.json +0 -0
  119. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/identity_vectors/README.md +0 -0
  120. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/integration/__init__.py +0 -0
  121. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/integration/conftest.py +0 -0
  122. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/integration/test_cli_deploy_smoketest.py +0 -0
  123. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/integration/test_logs_renders_traces.py +0 -0
  124. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/integration/test_optimizing_progress.py +0 -0
  125. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/integration/test_unhappy_path.py +0 -0
  126. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/parity/__init__.py +0 -0
  127. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/parity/conftest.py +0 -0
  128. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/parity/seed_parity.py +0 -0
  129. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/parity/test_auth.py +0 -0
  130. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/parity/test_datasets.py +0 -0
  131. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/parity/test_deployments.py +0 -0
  132. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/parity/test_evals.py +0 -0
  133. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/parity/test_health.py +0 -0
  134. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/parity/test_modules.py +0 -0
  135. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/parity/test_optimizations.py +0 -0
  136. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/parity/test_programs.py +0 -0
  137. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/parity/test_signatures.py +0 -0
  138. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/parity/test_trace_streaming.py +0 -0
  139. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/parity/test_traces.py +0 -0
  140. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/stress/__init__.py +0 -0
  141. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/stress/_reliability.py +0 -0
  142. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/stress/test_optimize_client_committee.py +0 -0
  143. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/stress/test_optimize_loop.py +0 -0
  144. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/stress/test_optimize_stress.py +0 -0
  145. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/stress/test_reliability.py +0 -0
  146. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_callback.py +0 -0
  147. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_cli/__init__.py +0 -0
  148. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_cli/conftest.py +0 -0
  149. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_cli/test_admin.py +0 -0
  150. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_cli/test_api_client.py +0 -0
  151. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_cli/test_auth.py +0 -0
  152. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_cli/test_credentials.py +0 -0
  153. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_cli/test_datasets.py +0 -0
  154. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_cli/test_deployments.py +0 -0
  155. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_cli/test_evals.py +0 -0
  156. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_cli/test_login.py +0 -0
  157. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_cli/test_main.py +0 -0
  158. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_cli/test_optimizations.py +0 -0
  159. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_cli/test_org.py +0 -0
  160. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_cli/test_parity.py +0 -0
  161. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_cli/test_programs.py +0 -0
  162. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_cli/test_shell.py +0 -0
  163. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_cli/test_top.py +0 -0
  164. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_cli/test_traces.py +0 -0
  165. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_clustering/__init__.py +0 -0
  166. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_clustering/conftest.py +0 -0
  167. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_clustering/test_edge_cases.py +0 -0
  168. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_clustering/test_hash_consistency.py +0 -0
  169. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_clustering/test_type_conversion.py +0 -0
  170. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_configuration.py +0 -0
  171. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_context.py +0 -0
  172. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_context_propagation.py +0 -0
  173. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_dataset_polling.py +0 -0
  174. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_dataset_sync.py +0 -0
  175. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_datasets.py +0 -0
  176. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_decorators.py +0 -0
  177. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_deploy.py +0 -0
  178. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_eval.py +0 -0
  179. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_eval_start.py +0 -0
  180. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_exception_paths.py +0 -0
  181. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_execute.py +0 -0
  182. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_exporter.py +0 -0
  183. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_exporter_real.py +0 -0
  184. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_fake_lm.py +0 -0
  185. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_gepa_callback.py +0 -0
  186. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_gepa_integration.py +0 -0
  187. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_gepa_resume.py +0 -0
  188. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_identity.py +0 -0
  189. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_integration.py +0 -0
  190. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_ir_encoder_properties.py +0 -0
  191. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_ir_golden_vectors.py +0 -0
  192. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_ir_roundtrip.py +0 -0
  193. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_models.py +0 -0
  194. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_optimization_start.py +0 -0
  195. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_optimize.py +0 -0
  196. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_optimize_lift.py +0 -0
  197. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_optimize_terminal_failure.py +0 -0
  198. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_packaging.py +0 -0
  199. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_packaging_callable_source.py +0 -0
  200. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_packaging_program_source.py +0 -0
  201. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_program_lineage_persist.py +0 -0
  202. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_project_payload.py +0 -0
  203. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_reflector_properties.py +0 -0
  204. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_reflector_smoke.py +0 -0
  205. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_repro_reactv2_bugs.py +0 -0
  206. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_schemas.py +0 -0
  207. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_seed_review_run.py +0 -0
  208. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_stats_export.py +0 -0
  209. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_tracked_gepa.py +0 -0
  210. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_tracker_enrich.py +0 -0
  211. {cmpnd-0.4.1 → cmpnd-0.4.2}/tests/test_truncation.py +0 -0
  212. {cmpnd-0.4.1 → cmpnd-0.4.2}/todo.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cmpnd
3
- Version: 0.4.1
3
+ Version: 0.4.2
4
4
  Summary: DSPy observability and deployment SDK for cmpnd
5
5
  Project-URL: Homepage, https://cmpnd.ai
6
6
  Author-email: cmpnd <hello@cmpnd.ai>
@@ -136,4 +136,10 @@ def auto_instrument() -> CmpndCallback:
136
136
 
137
137
  _program_patch.patch_program_state()
138
138
 
139
+ # Emit tool spans for dspy.RLM tool calls, which otherwise bypass
140
+ # dspy.Tool.__call__'s callbacks and produce no spans (CMP-187).
141
+ from . import _rlm_patch
142
+
143
+ _rlm_patch.patch_rlm_tools()
144
+
139
145
  return callback
@@ -0,0 +1,134 @@
1
+ """Emit tool spans for ``dspy.RLM`` tool calls (CMP-187).
2
+
3
+ cmpnd creates a ``TOOL`` span only from DSPy's ``on_tool_start``/``on_tool_end``
4
+ callbacks, and those fire only through ``dspy.Tool.__call__`` (decorated
5
+ ``@with_callbacks``). ``dspy.RLM._prepare_execution_tools`` hands the code
6
+ interpreter the **bare** ``tool.func`` for each user tool
7
+ (``{name: tool.func for name, tool in self._user_tools.items()}`` in
8
+ ``dspy/predict/rlm.py``), so ``Tool.__call__`` is bypassed and no span is
9
+ created — unlike ``dspy.ReAct``, which calls the ``Tool`` object and gets tool
10
+ spans for free.
11
+
12
+ ``patch_rlm_tools()`` wraps ``_prepare_execution_tools`` so each user tool's
13
+ func is replaced by a shim that dispatches the tool lifecycle to the active
14
+ ``CmpndCallback``\\ (s) around the original func call. Routing through the
15
+ callback (rather than through ``Tool.__call__``) keeps execution identical to
16
+ today's bare-func path — no re-validation / pydantic coercion of the
17
+ interpreter's native kwargs — while still emitting the span.
18
+
19
+ ``PythonInterpreter.execute`` dispatches tool calls inline on the calling
20
+ (forward) thread, so the shim runs with cmpnd's ``_span_stack`` contextvar
21
+ intact: the tool span nests under ``RLM.forward`` and any ``dspy.Predict`` run
22
+ inside the tool re-parents under it.
23
+
24
+ Signature preservation is mandatory: ``PythonInterpreter._extract_parameters``
25
+ builds its sandbox tool stub from ``inspect.signature(fn)``. ``functools.wraps``
26
+ sets ``__wrapped__``, which ``inspect.signature`` follows, so the real params
27
+ survive the wrap (guarded by a test).
28
+
29
+ Scope is user tools only; the ``llm_query`` / ``llm_query_batched`` builtins
30
+ share the gap but ``llm_query_batched`` fans out over its own thread pool (a
31
+ separate context-propagation problem), so they're left out here.
32
+
33
+ Installed lazily from ``cmpnd.auto_instrument()`` alongside ``patch_gepa()`` /
34
+ ``patch_program_state()``. Idempotent; a no-op when dspy isn't installed.
35
+ """
36
+
37
+ from __future__ import annotations
38
+
39
+ import functools
40
+ import uuid
41
+ from typing import Any, Callable
42
+
43
+ _patched = False
44
+
45
+
46
+ def _active_cmpnd_callback() -> Any | None:
47
+ """The single registered CmpndCallback (or None).
48
+
49
+ cmpnd is built around exactly one callback — auto_instrument() refuses to
50
+ add a second — and the callbacks share one span-stack contextvar. So we
51
+ dispatch the tool lifecycle to just the first CmpndCallback: pushing a span
52
+ per callback would nest one tool span under another for a single tool call
53
+ and desync on_tool_end's pop from the stack.
54
+ """
55
+ try:
56
+ import dspy
57
+ except ImportError:
58
+ return None
59
+ from . import callback
60
+
61
+ registered = dspy.settings.get("callbacks", []) or []
62
+ for cb in registered:
63
+ if isinstance(cb, callback.CmpndCallback):
64
+ return cb
65
+ return None
66
+
67
+
68
+ def _wrap_tool_func(tool_obj: Any) -> Callable[..., Any]:
69
+ """Wrap a dspy.Tool's func so a call emits a TOOL span around it.
70
+
71
+ The wrapper carries the original func's signature (``functools.wraps`` sets
72
+ ``__wrapped__``) so the interpreter's sandbox stub sees the real params. It
73
+ dispatches to the active cmpnd callback only; with none registered it's a
74
+ transparent passthrough.
75
+ """
76
+ func = tool_obj.func
77
+
78
+ @functools.wraps(func)
79
+ def shim(**kwargs: Any) -> Any:
80
+ cb = _active_cmpnd_callback()
81
+ if cb is None:
82
+ return func(**kwargs)
83
+
84
+ # on_tool_start (which creates + pushes the span) must succeed before we
85
+ # enter the try; on_tool_end is a no-op for an unknown call_id, so a
86
+ # failed start can't leave the span stack unbalanced.
87
+ call_id = uuid.uuid4().hex
88
+ cb.on_tool_start(call_id, tool_obj, kwargs)
89
+ output = None
90
+ exception = None
91
+ try:
92
+ output = func(**kwargs)
93
+ return output
94
+ except Exception as exc: # noqa: BLE001 - re-raised after closing the span
95
+ exception = exc
96
+ raise
97
+ finally:
98
+ cb.on_tool_end(call_id, output, exception)
99
+
100
+ return shim
101
+
102
+
103
+ def patch_rlm_tools() -> None:
104
+ """Wrap ``RLM._prepare_execution_tools`` so user tools emit spans.
105
+
106
+ Idempotent (a module-level flag guards double-patching); a no-op when dspy
107
+ or ``dspy.RLM`` isn't available.
108
+ """
109
+ global _patched
110
+ if _patched:
111
+ return
112
+
113
+ try:
114
+ import dspy
115
+ except ImportError:
116
+ return
117
+
118
+ rlm_cls = getattr(dspy, "RLM", None)
119
+ if rlm_cls is None or not hasattr(rlm_cls, "_prepare_execution_tools"):
120
+ return
121
+
122
+ orig = getattr(rlm_cls, "_prepare_execution_tools")
123
+
124
+ @functools.wraps(orig)
125
+ def _prepare_execution_tools(self: Any) -> dict[str, Callable[..., Any]]:
126
+ tools = orig(self)
127
+ user_tools = getattr(self, "_user_tools", {}) or {}
128
+ for name, tool_obj in user_tools.items():
129
+ if name in tools:
130
+ tools[name] = _wrap_tool_func(tool_obj)
131
+ return tools
132
+
133
+ setattr(rlm_cls, "_prepare_execution_tools", _prepare_execution_tools)
134
+ _patched = True
@@ -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.4.1"
8
+ version = "0.4.2"
9
9
  description = "DSPy observability and deployment SDK for cmpnd"
10
10
  readme = "README.md"
11
11
  requires-python = ">=3.10"
@@ -236,6 +236,19 @@ _RLM_FIXTURE = (
236
236
  pathlib.Path(__file__).resolve().parents[2] / "serve" / "scripts" / "seed_fixtures" / "rlm_district_counter.json"
237
237
  )
238
238
 
239
+ # Two flat-replay fixtures (CMP-187), shared with the local seeder
240
+ # (serve/scripts/seed_fixtures/traces.py make_source_categorizer_trace /
241
+ # make_tool_kind_demo_trace). Unlike _RLM_FIXTURE (pre-digested turns), these
242
+ # are a flat list of spans carrying lid/parent/start_ns + ingest fields, so
243
+ # _replay_flat_fixture just remaps ids + offsets timings. Loaded lazily so a
244
+ # missing file degrades to skipping that seed.
245
+ _SOURCE_CATEGORIZER_FIXTURE = (
246
+ pathlib.Path(__file__).resolve().parents[2] / "serve" / "scripts" / "seed_fixtures" / "rlm_source_categorizer.json"
247
+ )
248
+ _TOOL_KIND_DEMO_FIXTURE = (
249
+ pathlib.Path(__file__).resolve().parents[2] / "serve" / "scripts" / "seed_fixtures" / "tool_kind_demo.json"
250
+ )
251
+
239
252
 
240
253
  def _notebook_trace(now: datetime.datetime) -> dict:
241
254
  """The captured "DistrictCounter" dspy.RLM run as a review-app seed trace
@@ -378,6 +391,75 @@ def _notebook_trace(now: datetime.datetime) -> dict:
378
391
  }
379
392
 
380
393
 
394
+ def _replay_flat_fixture(
395
+ fixture_path: pathlib.Path,
396
+ now: datetime.datetime,
397
+ program_name: str,
398
+ project_tags: list[str],
399
+ request_preview: str,
400
+ response_preview: str,
401
+ ) -> dict:
402
+ """Replay a flat-format fixture (list of spans with lid/parent/start_ns/
403
+ duration_ns + ingest fields) as a review-app seed trace. Mirrors
404
+ make_source_categorizer_trace / make_tool_kind_demo_trace in the local
405
+ seeder, parameterized so both CMP-187 fixtures share one path. Mints a
406
+ fresh trace_id, maps every local id to a fresh uuid (parent_span_id via
407
+ that map, null for the root), offsets each span's start_ns by `now` with
408
+ the same at() helper the other seeds use, and copies the remaining fields
409
+ verbatim into the ingest span shape. Returns the same trace-payload dict
410
+ shape as _notebook_trace.
411
+ """
412
+ fx = json.loads(fixture_path.read_text())
413
+ root = fx[0]
414
+ trace_id = str(uuid.uuid4())
415
+ total_ns = root["duration_ns"]
416
+
417
+ def at(ns: int) -> str:
418
+ return (now + datetime.timedelta(microseconds=ns / 1000)).isoformat()
419
+
420
+ id_map = {s["lid"]: str(uuid.uuid4()) for s in fx}
421
+ skip_keys = {"lid", "parent", "start_ns", "signature_summary"}
422
+
423
+ spans: list[dict] = []
424
+ total_tokens = 0
425
+ for s in fx:
426
+ parent_lid = s["parent"]
427
+ span: dict = {
428
+ "span_id": id_map[s["lid"]],
429
+ "trace_id": trace_id,
430
+ "parent_span_id": id_map[parent_lid] if parent_lid is not None else None,
431
+ "start_time": at(s["start_ns"]),
432
+ "end_time": at(s["start_ns"] + s["duration_ns"]),
433
+ }
434
+ for k, v in s.items():
435
+ if k not in skip_keys:
436
+ span[k] = v
437
+ if s["span_type"] == "lm_call":
438
+ total_tokens += s.get("total_tokens") or 0
439
+ spans.append(span)
440
+
441
+ return {
442
+ "trace_id": trace_id,
443
+ "start_time": at(0),
444
+ "end_time": at(total_ns),
445
+ "duration_ns": total_ns,
446
+ "status": "ok",
447
+ "program_name": program_name, # DSPy module class → programs.module_class → sidebar badge
448
+ "program_version": "0.1.0",
449
+ "program_signature_hash": root["program_signature_hash"],
450
+ "signature_name": root["signature_name"],
451
+ "signature_input_fields": root["signature_input_fields"],
452
+ "signature_output_fields": root["signature_output_fields"],
453
+ "adapter_type": "ChatAdapter",
454
+ "request_preview": request_preview,
455
+ "response_preview": response_preview,
456
+ "project_tags": project_tags,
457
+ "total_tokens": total_tokens,
458
+ "span_count": len(spans),
459
+ "spans": spans,
460
+ }
461
+
462
+
381
463
  def _post(endpoint: str, path: str, payload, api_key: str):
382
464
  body = json.dumps(payload).encode("utf-8")
383
465
  req = urllib.request.Request(
@@ -494,6 +576,42 @@ def main() -> int:
494
576
  # optimization run has none. Unlinked to any optimization run.
495
577
  code, _ = _post(endpoint, "/api/v1/traces/batch", [_notebook_trace(now)], api_key)
496
578
  log.info("seeded RLM notebook trace (status %s)", code)
579
+
580
+ # 4b) Plant the two CMP-187 tool-span demo traces so every review app
581
+ # shows RLM tool spans (SourceCategorizer, with a vision sub-LLM nested
582
+ # under Tool.inspect_page) and the function-tool vs module-tool
583
+ # differentiation (ToolKindDemo). Each degrades to a skip if its shared
584
+ # fixture is absent, rather than taking the seed down.
585
+ tool_demos: list[dict] = []
586
+ if _SOURCE_CATEGORIZER_FIXTURE.exists():
587
+ tool_demos.append(
588
+ _replay_flat_fixture(
589
+ _SOURCE_CATEGORIZER_FIXTURE,
590
+ now,
591
+ "RLM",
592
+ _SEED_TAGS + ["tool-demo"],
593
+ "vector_pdf source_table/glenn-pdf-p15.pdf (page 1)",
594
+ "candidate_columns, precinct",
595
+ )
596
+ )
597
+ else:
598
+ log.warning("skipping SourceCategorizer seed: fixture missing at %s", _SOURCE_CATEGORIZER_FIXTURE)
599
+ if _TOOL_KIND_DEMO_FIXTURE.exists():
600
+ tool_demos.append(
601
+ _replay_flat_fixture(
602
+ _TOOL_KIND_DEMO_FIXTURE,
603
+ now,
604
+ "ToolKindDemo",
605
+ _SEED_TAGS + ["tool-demo"],
606
+ "espresso place review (function-tool + module-tool)",
607
+ "The speaker loved the new downtown espresso place and its incredible pastries.",
608
+ )
609
+ )
610
+ else:
611
+ log.warning("skipping ToolKindDemo seed: fixture missing at %s", _TOOL_KIND_DEMO_FIXTURE)
612
+ if tool_demos:
613
+ code, _ = _post(endpoint, "/api/v1/traces/batch", tool_demos, api_key)
614
+ log.info("seeded %d CMP-187 tool-demo trace(s) (status %s)", len(tool_demos), code)
497
615
  except Exception as exc: # noqa: BLE001 - non-gating; review apps are ephemeral
498
616
  log.warning("review seed failed (non-gating): %s", exc)
499
617
 
@@ -32,7 +32,7 @@ from tests.e2e import fake_lm
32
32
  E2E_ENDPOINT = os.getenv("CMPND_E2E_ENDPOINT", "http://localhost:3000")
33
33
  CMPND_E2E_API_KEY = os.getenv("CMPND_E2E_API_KEY")
34
34
  CMPND_E2E_GROQ_KEY = os.getenv("CMPND_E2E_GROQ_KEY")
35
- E2E_MODEL = "groq/meta-llama/llama-4-scout-17b-16e-instruct"
35
+ E2E_MODEL = "groq/llama-3.3-70b-versatile"
36
36
 
37
37
  # ---------------------------------------------------------------------------
38
38
  # Skip logic — skip entire module when env vars are missing
@@ -80,7 +80,7 @@ class TestDeployReActWikipedia:
80
80
  @pytest.fixture(scope="class")
81
81
  def deployment(self):
82
82
  lm = dspy.LM(
83
- "groq/meta-llama/llama-4-scout-17b-16e-instruct",
83
+ "groq/llama-3.3-70b-versatile",
84
84
  api_key=PLATFORM_GROQ_KEY or "",
85
85
  max_tokens=300,
86
86
  cache=False,
@@ -41,7 +41,7 @@ from tests._identity_helpers import program_hash_for_sig
41
41
  PLATFORM_API_KEY = os.environ.get("PLATFORM_API_KEY")
42
42
  PLATFORM_GROQ_KEY = os.environ.get("PLATFORM_GROQ_KEY")
43
43
  PLATFORM_ENDPOINT = os.environ.get("PLATFORM_ENDPOINT", "http://localhost:3000")
44
- STRESS_MODEL = "groq/meta-llama/llama-4-scout-17b-16e-instruct"
44
+ STRESS_MODEL = "groq/llama-3.3-70b-versatile"
45
45
 
46
46
  NUM_THREADS = 5
47
47
  ITERATIONS_PER_RUN = 3
@@ -34,7 +34,7 @@ from tests.stress._reliability import soft_content_check, tolerate_external_outa
34
34
  PLATFORM_API_KEY = os.environ.get("PLATFORM_API_KEY")
35
35
  PLATFORM_GROQ_KEY = os.environ.get("PLATFORM_GROQ_KEY")
36
36
  PLATFORM_ENDPOINT = os.environ.get("PLATFORM_ENDPOINT", "http://localhost:3000")
37
- STRESS_MODEL = "groq/meta-llama/llama-4-scout-17b-16e-instruct"
37
+ STRESS_MODEL = "groq/llama-3.3-70b-versatile"
38
38
 
39
39
  pytestmark = pytest.mark.skipif(
40
40
  not PLATFORM_API_KEY or not PLATFORM_GROQ_KEY,
@@ -0,0 +1,180 @@
1
+ """Tests for RLM tool-span instrumentation (_rlm_patch, CMP-187).
2
+
3
+ dspy.RLM hands its code interpreter the bare tool.func, bypassing
4
+ dspy.Tool.__call__'s @with_callbacks, so no tool span is created. The patch
5
+ wraps RLM._prepare_execution_tools so each user tool emits a TOOL span via the
6
+ active cmpnd callback while preserving the func's signature (the interpreter
7
+ builds its sandbox stub from inspect.signature).
8
+ """
9
+
10
+ import inspect
11
+ import uuid
12
+
13
+ import dspy
14
+ import pytest
15
+
16
+ import cmpnd
17
+ from cmpnd import _rlm_patch, callback, configuration, context, models
18
+
19
+
20
+ @pytest.fixture(autouse=True)
21
+ def _configured():
22
+ configuration.configure(api_key="test_key", endpoint="http://localhost:8000")
23
+ yield
24
+
25
+
26
+ def _make_rlm():
27
+ def add_pages(first: int, last: int) -> int:
28
+ """Sum a page range."""
29
+ return sum(range(first, last + 1))
30
+
31
+ return dspy.RLM("context, query -> answer", tools=[add_pages]), add_pages
32
+
33
+
34
+ def test_patch_wraps_prepare_execution_tools_and_is_idempotent():
35
+ orig = dspy.RLM._prepare_execution_tools
36
+ _rlm_patch._patched = False
37
+ _rlm_patch.patch_rlm_tools()
38
+ wrapped = dspy.RLM._prepare_execution_tools
39
+ assert wrapped is not orig
40
+ _rlm_patch.patch_rlm_tools()
41
+ assert dspy.RLM._prepare_execution_tools is wrapped
42
+
43
+
44
+ def test_wrapped_tool_preserves_signature():
45
+ """The interpreter reads inspect.signature(fn) to build its sandbox stub."""
46
+ _rlm_patch._patched = False
47
+ _rlm_patch.patch_rlm_tools()
48
+
49
+ rlm, add_pages = _make_rlm()
50
+ tools = rlm._prepare_execution_tools()
51
+ shim = tools["add_pages"]
52
+ assert inspect.signature(shim) == inspect.signature(add_pages)
53
+
54
+
55
+ def test_tool_span_created_and_nested_under_rlm(monkeypatch):
56
+ """With a callback and an RLM.forward span on the stack, invoking a wrapped
57
+ tool creates a TOOL span parented to the RLM span."""
58
+ _rlm_patch._patched = False
59
+ _rlm_patch.patch_rlm_tools()
60
+
61
+ tracked = []
62
+ cb = callback.CmpndCallback()
63
+ monkeypatch.setattr(cb, "_track_span", lambda span: tracked.append(span))
64
+ monkeypatch.setattr(dspy.settings, "callbacks", [cb], raising=False)
65
+
66
+ context.clear_context()
67
+ context.set_current_trace(models.TraceBuilder(trace_id=uuid.uuid4()))
68
+ rlm_span = context.create_child_span("RLM.forward", models.SpanType.MODULE)
69
+ context.push_span(rlm_span)
70
+ try:
71
+ rlm, _ = _make_rlm()
72
+ tools = rlm._prepare_execution_tools()
73
+ result = tools["add_pages"](first=1, last=3)
74
+ # the tool span is unwound after the tool returns, leaving the RLM span
75
+ span_after_tool = context.get_current_span()
76
+ finally:
77
+ context.pop_span()
78
+
79
+ assert result == 6
80
+ assert span_after_tool is rlm_span
81
+ tool_spans = [s for s in tracked if s.span_type == models.SpanType.TOOL]
82
+ assert len(tool_spans) == 1
83
+ span = tool_spans[0]
84
+ assert span.name == "Tool.add_pages"
85
+ assert span.parent_span_id == rlm_span.span_id
86
+ assert span.trace_id == rlm_span.trace_id
87
+
88
+
89
+ def test_multiple_callbacks_emit_one_tool_span(monkeypatch):
90
+ """Two registered CmpndCallbacks share one span-stack contextvar, so the
91
+ shim dispatches to only the first — one tool span, stack balanced, no
92
+ tool-span-nested-under-tool-span corruption."""
93
+ _rlm_patch._patched = False
94
+ _rlm_patch.patch_rlm_tools()
95
+
96
+ tracked_a, tracked_b = [], []
97
+ cb_a = callback.CmpndCallback()
98
+ cb_b = callback.CmpndCallback()
99
+ monkeypatch.setattr(cb_a, "_track_span", lambda span: tracked_a.append(span))
100
+ monkeypatch.setattr(cb_b, "_track_span", lambda span: tracked_b.append(span))
101
+ monkeypatch.setattr(dspy.settings, "callbacks", [cb_a, cb_b], raising=False)
102
+
103
+ context.clear_context()
104
+ context.set_current_trace(models.TraceBuilder(trace_id=uuid.uuid4()))
105
+ rlm_span = context.create_child_span("RLM.forward", models.SpanType.MODULE)
106
+ context.push_span(rlm_span)
107
+ try:
108
+ rlm, _ = _make_rlm()
109
+ tools = rlm._prepare_execution_tools()
110
+ tools["add_pages"](first=1, last=3)
111
+ span_after_tool = context.get_current_span()
112
+ finally:
113
+ context.pop_span()
114
+
115
+ assert span_after_tool is rlm_span # stack unwound to exactly the RLM span
116
+ tool_spans = [s for s in tracked_a + tracked_b if s.span_type == models.SpanType.TOOL]
117
+ assert len(tool_spans) == 1
118
+ assert tool_spans[0].parent_span_id == rlm_span.span_id
119
+ assert not tracked_b # only the first callback was dispatched to
120
+
121
+
122
+ def test_tracing_off_calls_func_without_span():
123
+ """No cmpnd callback registered -> func still runs, no span emitted."""
124
+ _rlm_patch._patched = False
125
+ _rlm_patch.patch_rlm_tools()
126
+
127
+ context.clear_context()
128
+ original = list(dspy.settings.get("callbacks", []))
129
+ dspy.settings.callbacks = []
130
+ try:
131
+ rlm, _ = _make_rlm()
132
+ tools = rlm._prepare_execution_tools()
133
+ assert tools["add_pages"](first=2, last=4) == 9
134
+ assert context.get_current_span() is None
135
+ finally:
136
+ dspy.settings.callbacks = original
137
+
138
+
139
+ def test_raising_tool_closes_span_and_reraises(monkeypatch):
140
+ _rlm_patch._patched = False
141
+ _rlm_patch.patch_rlm_tools()
142
+
143
+ tracked = []
144
+ cb = callback.CmpndCallback()
145
+ monkeypatch.setattr(cb, "_track_span", lambda span: tracked.append(span))
146
+ monkeypatch.setattr(dspy.settings, "callbacks", [cb], raising=False)
147
+
148
+ def boom(x: int) -> int:
149
+ raise ValueError("nope")
150
+
151
+ rlm = dspy.RLM("context, query -> answer", tools=[boom])
152
+
153
+ context.clear_context()
154
+ context.set_current_trace(models.TraceBuilder(trace_id=uuid.uuid4()))
155
+ rlm_span = context.create_child_span("RLM.forward", models.SpanType.MODULE)
156
+ context.push_span(rlm_span)
157
+ try:
158
+ tools = rlm._prepare_execution_tools()
159
+ raised = False
160
+ try:
161
+ tools["boom"](x=1)
162
+ except ValueError:
163
+ raised = True
164
+ assert raised
165
+ # the tool span is closed even when the tool raises
166
+ span_after_tool = context.get_current_span()
167
+ finally:
168
+ context.pop_span()
169
+
170
+ assert span_after_tool is rlm_span
171
+ tool_spans = [s for s in tracked if s.span_type == models.SpanType.TOOL]
172
+ assert len(tool_spans) == 1
173
+ assert tool_spans[0].status == models.TraceStatus.ERROR
174
+
175
+
176
+ def test_auto_instrument_installs_rlm_patch():
177
+ _rlm_patch._patched = False
178
+ orig = dspy.RLM._prepare_execution_tools
179
+ cmpnd.auto_instrument()
180
+ assert dspy.RLM._prepare_execution_tools is not orig
@@ -497,7 +497,7 @@ wheels = [
497
497
 
498
498
  [[package]]
499
499
  name = "cmpnd"
500
- version = "0.4.1"
500
+ version = "0.4.2"
501
501
  source = { editable = "." }
502
502
  dependencies = [
503
503
  { name = "dspy" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes