cmpnd 0.62.0__tar.gz → 0.64.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 (395) hide show
  1. {cmpnd-0.62.0 → cmpnd-0.64.0}/PKG-INFO +40 -46
  2. {cmpnd-0.62.0 → cmpnd-0.64.0}/README.md +39 -45
  3. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/_assessments.py +6 -5
  4. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/_traces.py +11 -14
  5. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/_valsets.py +9 -7
  6. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/callback.py +25 -0
  7. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/cli/__init__.py +0 -2
  8. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/cli/commands/assessments.py +11 -2
  9. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/cli/commands/evals.py +1 -1
  10. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/cli/commands/org.py +71 -4
  11. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/cli/commands/programs.py +59 -16
  12. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/cli/commands/top.py +21 -48
  13. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/cli/commands/traces.py +8 -14
  14. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/cli/commands/valsets.py +6 -25
  15. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/cli/verbs.py +5 -5
  16. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/client.py +32 -9
  17. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/programs.py +22 -1
  18. {cmpnd-0.62.0 → cmpnd-0.64.0}/docs/cli.md +67 -55
  19. {cmpnd-0.62.0 → cmpnd-0.64.0}/docs/client.md +18 -10
  20. {cmpnd-0.62.0 → cmpnd-0.64.0}/docs/reading-traces.md +2 -3
  21. {cmpnd-0.62.0 → cmpnd-0.64.0}/docs/sdk-instrumentation.md +16 -3
  22. {cmpnd-0.62.0 → cmpnd-0.64.0}/pyproject.toml +1 -1
  23. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/e2e/test_traces_read.py +1 -1
  24. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/e2e/test_valsets_cli.py +11 -10
  25. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_assessments_api.py +15 -5
  26. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_cli/test_assessments.py +34 -7
  27. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_cli/test_evals.py +1 -1
  28. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_cli/test_org.py +160 -4
  29. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_cli/test_parity.py +0 -4
  30. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_cli/test_programs.py +90 -4
  31. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_cli/test_schema_sample.py +12 -8
  32. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_cli/test_shell.py +7 -0
  33. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_cli/test_top.py +77 -7
  34. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_cli/test_traces.py +0 -4
  35. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_cli/test_traces_program_and_module.py +4 -5
  36. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_cli/test_traces_search_wire.py +28 -16
  37. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_cli/test_valsets.py +18 -14
  38. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_client.py +12 -2
  39. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_client_program_names.py +142 -0
  40. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_traces_read.py +0 -2
  41. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_valsets_api.py +34 -8
  42. {cmpnd-0.62.0 → cmpnd-0.64.0}/uv.lock +1 -1
  43. cmpnd-0.62.0/cmpnd/cli/commands/signatures.py +0 -164
  44. cmpnd-0.62.0/tests/test_cli/test_signatures.py +0 -225
  45. {cmpnd-0.62.0 → cmpnd-0.64.0}/.gitignore +0 -0
  46. {cmpnd-0.62.0 → cmpnd-0.64.0}/AGENTS.md +0 -0
  47. {cmpnd-0.62.0 → cmpnd-0.64.0}/CLAUDE.md +0 -0
  48. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/__init__.py +0 -0
  49. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/_authored_import.py +0 -0
  50. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/_deployments.py +0 -0
  51. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/_exporter_pool.py +0 -0
  52. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/_gepa_patch.py +0 -0
  53. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/_http.py +0 -0
  54. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/_program_patch.py +0 -0
  55. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/_rlm_patch.py +0 -0
  56. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/_runtime_tools.py +0 -0
  57. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/_timespec.py +0 -0
  58. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/authored.py +0 -0
  59. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/cli/api_client.py +0 -0
  60. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/cli/commands/__init__.py +0 -0
  61. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/cli/commands/admin.py +0 -0
  62. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/cli/commands/auth.py +0 -0
  63. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/cli/commands/datasets.py +0 -0
  64. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/cli/commands/deployments.py +0 -0
  65. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/cli/commands/login.py +0 -0
  66. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/cli/commands/optimizations.py +0 -0
  67. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/cli/commands/provider_credentials.py +0 -0
  68. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/cli/commands/tool_servers.py +0 -0
  69. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/cli/credentials.py +0 -0
  70. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/cli/deprecation.py +0 -0
  71. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/cli/pagination.py +0 -0
  72. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/cli/shell.py +0 -0
  73. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/cli/trace_render.py +0 -0
  74. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/cli/tree_help.py +0 -0
  75. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/configuration.py +0 -0
  76. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/context.py +0 -0
  77. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/credentials.py +0 -0
  78. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/dataset_sync.py +0 -0
  79. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/datasets.py +0 -0
  80. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/decorators.py +0 -0
  81. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/deployment.py +0 -0
  82. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/deployment_metadata.py +0 -0
  83. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/deployment_runs.py +0 -0
  84. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/eval_handler.py +0 -0
  85. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/execution.py +0 -0
  86. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/exporter.py +0 -0
  87. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/exporter_http.py +0 -0
  88. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/helpers.py +0 -0
  89. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/identity.py +0 -0
  90. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/imaging.py +0 -0
  91. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/ir/__init__.py +0 -0
  92. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/ir/decode.py +0 -0
  93. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/ir/encode.py +0 -0
  94. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/ir/hash.py +0 -0
  95. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/ir/program.py +0 -0
  96. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/ir/reflection.py +0 -0
  97. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/ir/types.py +0 -0
  98. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/ir/xxh64.py +0 -0
  99. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/lm_configuration.py +0 -0
  100. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/mcp_servers.py +0 -0
  101. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/models.py +0 -0
  102. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/named_programs.py +0 -0
  103. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/optimization.py +0 -0
  104. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/optimizers/__init__.py +0 -0
  105. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/optimizers/gepa.py +0 -0
  106. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/optimizers/gepa_callback.py +0 -0
  107. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/optimizers/resume.py +0 -0
  108. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/optimizers/tracker.py +0 -0
  109. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/packaging.py +0 -0
  110. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/references.py +0 -0
  111. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/retry.py +0 -0
  112. {cmpnd-0.62.0 → cmpnd-0.64.0}/cmpnd/traces.py +0 -0
  113. {cmpnd-0.62.0 → cmpnd-0.64.0}/docs/2026-04-02-code-review.md +0 -0
  114. {cmpnd-0.62.0 → cmpnd-0.64.0}/docs/custom-proposer.md +0 -0
  115. {cmpnd-0.62.0 → cmpnd-0.64.0}/docs/plans/2026-04-05-train-val-dataset-capture-design.md +0 -0
  116. {cmpnd-0.62.0 → cmpnd-0.64.0}/docs/plans/2026-04-05-train-val-dataset-capture.md +0 -0
  117. {cmpnd-0.62.0 → cmpnd-0.64.0}/examples/generalizing_proposer.py +0 -0
  118. {cmpnd-0.62.0 → cmpnd-0.64.0}/examples/local_ollama.py +0 -0
  119. {cmpnd-0.62.0 → cmpnd-0.64.0}/ingest/LINEAGE.md +0 -0
  120. {cmpnd-0.62.0 → cmpnd-0.64.0}/ingest/README.md +0 -0
  121. {cmpnd-0.62.0 → cmpnd-0.64.0}/ingest/cmpnd_ingest/__init__.py +0 -0
  122. {cmpnd-0.62.0 → cmpnd-0.64.0}/ingest/cmpnd_ingest/bodies.py +0 -0
  123. {cmpnd-0.62.0 → cmpnd-0.64.0}/ingest/cmpnd_ingest/client_floor.py +0 -0
  124. {cmpnd-0.62.0 → cmpnd-0.64.0}/ingest/cmpnd_ingest/exporter_http.py +0 -0
  125. {cmpnd-0.62.0 → cmpnd-0.64.0}/ingest/cmpnd_ingest/program_names.py +0 -0
  126. {cmpnd-0.62.0 → cmpnd-0.64.0}/ingest/cmpnd_ingest/protocols.py +0 -0
  127. {cmpnd-0.62.0 → cmpnd-0.64.0}/ingest/cmpnd_ingest/py.typed +0 -0
  128. {cmpnd-0.62.0 → cmpnd-0.64.0}/ingest/cmpnd_ingest/request_log.py +0 -0
  129. {cmpnd-0.62.0 → cmpnd-0.64.0}/ingest/cmpnd_ingest/retry.py +0 -0
  130. {cmpnd-0.62.0 → cmpnd-0.64.0}/ingest/cmpnd_ingest/trace_facts.py +0 -0
  131. {cmpnd-0.62.0 → cmpnd-0.64.0}/ingest/pyproject.toml +0 -0
  132. {cmpnd-0.62.0 → cmpnd-0.64.0}/scripts/audit-sdist.sh +0 -0
  133. {cmpnd-0.62.0 → cmpnd-0.64.0}/scripts/capture_review_run.py +0 -0
  134. {cmpnd-0.62.0 → cmpnd-0.64.0}/scripts/check-pip-install.sh +0 -0
  135. {cmpnd-0.62.0 → cmpnd-0.64.0}/scripts/check_release_artifacts.py +0 -0
  136. {cmpnd-0.62.0 → cmpnd-0.64.0}/scripts/fixtures/review_run.json.gz +0 -0
  137. {cmpnd-0.62.0 → cmpnd-0.64.0}/scripts/gepa_resume_bug.py +0 -0
  138. {cmpnd-0.62.0 → cmpnd-0.64.0}/scripts/install-test-env.sh +0 -0
  139. {cmpnd-0.62.0 → cmpnd-0.64.0}/scripts/release-stress.sh +0 -0
  140. {cmpnd-0.62.0 → cmpnd-0.64.0}/scripts/require_version_bump.py +0 -0
  141. {cmpnd-0.62.0 → cmpnd-0.64.0}/scripts/seed_review_run.py +0 -0
  142. {cmpnd-0.62.0 → cmpnd-0.64.0}/scripts/selfhost_compat.py +0 -0
  143. {cmpnd-0.62.0 → cmpnd-0.64.0}/scripts/test-in-linux.sh +0 -0
  144. {cmpnd-0.62.0 → cmpnd-0.64.0}/stubs/dspy/__init__.pyi +0 -0
  145. {cmpnd-0.62.0 → cmpnd-0.64.0}/stubs/dspy/primitives/__init__.pyi +0 -0
  146. {cmpnd-0.62.0 → cmpnd-0.64.0}/stubs/dspy/primitives/prediction.pyi +0 -0
  147. {cmpnd-0.62.0 → cmpnd-0.64.0}/stubs/dspy/utils/__init__.pyi +0 -0
  148. {cmpnd-0.62.0 → cmpnd-0.64.0}/stubs/dspy/utils/callback.pyi +0 -0
  149. {cmpnd-0.62.0 → cmpnd-0.64.0}/stubs/dspy/utils/dummies.pyi +0 -0
  150. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/__init__.py +0 -0
  151. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/_credential_route.py +0 -0
  152. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/_deployment_signature_helpers.py +0 -0
  153. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/_exporter_helpers.py +0 -0
  154. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/_fake_clock.py +0 -0
  155. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/_identity_helpers.py +0 -0
  156. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/_resolve_refs.py +0 -0
  157. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/_traced_run.py +0 -0
  158. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/conftest.py +0 -0
  159. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/e2e/__init__.py +0 -0
  160. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/e2e/committee_task.py +0 -0
  161. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/e2e/conftest.py +0 -0
  162. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/e2e/data/committee_train.ndjson.gz +0 -0
  163. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/e2e/data/committee_val.ndjson.gz +0 -0
  164. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/e2e/fake_lm.py +0 -0
  165. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/e2e/helpers.py +0 -0
  166. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/e2e/target_capabilities.py +0 -0
  167. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/e2e/test_assessments_cli.py +0 -0
  168. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/e2e/test_capture_prompts.py +0 -0
  169. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/e2e/test_composite_module.py +0 -0
  170. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/e2e/test_dataset_auto_creation.py +0 -0
  171. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/e2e/test_deploy_execute.py +0 -0
  172. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/e2e/test_deploy_pandas.py +0 -0
  173. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/e2e/test_deploy_programs.py +0 -0
  174. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/e2e/test_edge_cases.py +0 -0
  175. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/e2e/test_example_hash.py +0 -0
  176. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/e2e/test_gepa_optimization.py +0 -0
  177. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/e2e/test_interpreter_spans.py +0 -0
  178. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/e2e/test_lm_accuracy.py +0 -0
  179. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/e2e/test_module_tracing.py +0 -0
  180. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/e2e/test_optimization_lineage.py +0 -0
  181. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/e2e/test_optimization_metric_identity.py +0 -0
  182. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/e2e/test_optimize_authored_metric.py +0 -0
  183. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/e2e/test_optimize_custom_proposer.py +0 -0
  184. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/e2e/test_optimize_failures.py +0 -0
  185. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/e2e/test_optimize_loop.py +0 -0
  186. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/e2e/test_optimize_review_committee.py +0 -0
  187. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/e2e/test_optimize_server_owned_completion.py +0 -0
  188. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/e2e/test_parallel_export.py +0 -0
  189. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/e2e/test_project_attribution.py +0 -0
  190. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/e2e/test_react_tool_spans.py +0 -0
  191. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/e2e/test_save_load.py +0 -0
  192. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/e2e/test_trace_structure.py +0 -0
  193. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/e2e/test_user_attribution.py +0 -0
  194. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/identity_vectors/01_predict_qa.json +0 -0
  195. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/identity_vectors/02_predict_renamed_field.json +0 -0
  196. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/identity_vectors/03_chain_of_thought_qa.json +0 -0
  197. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/identity_vectors/04_predict_richer_types.json +0 -0
  198. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/identity_vectors/05_composite_two_predicts.json +0 -0
  199. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/identity_vectors/06_multichaincomparison.json +0 -0
  200. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/identity_vectors/07_program_of_thought.json +0 -0
  201. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/identity_vectors/08_opaque_module.json +0 -0
  202. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/identity_vectors/09_predict_pydantic_field.json +0 -0
  203. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/identity_vectors/10_react.json +0 -0
  204. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/identity_vectors/11_refine.json +0 -0
  205. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/identity_vectors/12_retrieve.json +0 -0
  206. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/identity_vectors/13_knn.json +0 -0
  207. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/identity_vectors/14_parallel.json +0 -0
  208. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/identity_vectors/README.md +0 -0
  209. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/ingest_conformance.py +0 -0
  210. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/integration/__init__.py +0 -0
  211. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/integration/authored_optimize/judge.py +0 -0
  212. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/integration/authored_optimize/optimize_hosted.py +0 -0
  213. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/integration/conftest.py +0 -0
  214. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/integration/hosted_mcp_tool_optimize_probe.py +0 -0
  215. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/integration/hosted_optimize_probe.py +0 -0
  216. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/integration/optimization_run_probe.py +0 -0
  217. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/integration/test_cli_deploy_smoketest.py +0 -0
  218. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/integration/test_logs_renders_traces.py +0 -0
  219. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/integration/test_optimization_run.py +0 -0
  220. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/integration/test_optimizing_progress.py +0 -0
  221. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/integration/test_unhappy_path.py +0 -0
  222. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/load/__init__.py +0 -0
  223. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/load/__main__.py +0 -0
  224. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/load/drain.py +0 -0
  225. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/load/ingest.py +0 -0
  226. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/load/latency_transport.py +0 -0
  227. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/load/rlm_shape.py +0 -0
  228. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/load/rlm_size_report.py +0 -0
  229. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/load/sweep.py +0 -0
  230. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/load/test_rlm_shape.py +0 -0
  231. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/load/test_rlm_size_report.py +0 -0
  232. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/parity/__init__.py +0 -0
  233. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/parity/conftest.py +0 -0
  234. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/parity/seed_parity.py +0 -0
  235. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/parity/test_auth.py +0 -0
  236. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/parity/test_datasets.py +0 -0
  237. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/parity/test_deployments.py +0 -0
  238. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/parity/test_evals.py +0 -0
  239. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/parity/test_health.py +0 -0
  240. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/parity/test_modules.py +0 -0
  241. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/parity/test_optimizations.py +0 -0
  242. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/parity/test_programs.py +0 -0
  243. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/parity/test_signatures.py +0 -0
  244. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/parity/test_trace_streaming.py +0 -0
  245. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/parity/test_traces.py +0 -0
  246. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/span_reference_fixture.py +0 -0
  247. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/stress/__init__.py +0 -0
  248. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/stress/_provider.py +0 -0
  249. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/stress/_readiness.py +0 -0
  250. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/stress/_reliability.py +0 -0
  251. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/stress/_result.py +0 -0
  252. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/stress/mint_bedrock_token.py +0 -0
  253. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/stress/test_bedrock_converse.py +0 -0
  254. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/stress/test_deploy_programs.py +0 -0
  255. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/stress/test_multi_client_stress.py +0 -0
  256. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/stress/test_optimize_client_committee.py +0 -0
  257. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/stress/test_optimize_loop.py +0 -0
  258. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/stress/test_optimize_loop_wasm.py +0 -0
  259. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/stress/test_optimize_stress.py +0 -0
  260. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/stress/test_provider_roles.py +0 -0
  261. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/stress/test_readiness.py +0 -0
  262. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/stress/test_real_provider_twins.py +0 -0
  263. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/stress/test_reliability.py +0 -0
  264. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/stress/test_reliability_classify.py +0 -0
  265. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/stress/test_rlm_wasm.py +0 -0
  266. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/stress/test_trace_search.py +0 -0
  267. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_adapter_reentrancy.py +0 -0
  268. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_authored.py +0 -0
  269. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_authored_binding_credentials.py +0 -0
  270. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_authored_bundle_collisions.py +0 -0
  271. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_authored_metric_contract.py +0 -0
  272. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_authored_named_models.py +0 -0
  273. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_authored_optimize.py +0 -0
  274. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_callback.py +0 -0
  275. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_candidate_lineage.py +0 -0
  276. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_cli/__init__.py +0 -0
  277. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_cli/conftest.py +0 -0
  278. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_cli/test_admin.py +0 -0
  279. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_cli/test_api_client.py +0 -0
  280. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_cli/test_auth.py +0 -0
  281. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_cli/test_credentials.py +0 -0
  282. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_cli/test_datasets.py +0 -0
  283. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_cli/test_deployment_runs.py +0 -0
  284. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_cli/test_deployments.py +0 -0
  285. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_cli/test_login.py +0 -0
  286. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_cli/test_main.py +0 -0
  287. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_cli/test_optimizations.py +0 -0
  288. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_cli/test_pagination.py +0 -0
  289. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_cli/test_provider_credentials.py +0 -0
  290. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_cli/test_tool_servers.py +0 -0
  291. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_cli/test_tree_help.py +0 -0
  292. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_client_floor.py +0 -0
  293. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_clustering/__init__.py +0 -0
  294. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_clustering/conftest.py +0 -0
  295. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_clustering/test_edge_cases.py +0 -0
  296. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_clustering/test_hash_consistency.py +0 -0
  297. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_clustering/test_type_conversion.py +0 -0
  298. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_cmp551_grouping_identity.py +0 -0
  299. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_configuration.py +0 -0
  300. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_context.py +0 -0
  301. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_context_propagation.py +0 -0
  302. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_credential_kwargs.py +0 -0
  303. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_credential_selection.py +0 -0
  304. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_credential_values.py +0 -0
  305. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_dataset_polling.py +0 -0
  306. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_dataset_sync.py +0 -0
  307. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_datasets.py +0 -0
  308. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_decision_client_spans.py +0 -0
  309. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_decorators.py +0 -0
  310. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_deploy.py +0 -0
  311. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_deploy_mcp_tool_references.py +0 -0
  312. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_deploy_program_id.py +0 -0
  313. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_deployment_metadata.py +0 -0
  314. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_deployment_runs.py +0 -0
  315. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_deployments_read.py +0 -0
  316. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_eval.py +0 -0
  317. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_eval_example_attribution.py +0 -0
  318. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_eval_start.py +0 -0
  319. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_eval_trace_grouping.py +0 -0
  320. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_exception_paths.py +0 -0
  321. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_execute.py +0 -0
  322. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_execute_run_identity.py +0 -0
  323. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_exporter.py +0 -0
  324. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_exporter_program_names.py +0 -0
  325. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_exporter_queue.py +0 -0
  326. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_exporter_real.py +0 -0
  327. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_fake_lm.py +0 -0
  328. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_gepa_callback.py +0 -0
  329. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_gepa_integration.py +0 -0
  330. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_gepa_output_normalization.py +0 -0
  331. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_gepa_resume.py +0 -0
  332. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_identity.py +0 -0
  333. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_imaging.py +0 -0
  334. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_import_weight.py +0 -0
  335. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_ingest_conformance.py +0 -0
  336. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_ingest_second_producer.py +0 -0
  337. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_integration.py +0 -0
  338. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_interpreter_callback.py +0 -0
  339. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_ir_encoder_properties.py +0 -0
  340. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_ir_golden_vectors.py +0 -0
  341. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_ir_roundtrip.py +0 -0
  342. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_lm15_native_calls.py +0 -0
  343. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_lm_configuration.py +0 -0
  344. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_lm_usage_shapes.py +0 -0
  345. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_mcp_tools.py +0 -0
  346. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_models.py +0 -0
  347. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_multi_client_stress_readiness.py +0 -0
  348. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_optimization_progress.py +0 -0
  349. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_optimization_start.py +0 -0
  350. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_optimize.py +0 -0
  351. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_optimize_budget.py +0 -0
  352. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_optimize_lift.py +0 -0
  353. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_optimize_lms.py +0 -0
  354. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_optimize_proposer.py +0 -0
  355. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_optimize_reflection_lm.py +0 -0
  356. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_optimize_terminal_failure.py +0 -0
  357. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_packaging.py +0 -0
  358. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_packaging_callable_source.py +0 -0
  359. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_packaging_mcp_tools.py +0 -0
  360. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_packaging_program_source.py +0 -0
  361. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_poll_until.py +0 -0
  362. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_program_id_deprecation.py +0 -0
  363. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_program_identity.py +0 -0
  364. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_program_lineage_persist.py +0 -0
  365. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_program_names_in_deploy.py +0 -0
  366. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_programs_create.py +0 -0
  367. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_project_payload.py +0 -0
  368. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_real_provider_twins_readiness.py +0 -0
  369. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_reflector_properties.py +0 -0
  370. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_reflector_smoke.py +0 -0
  371. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_release_stress_script.py +0 -0
  372. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_replaced_names.py +0 -0
  373. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_repro_reactv2_bugs.py +0 -0
  374. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_require_version_bump.py +0 -0
  375. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_retry.py +0 -0
  376. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_reversible_failures.py +0 -0
  377. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_rlm_patch.py +0 -0
  378. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_run_attribution_paths.py +0 -0
  379. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_runtime_tools.py +0 -0
  380. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_schemas.py +0 -0
  381. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_seal_lm_credential.py +0 -0
  382. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_seed_review_run.py +0 -0
  383. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_signature_inputs.py +0 -0
  384. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_span_references.py +0 -0
  385. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_span_sent_once.py +0 -0
  386. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_system_one.py +0 -0
  387. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_trace_context_concurrency.py +0 -0
  388. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_trace_fact_wire_contract.py +0 -0
  389. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_trace_facts.py +0 -0
  390. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_trace_render.py +0 -0
  391. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_tracked_gepa.py +0 -0
  392. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_tracker_enrich.py +0 -0
  393. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_truncation.py +0 -0
  394. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_upload_auth_headers.py +0 -0
  395. {cmpnd-0.62.0 → cmpnd-0.64.0}/tests/test_xxh64.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cmpnd
3
- Version: 0.62.0
3
+ Version: 0.64.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>
@@ -51,9 +51,9 @@ Description-Content-Type: text/markdown
51
51
 
52
52
  # cmpnd
53
53
 
54
- The `cmpnd` package traces DSPy programs and deploys and optimizes them on the
55
- cmpnd platform. One call turns on tracing, and a client deploys, runs and
56
- optimizes named programs.
54
+ The `cmpnd` package traces, deploys and optimizes DSPy programs on the cmpnd
55
+ platform. You give a program a name, and a client records its traces,
56
+ deployments and optimization runs under that name.
57
57
 
58
58
  ## Installation
59
59
 
@@ -66,46 +66,15 @@ uv add cmpnd # or: pip install cmpnd
66
66
  The `cmpnd` library works with the cmpnd service at
67
67
  [platform.cmpnd.ai](https://platform.cmpnd.ai). Create an API key on the
68
68
  platform under Settings → API keys, and pass it as `api_key=` to
69
- `cmpnd.configure()` or `cmpnd.Client()`. For a self-hosted cmpnd install, also
69
+ `cmpnd.Client()` or `cmpnd.configure()`. For a self-hosted cmpnd install, also
70
70
  pass the install's URL as `endpoint=`.
71
71
 
72
72
  ## Quick start
73
73
 
74
- `cmpnd.auto_instrument()` records a trace of every DSPy call in the process.
75
- Your code needs no other change. Use it to sketch a [DSPy program](https://dspy.ai/) and see what it
76
- does.
77
-
78
- ```python
79
- import cmpnd
80
- import dspy
81
-
82
- cmpnd.configure(api_key="ck_xxx", project_tags="my-project")
83
- cmpnd.auto_instrument()
84
-
85
- dspy.configure(lm=dspy.LM("openai/gpt-6-luna"))
86
-
87
- cot = dspy.ChainOfThought("question -> answer")
88
- result = cot(question="What is DSPy?") # the trace goes to cmpnd
89
-
90
- cmpnd.trace_id(result) # "a1b2c3d4-...", or None outside instrumentation
91
- ```
92
-
93
- `cmpnd.trace_id()` links a result to its platform trace, for example to attach
94
- feedback later. Traces recorded this way have no program name. The platform
95
- groups them by the structure of the program's modules. The API reference
96
- below lists the other options of `cmpnd.configure()`.
97
-
98
- ## Program names
99
-
100
- When you’re ready to name programs, evolve them, and share them with your team, switch
101
- to `cmpnd.Client`. A client replaces `configure()` and `auto_instrument()` and offers
102
- more features. A client records only the programs you run through it, and each of
103
- those programs has a program name. A client takes most of the same arguments as `configure()`.
104
-
105
- A program name is the name your organization gives a program, such as `question-answerer`.
106
- Traces, deployments and optimization runs under one program name go to one
107
- program. The platform makes the program the first time your organization uses
108
- the program name.
74
+ A program name is the name your organization gives a program, such as
75
+ `question-answerer`. Traces, deployments and optimization runs under one
76
+ program name go to one program. The platform makes the program the first time
77
+ your organization uses the program name.
109
78
 
110
79
  `@cmpnd.program(name)` above a `dspy.Module` class gives the class a program
111
80
  name. Building the class takes its own arguments and a required `lm=`. The
@@ -128,17 +97,23 @@ class QA(dspy.Predict):
128
97
  qa1 = QA(lm=dspy.LM("openai/gpt-6-luna"))
129
98
  ```
130
99
 
131
- A client runs the program and records its trace under the program name:
100
+ A `cmpnd.Client` runs the program and records its trace under the program
101
+ name:
132
102
 
133
103
  ```python
134
104
  gepa = dspy.GEPA(metric=metric, reflection_lm=dspy.LM("openai/gpt-6-luna"), auto="light")
135
105
 
136
106
  with cmpnd.Client(api_key="ck_xxx", project_tags="my-project") as sdk:
137
- # the trace will go to the program "question-answerer"
107
+ # the trace goes to the program "question-answerer"
138
108
  result = sdk.run_program(qa1, question="What is DSPy?")
139
109
  qa2 = sdk.optimize_program(qa1, gepa, trainset=train, valset=val)
110
+
111
+ cmpnd.trace_id(result) # "a1b2c3d4-...", the trace of this run
140
112
  ```
141
113
 
114
+ `cmpnd.trace_id()` links a result to its platform trace, for example to attach
115
+ feedback later. A client records only the programs you run through it.
116
+
142
117
  A client is active from construction. The `with` form flushes the client's
143
118
  traces and closes it on exit. Without `with`, the client stays active until
144
119
  you call `close()` or the process exits, and its traces go out at exit:
@@ -167,8 +142,26 @@ qa3 = cmpnd.Program(
167
142
  - `optimize_program` runs the optimizer in your process and returns a copy of
168
143
  the program that holds the optimized module. The copy keeps the program name
169
144
  and the LM.
170
- - Calling a `cmpnd.Program` directly while `auto_instrument()` is on raises a
171
- `TypeError` that points to `cmpnd.Client`.
145
+
146
+ ## Sketching with `auto_instrument()`
147
+
148
+ `cmpnd.auto_instrument()` records a trace of every DSPy call in the process,
149
+ with no other change to your code. Use it to sketch a
150
+ [DSPy program](https://dspy.ai/) and see what it does:
151
+
152
+ ```python
153
+ cmpnd.configure(api_key="ck_xxx", project_tags="my-project")
154
+ cmpnd.auto_instrument()
155
+
156
+ dspy.configure(lm=dspy.LM("openai/gpt-6-luna"))
157
+ dspy.ChainOfThought("question -> answer")(question="What is DSPy?")
158
+ ```
159
+
160
+ These traces carry no program, so the platform files them under Unattributed.
161
+ The first one in a process logs a warning that points to `@cmpnd.program` and
162
+ `cmpnd.Client`. Calling a `cmpnd.Program` directly while `auto_instrument()` is
163
+ on raises a `TypeError` that points to the client. The API reference below
164
+ lists the options of `cmpnd.configure()`.
172
165
 
173
166
  ## Deployed programs
174
167
 
@@ -531,7 +524,7 @@ describes one:
531
524
 
532
525
  ```
533
526
  usage: cmpnd [-h] [--version] [--api-key API_KEY] [--endpoint ENDPOINT]
534
- {auth,login,logout,org,credentials,tool-servers,optimizations,programs,signatures,traces,evals,datasets,deployments,ps,status,logs,inspect,schema,sample,run,deploy,optimize}
527
+ {auth,login,logout,org,credentials,tool-servers,optimizations,programs,traces,evals,datasets,deployments,ps,status,logs,inspect,schema,sample,run,deploy,optimize}
535
528
  ...
536
529
 
537
530
  CMPND platform CLI — deploy, run, and inspect DSPy programs.
@@ -625,7 +618,8 @@ captured input and output size limits, 65,536 bytes each by default.
625
618
 
626
619
  ### `cmpnd.auto_instrument()`
627
620
 
628
- Registers the callback with DSPy, so every DSPy call records a trace.
621
+ Registers the callback with DSPy, so every DSPy call records a trace. The
622
+ traces carry no program and land in Unattributed.
629
623
 
630
624
  ### `cmpnd.Client()`
631
625
 
@@ -1,8 +1,8 @@
1
1
  # cmpnd
2
2
 
3
- The `cmpnd` package traces DSPy programs and deploys and optimizes them on the
4
- cmpnd platform. One call turns on tracing, and a client deploys, runs and
5
- optimizes named programs.
3
+ The `cmpnd` package traces, deploys and optimizes DSPy programs on the cmpnd
4
+ platform. You give a program a name, and a client records its traces,
5
+ deployments and optimization runs under that name.
6
6
 
7
7
  ## Installation
8
8
 
@@ -15,46 +15,15 @@ uv add cmpnd # or: pip install cmpnd
15
15
  The `cmpnd` library works with the cmpnd service at
16
16
  [platform.cmpnd.ai](https://platform.cmpnd.ai). Create an API key on the
17
17
  platform under Settings → API keys, and pass it as `api_key=` to
18
- `cmpnd.configure()` or `cmpnd.Client()`. For a self-hosted cmpnd install, also
18
+ `cmpnd.Client()` or `cmpnd.configure()`. For a self-hosted cmpnd install, also
19
19
  pass the install's URL as `endpoint=`.
20
20
 
21
21
  ## Quick start
22
22
 
23
- `cmpnd.auto_instrument()` records a trace of every DSPy call in the process.
24
- Your code needs no other change. Use it to sketch a [DSPy program](https://dspy.ai/) and see what it
25
- does.
26
-
27
- ```python
28
- import cmpnd
29
- import dspy
30
-
31
- cmpnd.configure(api_key="ck_xxx", project_tags="my-project")
32
- cmpnd.auto_instrument()
33
-
34
- dspy.configure(lm=dspy.LM("openai/gpt-6-luna"))
35
-
36
- cot = dspy.ChainOfThought("question -> answer")
37
- result = cot(question="What is DSPy?") # the trace goes to cmpnd
38
-
39
- cmpnd.trace_id(result) # "a1b2c3d4-...", or None outside instrumentation
40
- ```
41
-
42
- `cmpnd.trace_id()` links a result to its platform trace, for example to attach
43
- feedback later. Traces recorded this way have no program name. The platform
44
- groups them by the structure of the program's modules. The API reference
45
- below lists the other options of `cmpnd.configure()`.
46
-
47
- ## Program names
48
-
49
- When you’re ready to name programs, evolve them, and share them with your team, switch
50
- to `cmpnd.Client`. A client replaces `configure()` and `auto_instrument()` and offers
51
- more features. A client records only the programs you run through it, and each of
52
- those programs has a program name. A client takes most of the same arguments as `configure()`.
53
-
54
- A program name is the name your organization gives a program, such as `question-answerer`.
55
- Traces, deployments and optimization runs under one program name go to one
56
- program. The platform makes the program the first time your organization uses
57
- the program name.
23
+ A program name is the name your organization gives a program, such as
24
+ `question-answerer`. Traces, deployments and optimization runs under one
25
+ program name go to one program. The platform makes the program the first time
26
+ your organization uses the program name.
58
27
 
59
28
  `@cmpnd.program(name)` above a `dspy.Module` class gives the class a program
60
29
  name. Building the class takes its own arguments and a required `lm=`. The
@@ -77,17 +46,23 @@ class QA(dspy.Predict):
77
46
  qa1 = QA(lm=dspy.LM("openai/gpt-6-luna"))
78
47
  ```
79
48
 
80
- A client runs the program and records its trace under the program name:
49
+ A `cmpnd.Client` runs the program and records its trace under the program
50
+ name:
81
51
 
82
52
  ```python
83
53
  gepa = dspy.GEPA(metric=metric, reflection_lm=dspy.LM("openai/gpt-6-luna"), auto="light")
84
54
 
85
55
  with cmpnd.Client(api_key="ck_xxx", project_tags="my-project") as sdk:
86
- # the trace will go to the program "question-answerer"
56
+ # the trace goes to the program "question-answerer"
87
57
  result = sdk.run_program(qa1, question="What is DSPy?")
88
58
  qa2 = sdk.optimize_program(qa1, gepa, trainset=train, valset=val)
59
+
60
+ cmpnd.trace_id(result) # "a1b2c3d4-...", the trace of this run
89
61
  ```
90
62
 
63
+ `cmpnd.trace_id()` links a result to its platform trace, for example to attach
64
+ feedback later. A client records only the programs you run through it.
65
+
91
66
  A client is active from construction. The `with` form flushes the client's
92
67
  traces and closes it on exit. Without `with`, the client stays active until
93
68
  you call `close()` or the process exits, and its traces go out at exit:
@@ -116,8 +91,26 @@ qa3 = cmpnd.Program(
116
91
  - `optimize_program` runs the optimizer in your process and returns a copy of
117
92
  the program that holds the optimized module. The copy keeps the program name
118
93
  and the LM.
119
- - Calling a `cmpnd.Program` directly while `auto_instrument()` is on raises a
120
- `TypeError` that points to `cmpnd.Client`.
94
+
95
+ ## Sketching with `auto_instrument()`
96
+
97
+ `cmpnd.auto_instrument()` records a trace of every DSPy call in the process,
98
+ with no other change to your code. Use it to sketch a
99
+ [DSPy program](https://dspy.ai/) and see what it does:
100
+
101
+ ```python
102
+ cmpnd.configure(api_key="ck_xxx", project_tags="my-project")
103
+ cmpnd.auto_instrument()
104
+
105
+ dspy.configure(lm=dspy.LM("openai/gpt-6-luna"))
106
+ dspy.ChainOfThought("question -> answer")(question="What is DSPy?")
107
+ ```
108
+
109
+ These traces carry no program, so the platform files them under Unattributed.
110
+ The first one in a process logs a warning that points to `@cmpnd.program` and
111
+ `cmpnd.Client`. Calling a `cmpnd.Program` directly while `auto_instrument()` is
112
+ on raises a `TypeError` that points to the client. The API reference below
113
+ lists the options of `cmpnd.configure()`.
121
114
 
122
115
  ## Deployed programs
123
116
 
@@ -480,7 +473,7 @@ describes one:
480
473
 
481
474
  ```
482
475
  usage: cmpnd [-h] [--version] [--api-key API_KEY] [--endpoint ENDPOINT]
483
- {auth,login,logout,org,credentials,tool-servers,optimizations,programs,signatures,traces,evals,datasets,deployments,ps,status,logs,inspect,schema,sample,run,deploy,optimize}
476
+ {auth,login,logout,org,credentials,tool-servers,optimizations,programs,traces,evals,datasets,deployments,ps,status,logs,inspect,schema,sample,run,deploy,optimize}
484
477
  ...
485
478
 
486
479
  CMPND platform CLI — deploy, run, and inspect DSPy programs.
@@ -574,7 +567,8 @@ captured input and output size limits, 65,536 bytes each by default.
574
567
 
575
568
  ### `cmpnd.auto_instrument()`
576
569
 
577
- Registers the callback with DSPy, so every DSPy call records a trace.
570
+ Registers the callback with DSPy, so every DSPy call records a trace. The
571
+ traces carry no program and land in Unattributed.
578
572
 
579
573
  ### `cmpnd.Client()`
580
574
 
@@ -13,7 +13,7 @@ import os
13
13
  from collections.abc import Sequence
14
14
  from typing import Any, Literal
15
15
 
16
- from . import _http, configuration
16
+ from . import _http, configuration, programs
17
17
 
18
18
  DEFAULT_ENDPOINT = "https://platform.cmpnd.ai"
19
19
 
@@ -58,22 +58,23 @@ def list(
58
58
  ) -> dict[str, Any]:
59
59
  """List assessments by program or by trace, newest first.
60
60
 
61
- Pass exactly one of ``program`` (every assessment of that program) or
62
- ``trace_id`` (only that trace's assessments). Returns
61
+ Pass exactly one of ``program`` (every assessment of that program, named by its
62
+ name or ``p_`` id) or ``trace_id`` (only that trace's assessments). Returns
63
63
  ``{"assessments": [...], "total": N, "limit", "offset", "has_more"}``.
64
64
  ``limit`` (max rows) and ``offset`` (skip this many) page the full set — the
65
65
  server clamps ``limit`` to at most 200.
66
66
  """
67
67
  if program is not None and trace_id is not None:
68
68
  raise ValueError("pass exactly one of program or trace_id")
69
+ client = _client(api_key, endpoint)
69
70
  if trace_id is not None:
70
71
  scope: dict[str, str] = {"trace_id": trace_id}
71
72
  elif program is not None:
72
- scope = {"program": program}
73
+ scope = {"program": programs.program_id_for(client, program)}
73
74
  else:
74
75
  raise ValueError("pass exactly one of program or trace_id")
75
76
  params = {**scope, "limit": str(limit), "offset": str(offset)}
76
- return _client(api_key, endpoint).get("/api/v1/assessments", params=params)
77
+ return client.get("/api/v1/assessments", params=params)
77
78
 
78
79
 
79
80
  def get(assessment_id: str, *, api_key: str | None = None, endpoint: str | None = None) -> dict[str, Any]:
@@ -4,7 +4,7 @@ The implementation of ``list`` and ``show`` over the shared ``_http`` transport.
4
4
  :class:`cmpnd.Client`'s ``list_traces`` / ``get_trace`` call these directly; the
5
5
  deprecated :mod:`cmpnd.traces` shim wraps them with a warning. The filter and
6
6
  verb names match the ``cmpnd traces`` CLI vocabulary — ``list`` filtered by
7
- ``query`` / ``program`` / ``module`` / ``hash`` / ``deployment`` / ``since`` / ``until`` /
7
+ ``query`` / ``program`` / ``module`` / ``deployment`` / ``since`` / ``until`` /
8
8
  ``all_history``, and ``show`` for one trace — so the surfaces do not drift.
9
9
  """
10
10
 
@@ -29,7 +29,6 @@ def list(
29
29
  model: str | None = None,
30
30
  program: str | None = None,
31
31
  module: str | None = None,
32
- hash: str | None = None,
33
32
  deployment: str | None = None,
34
33
  metadata: dict[str, str] | None = None,
35
34
  tags: list[str] | None = None,
@@ -49,11 +48,10 @@ def list(
49
48
  ``ignored_fields`` naming any request field an older server did not understand.
50
49
 
51
50
  The filters mirror ``cmpnd traces list``: ``query`` searches indexed span text;
52
- ``program`` names the program by name, and ``hash`` by its program id (``p_…``)
53
- or a legacy signature hash; ``module`` is the root module's class, matched exactly.
54
- No index serves the class, so without ``program`` or a program id in ``hash`` it
55
- reads every trace in the time range and slows as the range grows. Pair it with a program or a shorter
56
- ``since``;
51
+ ``program`` names the program by its name or its program id (``p_…``);
52
+ ``module`` is the root module's class, matched exactly. No index serves the
53
+ class, so without ``program`` it reads every trace in the time range and slows
54
+ as the range grows. Pair it with a program or a shorter ``since``;
57
55
  ``deployment`` is a deployment id; ``metadata`` is matched by containment (every
58
56
  pair must be present); ``tags`` matches any of the given tags. ``since`` /
59
57
  ``until`` accept an ISO 8601 timestamp, a bare date (``YYYY-MM-DD``), or a
@@ -71,15 +69,14 @@ def list(
71
69
  ("status", status),
72
70
  ("q", query),
73
71
  ("model", model),
74
- ("program_name", program),
75
72
  ("module_class", module),
76
73
  ("deployment_id", deployment),
77
74
  ("tags", tags),
78
75
  ):
79
76
  if val is not None:
80
77
  body[wire_key] = val
81
- if hash is not None:
82
- body.update(program_filter(hash))
78
+ if program is not None:
79
+ body.update(program_filter(program))
83
80
  if metadata is not None:
84
81
  body["metadata"] = dict(metadata)
85
82
  if since is not None:
@@ -92,14 +89,14 @@ def list(
92
89
  def program_filter(value: str) -> dict[str, str]:
93
90
  """The search-body key for one program filter value.
94
91
 
95
- A program id (``p_…``) goes in ``program_id`` and a legacy signature hash in
96
- ``program_signature_hash``, which the route decodes as a typed hash and would
97
- refuse an id in. One helper for the CLI and the SDK, so the two cannot split.
92
+ A program id (``p_…``) goes in ``program_id`` and anything else is a program
93
+ name, since no name starts with ``p_``. One helper for the CLI and the SDK,
94
+ so the two cannot split.
98
95
  """
99
96
  text = value.strip()
100
97
  if identity.is_program_id(text):
101
98
  return {"program_id": text}
102
- return {"program_signature_hash": value}
99
+ return {"program_name": value}
103
100
 
104
101
 
105
102
  def show(trace_id: str, *, api_key: str | None = None, endpoint: str | None = None) -> dict[str, Any]:
@@ -15,7 +15,7 @@ import datetime
15
15
  import os
16
16
  from typing import Any
17
17
 
18
- from . import _http, configuration
18
+ from . import _http, configuration, programs
19
19
 
20
20
  DEFAULT_ENDPOINT = "https://platform.cmpnd.ai"
21
21
 
@@ -30,22 +30,24 @@ def default_name() -> str:
30
30
  def create(
31
31
  program: str, *, name: str | None = None, api_key: str | None = None, endpoint: str | None = None
32
32
  ) -> dict[str, Any]:
33
- """Create a program-owned valset. ``name`` is auto-assigned when omitted."""
34
- payload = {"program": program, "name": name or default_name()}
35
- return _client(api_key, endpoint).post("/api/v1/valsets", json=payload)
33
+ """Create a valset owned by ``program``, a program name or ``p_`` id. ``name`` is auto-assigned when omitted."""
34
+ client = _client(api_key, endpoint)
35
+ payload = {"program": programs.program_id_for(client, program), "name": name or default_name()}
36
+ return client.post("/api/v1/valsets", json=payload)
36
37
 
37
38
 
38
39
  def list(
39
40
  program: str, *, limit: int = 50, offset: int = 0, api_key: str | None = None, endpoint: str | None = None
40
41
  ) -> dict[str, Any]:
41
- """List a program's valsets, newest first.
42
+ """List the valsets of ``program``, a program name or ``p_`` id, newest first.
42
43
 
43
44
  Returns ``{"valsets": [...], "total": N, "limit", "offset", "has_more"}``.
44
45
  ``limit`` / ``offset`` page the full set — the server clamps ``limit`` to at
45
46
  most 200.
46
47
  """
47
- params = {"program": program, "limit": str(limit), "offset": str(offset)}
48
- return _client(api_key, endpoint).get("/api/v1/valsets", params=params)
48
+ client = _client(api_key, endpoint)
49
+ params = {"program": programs.program_id_for(client, program), "limit": str(limit), "offset": str(offset)}
50
+ return client.get("/api/v1/valsets", params=params)
49
51
 
50
52
 
51
53
  def get(valset_id: str, *, api_key: str | None = None, endpoint: str | None = None) -> dict[str, Any]:
@@ -39,6 +39,27 @@ def _declared_program_name() -> str | None:
39
39
  return name if isinstance(name, str) else None
40
40
 
41
41
 
42
+ # Set once the process has warned that a trace names no program, so the warning
43
+ # appears once per process, whichever callback records the trace.
44
+ _warned_unattributed = False
45
+
46
+
47
+ def _warn_once_if_unattributed(trace: models.TraceBuilder) -> None:
48
+ """Warn the first time a trace in this process names neither a program name nor a program id.
49
+
50
+ Such a trace belongs to no program, and the platform files it under Unattributed.
51
+ """
52
+ global _warned_unattributed
53
+ if _warned_unattributed or trace.declared_name is not None or trace.program_id is not None:
54
+ return
55
+ _warned_unattributed = True
56
+ logger.warning(
57
+ "cmpnd: this trace names no program, so the platform files it under Unattributed. Give the program a "
58
+ "name with @cmpnd.program(name) and run it through cmpnd.Client, and its traces go to that program. "
59
+ "This warning appears once per process."
60
+ )
61
+
62
+
42
63
  def trace_id(prediction: Any) -> str | None:
43
64
  """Return the cmpnd trace id for a prediction returned by an instrumented program.
44
65
 
@@ -1193,6 +1214,10 @@ class _ModuleMixin(_CallbackBase):
1193
1214
  module_class=instance.__class__.__name__,
1194
1215
  )
1195
1216
  self._stamp(trace)
1217
+ # A root module call is where a program runs, so it is where an unnamed one
1218
+ # is caught. GEPA's own LM calls (_emit_optimization_lm_trace) belong to the
1219
+ # optimization run, which names the program, so they never warn.
1220
+ _warn_once_if_unattributed(trace)
1196
1221
 
1197
1222
  # Attach the run ids this trace starts under, from this callback's own
1198
1223
  # state right now — not at finalize (on_module_end), where the
@@ -24,7 +24,6 @@ from .commands import (
24
24
  org,
25
25
  programs,
26
26
  provider_credentials,
27
- signatures,
28
27
  tool_servers,
29
28
  top,
30
29
  traces,
@@ -164,7 +163,6 @@ def main() -> None:
164
163
  # gates each read on scope, so a caller without it gets a 403, not a
165
164
  # hidden command.
166
165
  programs.register(subparsers)
167
- signatures.register(subparsers)
168
166
  traces.register(subparsers)
169
167
  evals.register(subparsers)
170
168
  datasets.register(subparsers)
@@ -56,7 +56,8 @@ def register(subparsers: _SubParsersAction[ArgumentParser]) -> None:
56
56
  scope.add_argument(
57
57
  "program",
58
58
  nargs="?",
59
- help="Program id (p_…), or a legacy signature hash (16-hex, as `cmpnd signatures list` shows)",
59
+ metavar="PROGRAM",
60
+ help="Program name or p_ id",
60
61
  )
61
62
  scope.add_argument("--trace", dest="trace_id", metavar="TRACE_ID", help="A single trace's assessments")
62
63
  pagination.add_args(ls, with_page_aliases=False, default_limit=50)
@@ -88,7 +89,15 @@ def handle_create(client: api_client.ApiClient, args: Any) -> Any:
88
89
 
89
90
 
90
91
  def handle_list(client: api_client.ApiClient, args: Any) -> Any:
91
- scope = {"trace_id": args.trace_id} if args.trace_id else {"program": args.program}
92
+ if args.trace_id:
93
+ scope = {"trace_id": args.trace_id}
94
+ else:
95
+ from . import programs
96
+
97
+ program_id, _ = programs.resolve(
98
+ client, args.program, old_server_remedy="Upgrade the server to name a program."
99
+ )
100
+ scope = {"program": program_id}
92
101
  params = {**scope, **pagination.wire(args, "limit_offset", as_str=True)}
93
102
  return client.get("/api/v1/assessments", params=params)
94
103
 
@@ -50,7 +50,7 @@ def register(subparsers: _SubParsersAction[ArgumentParser]) -> None:
50
50
  # `evals compare` is deliberately absent. The route requires a
51
51
  # `dataset_hash`, and no eval read returns one: neither the list nor the
52
52
  # detail projection carries it, and nor does the compare response itself.
53
- # The only surface that emits one is `signatures evals`, so the command
53
+ # The only surface that emits one is `programs evals`, so the command
54
54
  # would ship with a required argument its own noun cannot produce. It goes
55
55
  # in once the projection carries the hash.
56
56
 
@@ -29,17 +29,21 @@ adds one) and `switch` picks which stored key later commands use, the way the
29
29
  web app switches a session's active org. `list` reads
30
30
  /api/v1/auth/organizations and marks the orgs you hold a key for and the
31
31
  active one. `switch` needs no network call when the org's key is already
32
- stored. `cmpnd org leave` removes your own membership and drops that org's
33
- stored key.
32
+ stored. When none is stored for an org you are an approved member of, it
33
+ offers to mint one with the current key's scopes (`--mint` skips the
34
+ question), stores it and switches to it. `cmpnd org leave` removes your own
35
+ membership and drops that org's stored key.
34
36
 
35
37
  Always registered (no env gate); the server is the authority.
36
38
  """
37
39
 
38
40
  from __future__ import annotations
39
41
 
42
+ import socket
40
43
  import sys
41
44
  import urllib.parse
42
45
  from argparse import ArgumentParser, _SubParsersAction
46
+ from datetime import datetime, timezone
43
47
  from typing import Any
44
48
 
45
49
  from .. import api_client, credentials
@@ -91,6 +95,11 @@ def register(subparsers: _SubParsersAction[ArgumentParser]) -> None:
91
95
 
92
96
  switch = sub.add_parser("switch", help="Use the stored key for another of your organizations")
93
97
  switch.add_argument("org", help="Organization slug or id (from `org list`)")
98
+ switch.add_argument(
99
+ "--mint",
100
+ action="store_true",
101
+ help="When no key is stored for the org, mint one with the current key's scopes without asking",
102
+ )
94
103
  switch.set_defaults(func=handle_switch, _skip_resolve_config=True)
95
104
 
96
105
  leave = sub.add_parser("leave", help="Leave one of your organizations and drop its stored key")
@@ -214,12 +223,70 @@ def handle_switch(_unused_client: api_client.ApiClient | None, args: Any) -> Non
214
223
  api_key, _ = api_client.resolve_config(args)
215
224
  except api_client.CliError:
216
225
  raise _no_key_stored(f"'{target}'", endpoint, target) from None
217
- for row in _memberships(api_client.ApiClient(api_key, endpoint)):
226
+ client = api_client.ApiClient(api_key, endpoint)
227
+ for row in _memberships(client):
218
228
  if target in (row.get("id"), row.get("slug")):
219
- raise _no_key_stored(str(row.get("name") or target), endpoint, target)
229
+ _mint_and_switch(client, row, target, ask=not getattr(args, "mint", False))
230
+ return
220
231
  raise api_client.CliError(f"You are not a member of an organization '{target}' on {endpoint}.")
221
232
 
222
233
 
234
+ def _mint_and_switch(client: api_client.ApiClient, row: dict[str, Any], target: str, *, ask: bool) -> None:
235
+ """Mint a key for a member org that has none stored, store it and switch.
236
+
237
+ The request names no scopes, so serve grants the calling key's own
238
+ non-admin scopes and the new key is never broader than the one in use.
239
+ serve refuses with 400 or 403 when the calling key cannot mint, and the
240
+ browser login is then the way to get the key.
241
+ """
242
+ endpoint = client._endpoint
243
+ label = str(row.get("name") or row.get("slug") or target)
244
+ if row.get("approved") is False:
245
+ raise api_client.CliError(
246
+ f"Your membership in {label} is not approved yet, so no key can be minted for it. "
247
+ "An owner of the org approves it."
248
+ )
249
+ if ask:
250
+ if not sys.stdin.isatty():
251
+ raise api_client.CliError(
252
+ f"No key is stored for {label} on {endpoint}. Run {top.hint_org_switch(target, mint=True)} "
253
+ f"to mint one with the current key's scopes, or {top.hint_login(target)}."
254
+ )
255
+ try:
256
+ answer = input(f"No key is stored for {label}. Mint one with the current key's scopes? [Y/n] ")
257
+ except EOFError:
258
+ # Ctrl-D at the prompt declines.
259
+ print(file=sys.stderr)
260
+ answer = "n"
261
+ if answer.strip().lower() not in ("", "y", "yes"):
262
+ raise _no_key_stored(label, endpoint, target)
263
+
264
+ hostname = socket.gethostname() or "unknown"
265
+ name = f"CLI: {hostname} {datetime.now(timezone.utc).strftime('%Y-%m-%d')}"
266
+ try:
267
+ created = client.post("/api/v1/org/keys", json={"name": name, "org_id": row.get("id") or target})
268
+ except api_client.CliError as exc:
269
+ if exc.status_code in (400, 403):
270
+ raise api_client.CliError(
271
+ f"The current key could not mint a key for {label}: {exc}. Run {top.hint_login(target)} to store one."
272
+ ) from exc
273
+ raise
274
+ token = created.get("token") if isinstance(created, dict) else None
275
+ org_id = created.get("org_id") if isinstance(created, dict) else None
276
+ if not isinstance(token, str) or not token or not isinstance(org_id, str) or not org_id:
277
+ raise api_client.CliError(f"The response did not include a key for {label}. Nothing was stored.")
278
+ name_out = created.get("org_name")
279
+ slug_out = created.get("org_slug")
280
+ credentials.save(
281
+ endpoint,
282
+ token,
283
+ org_id=org_id,
284
+ org_name=name_out if isinstance(name_out, str) else None,
285
+ org_slug=slug_out if isinstance(slug_out, str) else None,
286
+ )
287
+ print(f"Minted a key for {name_out or slug_out or org_id} ({org_id}) and switched to it on {endpoint}")
288
+
289
+
223
290
  def _say_active_moved(endpoint: str, moved_to: str | None) -> None:
224
291
  if moved_to:
225
292
  k = {k.org_id: k for k in credentials.stored_keys(endpoint)}.get(moved_to)