cmpnd 0.60.0__tar.gz → 0.62.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 (398) hide show
  1. {cmpnd-0.60.0 → cmpnd-0.62.0}/PKG-INFO +1 -1
  2. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/_traces.py +7 -2
  3. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/callback.py +2 -2
  4. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/cli/commands/programs.py +214 -51
  5. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/cli/commands/top.py +17 -4
  6. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/cli/commands/traces.py +40 -1
  7. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/cli/commands/valsets.py +29 -4
  8. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/cli/shell.py +6 -4
  9. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/cli/trace_render.py +29 -2
  10. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/models.py +9 -3
  11. {cmpnd-0.60.0 → cmpnd-0.62.0}/docs/cli.md +71 -14
  12. {cmpnd-0.60.0 → cmpnd-0.62.0}/docs/reading-traces.md +3 -2
  13. {cmpnd-0.60.0 → cmpnd-0.62.0}/docs/sdk-instrumentation.md +7 -0
  14. {cmpnd-0.60.0 → cmpnd-0.62.0}/ingest/cmpnd_ingest/trace_facts.py +8 -3
  15. {cmpnd-0.60.0 → cmpnd-0.62.0}/pyproject.toml +1 -1
  16. {cmpnd-0.60.0 → cmpnd-0.62.0}/scripts/capture_review_run.py +5 -5
  17. cmpnd-0.62.0/scripts/fixtures/review_run.json.gz +0 -0
  18. {cmpnd-0.60.0 → cmpnd-0.62.0}/scripts/seed_review_run.py +5 -5
  19. cmpnd-0.62.0/tests/e2e/target_capabilities.py +58 -0
  20. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/e2e/test_composite_module.py +1 -1
  21. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/e2e/test_deploy_execute.py +0 -3
  22. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/e2e/test_deploy_pandas.py +6 -0
  23. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/e2e/test_deploy_programs.py +5 -0
  24. cmpnd-0.62.0/tests/e2e/test_optimize_authored_metric.py +78 -0
  25. cmpnd-0.62.0/tests/e2e/test_optimize_custom_proposer.py +158 -0
  26. cmpnd-0.62.0/tests/e2e/test_optimize_failures.py +69 -0
  27. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/e2e/test_optimize_server_owned_completion.py +7 -1
  28. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/e2e/test_parallel_export.py +2 -2
  29. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/e2e/test_project_attribution.py +1 -1
  30. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/load/rlm_shape.py +1 -1
  31. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/parity/test_trace_streaming.py +1 -1
  32. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/parity/test_traces.py +4 -4
  33. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/span_reference_fixture.py +1 -1
  34. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/stress/_provider.py +19 -25
  35. cmpnd-0.62.0/tests/stress/test_bedrock_converse.py +127 -0
  36. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/stress/test_multi_client_stress.py +15 -5
  37. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_callback.py +4 -4
  38. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_cli/test_programs.py +247 -38
  39. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_cli/test_shell.py +3 -1
  40. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_cli/test_traces.py +2 -2
  41. cmpnd-0.62.0/tests/test_cli/test_traces_program_and_module.py +274 -0
  42. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_cli/test_traces_search_wire.py +4 -1
  43. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_cli/test_valsets.py +66 -3
  44. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_client.py +2 -2
  45. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_client_program_names.py +2 -2
  46. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_context_propagation.py +1 -1
  47. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_eval_example_attribution.py +6 -6
  48. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_eval_trace_grouping.py +3 -3
  49. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_exporter.py +3 -3
  50. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_exporter_program_names.py +1 -1
  51. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_exporter_queue.py +1 -1
  52. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_exporter_real.py +3 -3
  53. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_ingest_conformance.py +1 -1
  54. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_integration.py +1 -1
  55. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_models.py +6 -6
  56. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_multi_client_stress_readiness.py +6 -5
  57. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_program_identity.py +1 -1
  58. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_project_payload.py +7 -7
  59. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_repro_reactv2_bugs.py +1 -1
  60. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_run_attribution_paths.py +1 -1
  61. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_schemas.py +1 -1
  62. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_trace_fact_wire_contract.py +1 -1
  63. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_trace_facts.py +8 -8
  64. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_trace_render.py +3 -2
  65. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_truncation.py +2 -2
  66. {cmpnd-0.60.0 → cmpnd-0.62.0}/uv.lock +1 -1
  67. cmpnd-0.60.0/scripts/fixtures/review_run.json.gz +0 -0
  68. cmpnd-0.60.0/tests/e2e/target_capabilities.py +0 -70
  69. cmpnd-0.60.0/tests/e2e/test_optimize_failures.py +0 -136
  70. {cmpnd-0.60.0 → cmpnd-0.62.0}/.gitignore +0 -0
  71. {cmpnd-0.60.0 → cmpnd-0.62.0}/AGENTS.md +0 -0
  72. {cmpnd-0.60.0 → cmpnd-0.62.0}/CLAUDE.md +0 -0
  73. {cmpnd-0.60.0 → cmpnd-0.62.0}/README.md +0 -0
  74. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/__init__.py +0 -0
  75. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/_assessments.py +0 -0
  76. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/_authored_import.py +0 -0
  77. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/_deployments.py +0 -0
  78. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/_exporter_pool.py +0 -0
  79. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/_gepa_patch.py +0 -0
  80. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/_http.py +0 -0
  81. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/_program_patch.py +0 -0
  82. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/_rlm_patch.py +0 -0
  83. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/_runtime_tools.py +0 -0
  84. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/_timespec.py +0 -0
  85. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/_valsets.py +0 -0
  86. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/authored.py +0 -0
  87. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/cli/__init__.py +0 -0
  88. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/cli/api_client.py +0 -0
  89. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/cli/commands/__init__.py +0 -0
  90. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/cli/commands/admin.py +0 -0
  91. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/cli/commands/assessments.py +0 -0
  92. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/cli/commands/auth.py +0 -0
  93. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/cli/commands/datasets.py +0 -0
  94. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/cli/commands/deployments.py +0 -0
  95. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/cli/commands/evals.py +0 -0
  96. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/cli/commands/login.py +0 -0
  97. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/cli/commands/optimizations.py +0 -0
  98. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/cli/commands/org.py +0 -0
  99. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/cli/commands/provider_credentials.py +0 -0
  100. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/cli/commands/signatures.py +0 -0
  101. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/cli/commands/tool_servers.py +0 -0
  102. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/cli/credentials.py +0 -0
  103. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/cli/deprecation.py +0 -0
  104. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/cli/pagination.py +0 -0
  105. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/cli/tree_help.py +0 -0
  106. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/cli/verbs.py +0 -0
  107. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/client.py +0 -0
  108. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/configuration.py +0 -0
  109. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/context.py +0 -0
  110. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/credentials.py +0 -0
  111. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/dataset_sync.py +0 -0
  112. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/datasets.py +0 -0
  113. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/decorators.py +0 -0
  114. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/deployment.py +0 -0
  115. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/deployment_metadata.py +0 -0
  116. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/deployment_runs.py +0 -0
  117. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/eval_handler.py +0 -0
  118. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/execution.py +0 -0
  119. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/exporter.py +0 -0
  120. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/exporter_http.py +0 -0
  121. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/helpers.py +0 -0
  122. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/identity.py +0 -0
  123. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/imaging.py +0 -0
  124. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/ir/__init__.py +0 -0
  125. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/ir/decode.py +0 -0
  126. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/ir/encode.py +0 -0
  127. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/ir/hash.py +0 -0
  128. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/ir/program.py +0 -0
  129. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/ir/reflection.py +0 -0
  130. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/ir/types.py +0 -0
  131. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/ir/xxh64.py +0 -0
  132. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/lm_configuration.py +0 -0
  133. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/mcp_servers.py +0 -0
  134. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/named_programs.py +0 -0
  135. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/optimization.py +0 -0
  136. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/optimizers/__init__.py +0 -0
  137. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/optimizers/gepa.py +0 -0
  138. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/optimizers/gepa_callback.py +0 -0
  139. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/optimizers/resume.py +0 -0
  140. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/optimizers/tracker.py +0 -0
  141. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/packaging.py +0 -0
  142. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/programs.py +0 -0
  143. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/references.py +0 -0
  144. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/retry.py +0 -0
  145. {cmpnd-0.60.0 → cmpnd-0.62.0}/cmpnd/traces.py +0 -0
  146. {cmpnd-0.60.0 → cmpnd-0.62.0}/docs/2026-04-02-code-review.md +0 -0
  147. {cmpnd-0.60.0 → cmpnd-0.62.0}/docs/client.md +0 -0
  148. {cmpnd-0.60.0 → cmpnd-0.62.0}/docs/custom-proposer.md +0 -0
  149. {cmpnd-0.60.0 → cmpnd-0.62.0}/docs/plans/2026-04-05-train-val-dataset-capture-design.md +0 -0
  150. {cmpnd-0.60.0 → cmpnd-0.62.0}/docs/plans/2026-04-05-train-val-dataset-capture.md +0 -0
  151. {cmpnd-0.60.0 → cmpnd-0.62.0}/examples/generalizing_proposer.py +0 -0
  152. {cmpnd-0.60.0 → cmpnd-0.62.0}/examples/local_ollama.py +0 -0
  153. {cmpnd-0.60.0 → cmpnd-0.62.0}/ingest/LINEAGE.md +0 -0
  154. {cmpnd-0.60.0 → cmpnd-0.62.0}/ingest/README.md +0 -0
  155. {cmpnd-0.60.0 → cmpnd-0.62.0}/ingest/cmpnd_ingest/__init__.py +0 -0
  156. {cmpnd-0.60.0 → cmpnd-0.62.0}/ingest/cmpnd_ingest/bodies.py +0 -0
  157. {cmpnd-0.60.0 → cmpnd-0.62.0}/ingest/cmpnd_ingest/client_floor.py +0 -0
  158. {cmpnd-0.60.0 → cmpnd-0.62.0}/ingest/cmpnd_ingest/exporter_http.py +0 -0
  159. {cmpnd-0.60.0 → cmpnd-0.62.0}/ingest/cmpnd_ingest/program_names.py +0 -0
  160. {cmpnd-0.60.0 → cmpnd-0.62.0}/ingest/cmpnd_ingest/protocols.py +0 -0
  161. {cmpnd-0.60.0 → cmpnd-0.62.0}/ingest/cmpnd_ingest/py.typed +0 -0
  162. {cmpnd-0.60.0 → cmpnd-0.62.0}/ingest/cmpnd_ingest/request_log.py +0 -0
  163. {cmpnd-0.60.0 → cmpnd-0.62.0}/ingest/cmpnd_ingest/retry.py +0 -0
  164. {cmpnd-0.60.0 → cmpnd-0.62.0}/ingest/pyproject.toml +0 -0
  165. {cmpnd-0.60.0 → cmpnd-0.62.0}/scripts/audit-sdist.sh +0 -0
  166. {cmpnd-0.60.0 → cmpnd-0.62.0}/scripts/check-pip-install.sh +0 -0
  167. {cmpnd-0.60.0 → cmpnd-0.62.0}/scripts/check_release_artifacts.py +0 -0
  168. {cmpnd-0.60.0 → cmpnd-0.62.0}/scripts/gepa_resume_bug.py +0 -0
  169. {cmpnd-0.60.0 → cmpnd-0.62.0}/scripts/install-test-env.sh +0 -0
  170. {cmpnd-0.60.0 → cmpnd-0.62.0}/scripts/release-stress.sh +0 -0
  171. {cmpnd-0.60.0 → cmpnd-0.62.0}/scripts/require_version_bump.py +0 -0
  172. {cmpnd-0.60.0 → cmpnd-0.62.0}/scripts/selfhost_compat.py +0 -0
  173. {cmpnd-0.60.0 → cmpnd-0.62.0}/scripts/test-in-linux.sh +0 -0
  174. {cmpnd-0.60.0 → cmpnd-0.62.0}/stubs/dspy/__init__.pyi +0 -0
  175. {cmpnd-0.60.0 → cmpnd-0.62.0}/stubs/dspy/primitives/__init__.pyi +0 -0
  176. {cmpnd-0.60.0 → cmpnd-0.62.0}/stubs/dspy/primitives/prediction.pyi +0 -0
  177. {cmpnd-0.60.0 → cmpnd-0.62.0}/stubs/dspy/utils/__init__.pyi +0 -0
  178. {cmpnd-0.60.0 → cmpnd-0.62.0}/stubs/dspy/utils/callback.pyi +0 -0
  179. {cmpnd-0.60.0 → cmpnd-0.62.0}/stubs/dspy/utils/dummies.pyi +0 -0
  180. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/__init__.py +0 -0
  181. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/_credential_route.py +0 -0
  182. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/_deployment_signature_helpers.py +0 -0
  183. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/_exporter_helpers.py +0 -0
  184. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/_fake_clock.py +0 -0
  185. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/_identity_helpers.py +0 -0
  186. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/_resolve_refs.py +0 -0
  187. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/_traced_run.py +0 -0
  188. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/conftest.py +0 -0
  189. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/e2e/__init__.py +0 -0
  190. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/e2e/committee_task.py +0 -0
  191. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/e2e/conftest.py +0 -0
  192. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/e2e/data/committee_train.ndjson.gz +0 -0
  193. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/e2e/data/committee_val.ndjson.gz +0 -0
  194. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/e2e/fake_lm.py +0 -0
  195. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/e2e/helpers.py +0 -0
  196. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/e2e/test_assessments_cli.py +0 -0
  197. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/e2e/test_capture_prompts.py +0 -0
  198. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/e2e/test_dataset_auto_creation.py +0 -0
  199. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/e2e/test_edge_cases.py +0 -0
  200. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/e2e/test_example_hash.py +0 -0
  201. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/e2e/test_gepa_optimization.py +0 -0
  202. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/e2e/test_interpreter_spans.py +0 -0
  203. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/e2e/test_lm_accuracy.py +0 -0
  204. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/e2e/test_module_tracing.py +0 -0
  205. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/e2e/test_optimization_lineage.py +0 -0
  206. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/e2e/test_optimization_metric_identity.py +0 -0
  207. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/e2e/test_optimize_loop.py +0 -0
  208. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/e2e/test_optimize_review_committee.py +0 -0
  209. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/e2e/test_react_tool_spans.py +0 -0
  210. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/e2e/test_save_load.py +0 -0
  211. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/e2e/test_trace_structure.py +0 -0
  212. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/e2e/test_traces_read.py +0 -0
  213. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/e2e/test_user_attribution.py +0 -0
  214. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/e2e/test_valsets_cli.py +0 -0
  215. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/identity_vectors/01_predict_qa.json +0 -0
  216. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/identity_vectors/02_predict_renamed_field.json +0 -0
  217. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/identity_vectors/03_chain_of_thought_qa.json +0 -0
  218. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/identity_vectors/04_predict_richer_types.json +0 -0
  219. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/identity_vectors/05_composite_two_predicts.json +0 -0
  220. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/identity_vectors/06_multichaincomparison.json +0 -0
  221. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/identity_vectors/07_program_of_thought.json +0 -0
  222. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/identity_vectors/08_opaque_module.json +0 -0
  223. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/identity_vectors/09_predict_pydantic_field.json +0 -0
  224. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/identity_vectors/10_react.json +0 -0
  225. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/identity_vectors/11_refine.json +0 -0
  226. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/identity_vectors/12_retrieve.json +0 -0
  227. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/identity_vectors/13_knn.json +0 -0
  228. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/identity_vectors/14_parallel.json +0 -0
  229. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/identity_vectors/README.md +0 -0
  230. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/ingest_conformance.py +0 -0
  231. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/integration/__init__.py +0 -0
  232. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/integration/authored_optimize/judge.py +0 -0
  233. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/integration/authored_optimize/optimize_hosted.py +0 -0
  234. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/integration/conftest.py +0 -0
  235. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/integration/hosted_mcp_tool_optimize_probe.py +0 -0
  236. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/integration/hosted_optimize_probe.py +0 -0
  237. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/integration/optimization_run_probe.py +0 -0
  238. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/integration/test_cli_deploy_smoketest.py +0 -0
  239. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/integration/test_logs_renders_traces.py +0 -0
  240. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/integration/test_optimization_run.py +0 -0
  241. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/integration/test_optimizing_progress.py +0 -0
  242. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/integration/test_unhappy_path.py +0 -0
  243. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/load/__init__.py +0 -0
  244. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/load/__main__.py +0 -0
  245. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/load/drain.py +0 -0
  246. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/load/ingest.py +0 -0
  247. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/load/latency_transport.py +0 -0
  248. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/load/rlm_size_report.py +0 -0
  249. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/load/sweep.py +0 -0
  250. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/load/test_rlm_shape.py +0 -0
  251. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/load/test_rlm_size_report.py +0 -0
  252. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/parity/__init__.py +0 -0
  253. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/parity/conftest.py +0 -0
  254. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/parity/seed_parity.py +0 -0
  255. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/parity/test_auth.py +0 -0
  256. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/parity/test_datasets.py +0 -0
  257. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/parity/test_deployments.py +0 -0
  258. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/parity/test_evals.py +0 -0
  259. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/parity/test_health.py +0 -0
  260. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/parity/test_modules.py +0 -0
  261. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/parity/test_optimizations.py +0 -0
  262. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/parity/test_programs.py +0 -0
  263. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/parity/test_signatures.py +0 -0
  264. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/stress/__init__.py +0 -0
  265. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/stress/_readiness.py +0 -0
  266. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/stress/_reliability.py +0 -0
  267. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/stress/_result.py +0 -0
  268. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/stress/mint_bedrock_token.py +0 -0
  269. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/stress/test_deploy_programs.py +0 -0
  270. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/stress/test_optimize_client_committee.py +0 -0
  271. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/stress/test_optimize_loop.py +0 -0
  272. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/stress/test_optimize_loop_wasm.py +0 -0
  273. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/stress/test_optimize_stress.py +0 -0
  274. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/stress/test_provider_roles.py +0 -0
  275. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/stress/test_readiness.py +0 -0
  276. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/stress/test_real_provider_twins.py +0 -0
  277. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/stress/test_reliability.py +0 -0
  278. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/stress/test_reliability_classify.py +0 -0
  279. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/stress/test_rlm_wasm.py +0 -0
  280. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/stress/test_trace_search.py +0 -0
  281. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_adapter_reentrancy.py +0 -0
  282. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_assessments_api.py +0 -0
  283. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_authored.py +0 -0
  284. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_authored_binding_credentials.py +0 -0
  285. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_authored_bundle_collisions.py +0 -0
  286. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_authored_metric_contract.py +0 -0
  287. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_authored_named_models.py +0 -0
  288. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_authored_optimize.py +0 -0
  289. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_candidate_lineage.py +0 -0
  290. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_cli/__init__.py +0 -0
  291. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_cli/conftest.py +0 -0
  292. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_cli/test_admin.py +0 -0
  293. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_cli/test_api_client.py +0 -0
  294. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_cli/test_assessments.py +0 -0
  295. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_cli/test_auth.py +0 -0
  296. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_cli/test_credentials.py +0 -0
  297. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_cli/test_datasets.py +0 -0
  298. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_cli/test_deployment_runs.py +0 -0
  299. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_cli/test_deployments.py +0 -0
  300. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_cli/test_evals.py +0 -0
  301. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_cli/test_login.py +0 -0
  302. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_cli/test_main.py +0 -0
  303. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_cli/test_optimizations.py +0 -0
  304. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_cli/test_org.py +0 -0
  305. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_cli/test_pagination.py +0 -0
  306. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_cli/test_parity.py +0 -0
  307. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_cli/test_provider_credentials.py +0 -0
  308. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_cli/test_schema_sample.py +0 -0
  309. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_cli/test_signatures.py +0 -0
  310. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_cli/test_tool_servers.py +0 -0
  311. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_cli/test_top.py +0 -0
  312. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_cli/test_tree_help.py +0 -0
  313. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_client_floor.py +0 -0
  314. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_clustering/__init__.py +0 -0
  315. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_clustering/conftest.py +0 -0
  316. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_clustering/test_edge_cases.py +0 -0
  317. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_clustering/test_hash_consistency.py +0 -0
  318. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_clustering/test_type_conversion.py +0 -0
  319. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_cmp551_grouping_identity.py +0 -0
  320. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_configuration.py +0 -0
  321. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_context.py +0 -0
  322. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_credential_kwargs.py +0 -0
  323. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_credential_selection.py +0 -0
  324. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_credential_values.py +0 -0
  325. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_dataset_polling.py +0 -0
  326. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_dataset_sync.py +0 -0
  327. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_datasets.py +0 -0
  328. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_decision_client_spans.py +0 -0
  329. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_decorators.py +0 -0
  330. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_deploy.py +0 -0
  331. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_deploy_mcp_tool_references.py +0 -0
  332. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_deploy_program_id.py +0 -0
  333. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_deployment_metadata.py +0 -0
  334. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_deployment_runs.py +0 -0
  335. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_deployments_read.py +0 -0
  336. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_eval.py +0 -0
  337. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_eval_start.py +0 -0
  338. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_exception_paths.py +0 -0
  339. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_execute.py +0 -0
  340. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_execute_run_identity.py +0 -0
  341. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_fake_lm.py +0 -0
  342. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_gepa_callback.py +0 -0
  343. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_gepa_integration.py +0 -0
  344. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_gepa_output_normalization.py +0 -0
  345. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_gepa_resume.py +0 -0
  346. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_identity.py +0 -0
  347. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_imaging.py +0 -0
  348. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_import_weight.py +0 -0
  349. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_ingest_second_producer.py +0 -0
  350. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_interpreter_callback.py +0 -0
  351. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_ir_encoder_properties.py +0 -0
  352. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_ir_golden_vectors.py +0 -0
  353. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_ir_roundtrip.py +0 -0
  354. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_lm15_native_calls.py +0 -0
  355. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_lm_configuration.py +0 -0
  356. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_lm_usage_shapes.py +0 -0
  357. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_mcp_tools.py +0 -0
  358. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_optimization_progress.py +0 -0
  359. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_optimization_start.py +0 -0
  360. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_optimize.py +0 -0
  361. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_optimize_budget.py +0 -0
  362. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_optimize_lift.py +0 -0
  363. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_optimize_lms.py +0 -0
  364. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_optimize_proposer.py +0 -0
  365. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_optimize_reflection_lm.py +0 -0
  366. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_optimize_terminal_failure.py +0 -0
  367. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_packaging.py +0 -0
  368. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_packaging_callable_source.py +0 -0
  369. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_packaging_mcp_tools.py +0 -0
  370. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_packaging_program_source.py +0 -0
  371. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_poll_until.py +0 -0
  372. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_program_id_deprecation.py +0 -0
  373. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_program_lineage_persist.py +0 -0
  374. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_program_names_in_deploy.py +0 -0
  375. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_programs_create.py +0 -0
  376. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_real_provider_twins_readiness.py +0 -0
  377. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_reflector_properties.py +0 -0
  378. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_reflector_smoke.py +0 -0
  379. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_release_stress_script.py +0 -0
  380. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_replaced_names.py +0 -0
  381. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_require_version_bump.py +0 -0
  382. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_retry.py +0 -0
  383. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_reversible_failures.py +0 -0
  384. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_rlm_patch.py +0 -0
  385. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_runtime_tools.py +0 -0
  386. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_seal_lm_credential.py +0 -0
  387. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_seed_review_run.py +0 -0
  388. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_signature_inputs.py +0 -0
  389. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_span_references.py +0 -0
  390. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_span_sent_once.py +0 -0
  391. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_system_one.py +0 -0
  392. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_trace_context_concurrency.py +0 -0
  393. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_traces_read.py +0 -0
  394. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_tracked_gepa.py +0 -0
  395. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_tracker_enrich.py +0 -0
  396. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_upload_auth_headers.py +0 -0
  397. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_valsets_api.py +0 -0
  398. {cmpnd-0.60.0 → cmpnd-0.62.0}/tests/test_xxh64.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cmpnd
3
- Version: 0.60.0
3
+ Version: 0.62.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>
@@ -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`` / ``hash`` / ``deployment`` / ``since`` / ``until`` /
7
+ ``query`` / ``program`` / ``module`` / ``hash`` / ``deployment`` / ``since`` / ``until`` /
8
8
  ``all_history``, and ``show`` for one trace — so the surfaces do not drift.
9
9
  """
10
10
 
@@ -28,6 +28,7 @@ def list(
28
28
  query: str | None = None,
29
29
  model: str | None = None,
30
30
  program: str | None = None,
31
+ module: str | None = None,
31
32
  hash: str | None = None,
32
33
  deployment: str | None = None,
33
34
  metadata: dict[str, str] | None = None,
@@ -49,7 +50,10 @@ def list(
49
50
 
50
51
  The filters mirror ``cmpnd traces list``: ``query`` searches indexed span text;
51
52
  ``program`` names the program by name, and ``hash`` by its program id (``p_…``)
52
- or a legacy signature hash;
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``;
53
57
  ``deployment`` is a deployment id; ``metadata`` is matched by containment (every
54
58
  pair must be present); ``tags`` matches any of the given tags. ``since`` /
55
59
  ``until`` accept an ISO 8601 timestamp, a bare date (``YYYY-MM-DD``), or a
@@ -68,6 +72,7 @@ def list(
68
72
  ("q", query),
69
73
  ("model", model),
70
74
  ("program_name", program),
75
+ ("module_class", module),
71
76
  ("deployment_id", deployment),
72
77
  ("tags", tags),
73
78
  ):
@@ -1190,7 +1190,7 @@ class _ModuleMixin(_CallbackBase):
1190
1190
  trace = models.TraceBuilder(
1191
1191
  trace_id=uuid4(),
1192
1192
  start_time=datetime.now(timezone.utc),
1193
- program_name=instance.__class__.__name__,
1193
+ module_class=instance.__class__.__name__,
1194
1194
  )
1195
1195
  self._stamp(trace)
1196
1196
 
@@ -1633,7 +1633,7 @@ class _LMMixin(_CallbackBase):
1633
1633
  trace_id=span.trace_id or uuid4(),
1634
1634
  start_time=span.start_time,
1635
1635
  end_time=span.end_time,
1636
- program_name="GEPA",
1636
+ module_class="GEPA",
1637
1637
  )
1638
1638
  self._stamp(trace)
1639
1639
  # A metric-phase call can start while DSPy's Evaluate span is still on the
@@ -13,12 +13,12 @@ from .. import api_client, pagination
13
13
  from . import provider_credentials
14
14
 
15
15
 
16
- # The hash that show, traces, stats, usage and select take is the value `programs optimized list`
17
- # prints as `program_hash`, which the routes resolve as a *compiled* program hash — they look it up
18
- # against optimization runs and map it to a program signature hash internally.
19
- # The two hashes are different values, so a real signature hash 404s here. The
20
- # argument help says which one it wants rather than naming the concept the
21
- # route resolves it to, because the caller has only the former to paste.
16
+ # `show`, `traces`, `stats` and `usage` take a program name or a p_ id, as the
17
+ # Trash commands do. The `programs optimized` group and `select` take a compiled
18
+ # program hash, the value `programs optimized list` prints as `program_hash`.
19
+ # The routes resolve that hash against optimization runs, so a program
20
+ # signature hash 404s there. A compiled hash and a program name have the same
21
+ # shape, so no command takes both in one slot.
22
22
  def register(subparsers: _SubParsersAction[ArgumentParser]) -> None:
23
23
  parser = subparsers.add_parser("programs", help="Create and browse programs")
24
24
  sub = parser.add_subparsers(dest="action")
@@ -83,7 +83,8 @@ def register(subparsers: _SubParsersAction[ArgumentParser]) -> None:
83
83
  help="List optimized programs by compiled program hash",
84
84
  description=(
85
85
  "List the compiled programs your optimization runs produced, one row per compiled program hash, with "
86
- "their run counts and scores. `programs show`, `traces`, `stats` and `usage` take the hash from the "
86
+ "their run counts and scores. `programs optimized show`, `traces`, `stats` and `usage` take the hash "
87
+ "from the "
87
88
  "program_hash column."
88
89
  ),
89
90
  )
@@ -94,37 +95,54 @@ def register(subparsers: _SubParsersAction[ArgumentParser]) -> None:
94
95
  pagination.add_args(opt_list, with_page_aliases=True, default_limit=50)
95
96
  opt_list.set_defaults(func=handle_optimized_list)
96
97
 
97
- show = sub.add_parser("show", help="Show program detail")
98
- show.add_argument(
99
- "id", help="Compiled program hash, as `programs optimized list` prints it in the program_hash column"
98
+ opt_show = optimized_sub.add_parser("show", help="Show the optimization runs that produced a compiled program")
99
+ opt_show.add_argument("hash", help=_COMPILED_HASH_HELP)
100
+ opt_show.set_defaults(func=handle_optimized_show)
101
+
102
+ opt_traces = optimized_sub.add_parser("traces", help="List the traces of a compiled program's program")
103
+ opt_traces.add_argument("hash", help=_COMPILED_HASH_HELP)
104
+ pagination.add_args(opt_traces, with_page_aliases=False, default_limit=50)
105
+ opt_traces.set_defaults(func=handle_optimized_traces)
106
+
107
+ opt_stats = optimized_sub.add_parser("stats", help="Show the execution statistics of a compiled program's program")
108
+ opt_stats.add_argument("hash", help=_COMPILED_HASH_HELP)
109
+ opt_stats.set_defaults(func=handle_optimized_stats)
110
+
111
+ opt_usage = optimized_sub.add_parser("usage", help="Show the usage over time of a compiled program's program")
112
+ opt_usage.add_argument("hash", help=_COMPILED_HASH_HELP)
113
+ _add_usage_window(opt_usage)
114
+ opt_usage.set_defaults(func=handle_optimized_usage)
115
+
116
+ show = sub.add_parser(
117
+ "show",
118
+ help="Show a program",
119
+ description=(
120
+ "Show PROGRAM: its name, p_ id, whether it is in the Trash, its trace count and latest trace, and "
121
+ "the optimized programs its optimization runs produced."
122
+ ),
100
123
  )
124
+ show.add_argument("program", metavar="PROGRAM", help="Program name or p_ id")
101
125
  show.set_defaults(func=handle_show)
102
126
 
103
- tr = sub.add_parser("traces", help="List traces for a program")
104
- tr.add_argument(
105
- "id", help="Compiled program hash, as `programs optimized list` prints it in the program_hash column"
127
+ tr = sub.add_parser(
128
+ "traces",
129
+ help="List a program's traces, newest first, over the widest window trace search reads",
130
+ description=(
131
+ "List PROGRAM's traces, newest first, through trace search over its widest window, 365 days. "
132
+ "`cmpnd traces list --hash ID --since DATE` sets the window itself. --limit is at most 500."
133
+ ),
106
134
  )
135
+ tr.add_argument("program", metavar="PROGRAM", help="Program name or p_ id")
107
136
  pagination.add_args(tr, with_page_aliases=False, default_limit=50)
108
137
  tr.set_defaults(func=handle_traces)
109
138
 
110
- stats = sub.add_parser("stats", help="Show a program's execution statistics")
111
- stats.add_argument(
112
- "id", help="Compiled program hash, as `programs optimized list` prints it in the program_hash column"
113
- )
139
+ stats = sub.add_parser("stats", help="Show a program's execution statistics over all history")
140
+ stats.add_argument("program", metavar="PROGRAM", help="Program name or p_ id")
114
141
  stats.set_defaults(func=handle_stats)
115
142
 
116
143
  usage = sub.add_parser("usage", help="Show a program's trace volume, tokens and cost over time")
117
- usage.add_argument(
118
- "id", help="Compiled program hash, as `programs optimized list` prints it in the program_hash column"
119
- )
120
- usage.add_argument("--hours", type=int, default=24, help="Rolling window in hours (default %(default)s)")
121
- # Constrained on purpose: an unrecognised period buckets hourly server-side
122
- # rather than failing, so a typo would answer a different question silently.
123
- usage.add_argument(
124
- "--period",
125
- choices=["minute", "5m", "10m", "30m", "hour", "day"],
126
- help="Bucket width (default: hour)",
127
- )
144
+ usage.add_argument("program", metavar="PROGRAM", help="Program name or p_ id")
145
+ _add_usage_window(usage)
128
146
  usage.set_defaults(func=handle_usage)
129
147
 
130
148
  # The registry is the programs an SDK client registered, as distinct from
@@ -173,13 +191,32 @@ def register(subparsers: _SubParsersAction[ArgumentParser]) -> None:
173
191
  purge.set_defaults(func=handle_purge)
174
192
 
175
193
  select = sub.add_parser("select", help="Pin an optimization run as this program's chosen version")
176
- select.add_argument(
177
- "id", help="Compiled program hash, as `programs optimized list` prints it in the program_hash column"
178
- )
194
+ select.add_argument("id", help=_COMPILED_HASH_HELP)
179
195
  select.add_argument("--run", dest="run_id", required=True, help="Optimization run ID (op_...) to pin")
180
196
  select.set_defaults(func=handle_select)
181
197
 
182
198
 
199
+ _COMPILED_HASH_HELP = "Compiled program hash, as `programs optimized list` prints it in the program_hash column"
200
+
201
+
202
+ def _add_usage_window(parser: ArgumentParser) -> None:
203
+ parser.add_argument("--hours", type=int, default=24, help="Rolling window in hours (default %(default)s)")
204
+ # Constrained on purpose: an unrecognised period buckets hourly server-side
205
+ # rather than failing, so a typo would answer a different question silently.
206
+ parser.add_argument(
207
+ "--period",
208
+ choices=["minute", "5m", "10m", "30m", "hour", "day"],
209
+ help="Bucket width (default: hour)",
210
+ )
211
+
212
+
213
+ def _usage_params(args: Any) -> dict[str, str]:
214
+ params = {"hours": str(args.hours)}
215
+ if args.period:
216
+ params["period"] = args.period
217
+ return params
218
+
219
+
183
220
  def handle_create(client: api_client.ApiClient, args: Any) -> Any:
184
221
  """Create the program named ``args.name``, and refuse a name that already has one.
185
222
 
@@ -246,7 +283,7 @@ def handle_list(client: api_client.ApiClient, args: Any) -> Any:
246
283
  ]:
247
284
  if getattr(args, attr, default) != default:
248
285
  raise api_client.CliError(f"{flag} does not apply to --trashed, which lists the whole Trash")
249
- return client.get("/api/v1/programs/trash")
286
+ return _trash_listing(client)
250
287
  params = pagination.wire(args, "limit_offset", as_str=True)
251
288
  # Read as text: a server from before the program list, such as self-host release 2026-001, redirects this
252
289
  # path to its dispatcher, and the redirect's body is HTML.
@@ -310,24 +347,125 @@ def handle_optimized_list(client: api_client.ApiClient, args: Any) -> Any:
310
347
  return client.get("/api/v1/programs/v2", params=params)
311
348
 
312
349
 
350
+ # A server from before program names, p_ ids and the Trash API, such as self-host release 2026-001, reads every
351
+ # path under /api/v1/programs/ as a compiled program hash. A GET there answers 404 and a POST answers 405, and its
352
+ # trace search ignores program_id. A current server answers the Trash listing with 200, so that read tells them
353
+ # apart once a command has already failed.
354
+ _OLD_SERVER = "this cmpnd server predates program names, p_ ids and the Trash API, as self-host release 2026-001 does."
355
+
356
+
357
+ def _predates_program_ids(client: api_client.ApiClient) -> bool:
358
+ try:
359
+ client.get("/api/v1/programs/trash")
360
+ except api_client.CliError as exc:
361
+ return exc.status_code == 404
362
+ return False
363
+
364
+
365
+ def _old_server_refusal(remedy: str) -> api_client.CliError:
366
+ return api_client.CliError(f"{_OLD_SERVER} {remedy}")
367
+
368
+
369
+ def _refuse_if_old(client: api_client.ApiClient, exc: api_client.CliError, remedy: str) -> None:
370
+ """Raise the old-server refusal in place of ``exc`` when the server predates program ids."""
371
+ if exc.status_code in (404, 405) and _predates_program_ids(client):
372
+ raise _old_server_refusal(remedy) from exc
373
+
374
+
375
+ def _trash_remedy() -> str:
376
+ return "Manage its Trash in the web app, at /ui/account/trash, or upgrade the server."
377
+
378
+
379
+ def _read_remedy(verb: str) -> str:
380
+ from . import top
381
+
382
+ return (
383
+ f"`{top.hint_programs('optimized', verb, 'HASH')}` reads a compiled program hash there, and "
384
+ f"`{top.hint_programs('list')}` lists the programs it recorded traces for."
385
+ )
386
+
387
+
388
+ def _resolve_for_read(client: api_client.ApiClient, ref: str, verb: str, *, trashed_ok: bool = False) -> str:
389
+ """Return the p_ id of the program ``ref`` names, for a read of its activity.
390
+
391
+ A program in the Trash has no activity to read, so it is refused unless
392
+ ``trashed_ok``. An unknown name may be a compiled program hash, which the
393
+ ``programs optimized`` command of the same verb takes.
394
+ """
395
+ from . import top
396
+
397
+ program_id, trashed = resolve(
398
+ client,
399
+ ref,
400
+ unknown_hint=f"A compiled program hash goes to `{top.hint_programs('optimized', verb, ref)}`.",
401
+ old_server_remedy=_read_remedy(verb),
402
+ )
403
+ if trashed and not trashed_ok:
404
+ raise api_client.CliError(
405
+ f"{ref} is in the Trash, so it has no activity to read. "
406
+ f"Restore it with `{top.hint_programs('restore', ref)}`."
407
+ )
408
+ return program_id
409
+
410
+
411
+ def _read(client: api_client.ApiClient, verb: str, path: str, params: dict[str, str] | None = None) -> Any:
412
+ try:
413
+ return client.get(path) if params is None else client.get(path, params=params)
414
+ except api_client.CliError as exc:
415
+ _refuse_if_old(client, exc, _read_remedy(verb))
416
+ raise
417
+
418
+
313
419
  def handle_show(client: api_client.ApiClient, args: Any) -> Any:
314
- return client.get(f"/api/v1/programs/{args.id}")
420
+ program_id = _resolve_for_read(client, args.program, "show", trashed_ok=True)
421
+ return _read(client, "show", f"/api/v1/programs/{program_id}")
422
+
423
+
424
+ # Trace search reads a page_size above this as its default of 50, which would answer other rows than the ones asked
425
+ # for.
426
+ _MAX_TRACE_PAGE = 500
315
427
 
316
428
 
317
429
  def handle_traces(client: api_client.ApiClient, args: Any) -> Any:
318
- params = pagination.wire(args, "limit_offset", as_str=True)
319
- return client.get(f"/api/v1/programs/{args.id}/traces", params=params)
430
+ """List the program's traces through trace search, which takes a program id, over its widest window."""
431
+ if args.limit > _MAX_TRACE_PAGE:
432
+ raise api_client.CliError(
433
+ f"--limit is at most {_MAX_TRACE_PAGE}, the largest page trace search returns. Page with --offset."
434
+ )
435
+ program_id = _resolve_for_read(client, args.program, "traces")
436
+ body: dict[str, Any] = {"program_id": program_id, **pagination.wire(args, "page_size"), "max_window": True}
437
+ answer = client.post("/api/v1/traces/search", json=body)
438
+ # A server that ignores program_id answers every trace in the org, which must not pass for the program's.
439
+ if "program_id" in (answer.get("ignored_fields") or []):
440
+ raise _old_server_refusal(_read_remedy("traces"))
441
+ return answer
320
442
 
321
443
 
322
444
  def handle_stats(client: api_client.ApiClient, args: Any) -> Any:
323
- return client.get(f"/api/v1/programs/{args.id}/execution-stats")
445
+ program_id = _resolve_for_read(client, args.program, "stats")
446
+ return _read(client, "stats", f"/api/v1/programs/{program_id}/execution-stats")
324
447
 
325
448
 
326
449
  def handle_usage(client: api_client.ApiClient, args: Any) -> Any:
327
- params = {"hours": str(args.hours)}
328
- if args.period:
329
- params["period"] = args.period
330
- return client.get(f"/api/v1/programs/{args.id}/usage-history", params=params)
450
+ program_id = _resolve_for_read(client, args.program, "usage")
451
+ return _read(client, "usage", f"/api/v1/programs/{program_id}/usage-history", _usage_params(args))
452
+
453
+
454
+ def handle_optimized_show(client: api_client.ApiClient, args: Any) -> Any:
455
+ return client.get(f"/api/v1/programs/{args.hash}")
456
+
457
+
458
+ def handle_optimized_traces(client: api_client.ApiClient, args: Any) -> Any:
459
+ params = pagination.wire(args, "limit_offset", as_str=True)
460
+ return client.get(f"/api/v1/programs/{args.hash}/traces", params=params)
461
+
462
+
463
+ def handle_optimized_stats(client: api_client.ApiClient, args: Any) -> Any:
464
+ return client.get(f"/api/v1/programs/{args.hash}/execution-stats")
465
+
466
+
467
+ def handle_optimized_usage(client: api_client.ApiClient, args: Any) -> Any:
468
+ return client.get(f"/api/v1/programs/{args.hash}/usage-history", params=_usage_params(args))
331
469
 
332
470
 
333
471
  def handle_registry_list(client: api_client.ApiClient, args: Any) -> Any:
@@ -351,12 +489,17 @@ def handle_select(client: api_client.ApiClient, args: Any) -> Any:
351
489
  return client.post(f"/api/v1/programs/{args.id}/select", json={"optimization_run_id": args.run_id})
352
490
 
353
491
 
354
- def _resolve(client: api_client.ApiClient, ref: str) -> tuple[str, bool | None]:
492
+ def resolve(
493
+ client: api_client.ApiClient, ref: str, *, unknown_hint: str = "", old_server_remedy: str = ""
494
+ ) -> tuple[str, bool | None]:
355
495
  """Return the program id ``ref`` names, and whether it is in the Trash when known.
356
496
 
357
497
  A ``p_`` id is used as given, because no program name starts with ``p_``, and its
358
498
  Trash state is unknown until the route answers. A name is looked up, which never
359
- makes a program, and the answer says whether it is in the Trash.
499
+ makes a program, and the answer says whether it is in the Trash. ``unknown_hint``
500
+ follows the error for a name no program has. On a server from before program
501
+ names the lookup fails too, and the error is the old-server refusal with
502
+ ``old_server_remedy``.
360
503
  """
361
504
  from ... import programs
362
505
 
@@ -370,7 +513,9 @@ def _resolve(client: api_client.ApiClient, ref: str) -> tuple[str, bool | None]:
370
513
  answer = client.get("/api/v1/programs/by-name/" + urllib.parse.quote(ref, safe=""))
371
514
  except api_client.CliError as exc:
372
515
  if exc.status_code == 404:
373
- raise api_client.CliError(f"no program in your organization is named {ref!r}") from exc
516
+ _refuse_if_old(client, exc, old_server_remedy)
517
+ message = f"no program in your organization is named {ref!r}"
518
+ raise api_client.CliError(f"{message}. {unknown_hint}" if unknown_hint else message) from exc
374
519
  if exc.status_code == 410:
375
520
  raise api_client.CliError(f"{ref!r} belongs to a purged program, which nothing restores") from exc
376
521
  raise
@@ -381,21 +526,21 @@ def handle_trash(client: api_client.ApiClient, args: Any) -> Any:
381
526
  """Move a program to the Trash, which frees its place under the plan's program limit."""
382
527
  from . import top
383
528
 
384
- program_id, trashed = _resolve(client, args.program)
529
+ program_id, trashed = resolve(client, args.program, old_server_remedy=_trash_remedy())
385
530
  if trashed:
386
531
  print(
387
532
  f"{args.program} is already in the Trash. Restore it with `{top.hint_programs('restore', args.program)}`.",
388
533
  file=sys.stderr,
389
534
  )
390
535
  return {"program_id": program_id, "trashed": True}
391
- return client.post(f"/api/v1/programs/{program_id}/trash")
536
+ return _trash_write(client, f"/api/v1/programs/{program_id}/trash")
392
537
 
393
538
 
394
539
  def handle_restore(client: api_client.ApiClient, args: Any) -> Any:
395
540
  """Return a program from the Trash. The server refuses it when the plan's program limit is full."""
396
541
  from . import top
397
542
 
398
- program_id, trashed = _resolve(client, args.program)
543
+ program_id, trashed = resolve(client, args.program, old_server_remedy=_trash_remedy())
399
544
  if trashed is False:
400
545
  # A name another program took from a Trashed one reaches the new, live program. The Trashed one is
401
546
  # restored by its id.
@@ -403,7 +548,7 @@ def handle_restore(client: api_client.ApiClient, args: Any) -> Any:
403
548
  f"{args.program} is not in the Trash. If its name now belongs to another program, restore the one "
404
549
  f"in the Trash by the p_ id that `{top.hint_programs('list', '--trashed')}` prints."
405
550
  )
406
- return client.post(f"/api/v1/programs/{program_id}/restore")
551
+ return _trash_write(client, f"/api/v1/programs/{program_id}/restore")
407
552
 
408
553
 
409
554
  def handle_purge(client: api_client.ApiClient, args: Any) -> Any:
@@ -417,20 +562,38 @@ def handle_purge(client: api_client.ApiClient, args: Any) -> Any:
417
562
  # The count is for the person confirming. --force skips the listing, which needs traces:read, so a key
418
563
  # that may only write can still empty the Trash.
419
564
  if not args.force:
420
- listed = client.get("/api/v1/programs/trash")
565
+ listed = _trash_listing(client)
421
566
  count = len(listed.get("programs") or [])
422
567
  if count == 0:
423
568
  return {"purged": 0, "kept": 0}
424
569
  noun = "program" if count == 1 else "programs"
425
570
  provider_credentials.confirm(args, f"the {count} {noun} in the Trash for good", consequence)
426
- return client.post("/api/v1/programs/trash/empty")
571
+ return _trash_write(client, "/api/v1/programs/trash/empty")
427
572
  if args.program is None:
428
573
  raise api_client.CliError("name the PROGRAM to purge, or pass --all to empty the Trash")
429
- program_id, trashed = _resolve(client, args.program)
574
+ program_id, trashed = resolve(client, args.program, old_server_remedy=_trash_remedy())
430
575
  if trashed is False:
431
576
  raise api_client.CliError(
432
577
  f"{args.program} is live, and only a program in the Trash can be purged. "
433
578
  f"Move it there first with `{top.hint_programs('trash', args.program)}`."
434
579
  )
435
580
  provider_credentials.confirm(args, f"the program {args.program} for good", consequence)
436
- return client.post(f"/api/v1/programs/{program_id}/purge")
581
+ return _trash_write(client, f"/api/v1/programs/{program_id}/purge")
582
+
583
+
584
+ def _trash_listing(client: api_client.ApiClient) -> Any:
585
+ """GET the Trash. On a server from before the Trash API it answers 404, which is the old-server signal."""
586
+ try:
587
+ return client.get("/api/v1/programs/trash")
588
+ except api_client.CliError as exc:
589
+ if exc.status_code == 404:
590
+ raise _old_server_refusal(_trash_remedy()) from exc
591
+ raise
592
+
593
+
594
+ def _trash_write(client: api_client.ApiClient, path: str) -> Any:
595
+ try:
596
+ return client.post(path)
597
+ except api_client.CliError as exc:
598
+ _refuse_if_old(client, exc, _trash_remedy())
599
+ raise
@@ -18,7 +18,7 @@ from typing import Any, NoReturn
18
18
 
19
19
  from ... import _deployments, deployment_runs
20
20
  from ...deployment_metadata import DeploymentMetadataError
21
- from .. import api_client, pagination
21
+ from .. import api_client, pagination, trace_render
22
22
  from . import deployments as deployment_commands
23
23
  from . import signatures
24
24
 
@@ -338,8 +338,20 @@ def render_deployments_table(deployments: list[dict[str, Any]]) -> str:
338
338
  return "\n".join(lines)
339
339
 
340
340
 
341
+ def trace_program_cells(trace: dict[str, Any]) -> tuple[str, str]:
342
+ """The PROGRAM and MODULE cells for one trace record.
343
+
344
+ PROGRAM is the name the user gave the program, or a dash when it has none.
345
+ MODULE is the root module's class, or a dash when the trace carries no
346
+ class. `trace_render.program_and_module` reads an older server's shape,
347
+ where `program_name` holds the class.
348
+ """
349
+ name, module = trace_render.program_and_module(trace)
350
+ return name or "—", module or "—"
351
+
352
+
341
353
  def render_traces_table(traces: list[dict[str, Any]]) -> str:
342
- """Return a colored TRACE / STATUS / PROGRAM / AGE table as a string.
354
+ """Return a colored TRACE / STATUS / PROGRAM / MODULE / AGE table as a string.
343
355
 
344
356
  Reused by the shell when typing `d.traces` at the REPL (via
345
357
  `Trace._render_table`). Mirrors `render_deployments_table` so the
@@ -353,13 +365,13 @@ def render_traces_table(traces: list[dict[str, Any]]) -> str:
353
365
  (
354
366
  id_render(t["trace_id"]),
355
367
  t.get("status", "—"),
356
- _truncate(t.get("program_name") or _hash_fallback(t.get("program_signature_hash")), 28),
368
+ *(_truncate(cell, 28) for cell in trace_program_cells(t)),
357
369
  _age(t.get("start_time")),
358
370
  )
359
371
  for t in traces
360
372
  ]
361
373
 
362
- headers = ("TRACE", "STATUS", "PROGRAM", "AGE")
374
+ headers = ("TRACE", "STATUS", "PROGRAM", "MODULE", "AGE")
363
375
  widths = [max(len(h), max((len(r[i]) for r in rows), default=0)) for i, h in enumerate(headers)]
364
376
 
365
377
  header_line = " ".join(h.ljust(widths[i]) for i, h in enumerate(headers))
@@ -370,6 +382,7 @@ def render_traces_table(traces: list[dict[str, Any]]) -> str:
370
382
  _trace_status(r[1]).ljust(widths[1] + (len(_trace_status(r[1])) - len(r[1]))),
371
383
  r[2].ljust(widths[2]),
372
384
  r[3].ljust(widths[3]),
385
+ r[4].ljust(widths[4]),
373
386
  ]
374
387
  lines.append(" ".join(cells))
375
388
  return "\n".join(lines)
@@ -28,6 +28,16 @@ def _metadata_pair(value: str) -> tuple[str, str]:
28
28
  return key, val
29
29
 
30
30
 
31
+ # The wait after which a class search with no program filter gets a hint.
32
+ MODULE_SLOW_HINT_SECONDS = 5.0
33
+
34
+ # The search-body keys that name a program, whichever spelling the flag took.
35
+ _PROGRAM_FILTER_KEYS = ("program_name", "program_id", "program_signature_hash")
36
+
37
+ # The clock the hint measures with, a seam for tests.
38
+ _monotonic = time.monotonic
39
+
40
+
31
41
  def _add_list_args(p: ArgumentParser) -> None:
32
42
  p.add_argument("--status", help="Filter by status (ok, error, incomplete, in_progress)")
33
43
  p.add_argument(
@@ -49,7 +59,20 @@ def _add_list_args(p: ArgumentParser) -> None:
49
59
  help="Filter by a metadata pair your program recorded; repeat for all-of. "
50
60
  "This is how you find a trace by your own identifier",
51
61
  )
52
- p.add_argument("--program", dest="program_name", help="Filter by program name")
62
+ p.add_argument(
63
+ "--program",
64
+ dest="program_name",
65
+ metavar="NAME",
66
+ help="Filter by the program's name, the one given with cmpnd.Program or `cmpnd programs create`",
67
+ )
68
+ p.add_argument(
69
+ "--module",
70
+ dest="module_class",
71
+ metavar="CLASS",
72
+ help="Filter by the root module's class (exact, e.g. ReAct). No index serves the class: "
73
+ "without --program or --hash it reads every trace in the time range, so it gets slower "
74
+ "as the range grows. Narrow it with --program, --hash or --since",
75
+ )
53
76
  p.add_argument(
54
77
  "--hash",
55
78
  dest="program_signature_hash",
@@ -189,6 +212,7 @@ def handle_list(client: api_client.ApiClient, args: Any) -> Any:
189
212
  ("q", "q"),
190
213
  ("model", "model"),
191
214
  ("program_name", "program_name"),
215
+ ("module_class", "module_class"),
192
216
  ("deployment_id", "deployment_id"),
193
217
  ("tags", "tags"),
194
218
  ("start_time_from", "start_time_from"),
@@ -211,7 +235,22 @@ def handle_list(client: api_client.ApiClient, args: Any) -> Any:
211
235
  body["start_time_from"] = _timespec.resolve_search_time(str(body["start_time_from"]))
212
236
  if "start_time_to" in body:
213
237
  body["start_time_to"] = _timespec.resolve_search_time(str(body["start_time_to"]), end_of_day=True)
238
+ started = _monotonic()
214
239
  resp = client.post("/api/v1/traces/search", json=body)
240
+ # No index serves the class, so a class search with no program filter reads
241
+ # every trace in the range. Say what narrows it once the wait is long enough
242
+ # to notice, on stderr so stdout stays the response.
243
+ if (
244
+ "module_class" in body
245
+ and not any(key in body for key in _PROGRAM_FILTER_KEYS)
246
+ and _monotonic() - started > MODULE_SLOW_HINT_SECONDS
247
+ ):
248
+ print(
249
+ "note: --module reads every trace in the time range when no program is given. "
250
+ "Consider filtering by program (--program or --hash) or a shorter range (--since) "
251
+ "for faster results.",
252
+ file=sys.stderr,
253
+ )
215
254
 
216
255
  # A window the caller did not ask for has to be visible: read as an org
217
256
  # figure, a windowed total is simply wrong, and nothing in the numbers says
@@ -8,6 +8,7 @@ inputs and expected outputs of one validation example.
8
8
  from __future__ import annotations
9
9
 
10
10
  import json
11
+ import re
11
12
  from argparse import ArgumentParser, _SubParsersAction
12
13
  from typing import Any
13
14
 
@@ -23,7 +24,7 @@ def register(subparsers: _SubParsersAction[ArgumentParser]) -> None:
23
24
  create.add_argument(
24
25
  "program_hash",
25
26
  metavar="program",
26
- help="Owning program: a program id (p_…), or a legacy signature hash "
27
+ help="Owning program: a program name, a program id (p_…), or a legacy signature hash "
27
28
  "(16-hex, as `cmpnd signatures list` shows)",
28
29
  )
29
30
  create.add_argument(
@@ -33,7 +34,7 @@ def register(subparsers: _SubParsersAction[ArgumentParser]) -> None:
33
34
  create.set_defaults(func=handle_create)
34
35
 
35
36
  ls = sub.add_parser("list", help="List a program's validation sets")
36
- ls.add_argument("program", help="Program id (p_…), or a legacy signature hash (16-hex)")
37
+ ls.add_argument("program", help="Program name, program id (p_…), or a legacy signature hash (16-hex)")
37
38
  pagination.add_args(ls, with_page_aliases=False, default_limit=50)
38
39
  ls.set_defaults(func=handle_list)
39
40
 
@@ -75,15 +76,39 @@ def register(subparsers: _SubParsersAction[ArgumentParser]) -> None:
75
76
  edelete.set_defaults(func=handle_entries_delete)
76
77
 
77
78
 
79
+ # A legacy program signature hash in its display form: 16 hex digits.
80
+ _LEGACY_HASH = re.compile(r"[0-9a-fA-F]{16}")
81
+
82
+
83
+ def _program(client: api_client.ApiClient, ref: str) -> str:
84
+ """The value the valsets routes take for ``ref``.
85
+
86
+ A p_ id and a legacy 16-hex hash pass unchanged. Anything else is a program
87
+ name, which resolves to its p_ id without making a program. A program whose
88
+ name is 16 hex digits is reached by its p_ id.
89
+ """
90
+ from . import programs
91
+
92
+ if _LEGACY_HASH.fullmatch(ref):
93
+ return ref
94
+ program_id, _ = programs.resolve(
95
+ client,
96
+ ref,
97
+ old_server_remedy="Name the program by its 16-hex signature hash, as `cmpnd signatures list` shows, "
98
+ "or upgrade the server.",
99
+ )
100
+ return program_id
101
+
102
+
78
103
  def handle_create(client: api_client.ApiClient, args: Any) -> Any:
79
104
  # One generator (cmpnd._valsets.default_name) for both the CLI and the
80
105
  # Client, so the collision-avoiding suffix can't drift between them.
81
106
  name = args.name or _valsets.default_name()
82
- return client.post("/api/v1/valsets", json={"program": args.program_hash, "name": name})
107
+ return client.post("/api/v1/valsets", json={"program": _program(client, args.program_hash), "name": name})
83
108
 
84
109
 
85
110
  def handle_list(client: api_client.ApiClient, args: Any) -> Any:
86
- params = {"program": args.program, **pagination.wire(args, "limit_offset", as_str=True)}
111
+ params = {"program": _program(client, args.program), **pagination.wire(args, "limit_offset", as_str=True)}
87
112
  return client.get("/api/v1/valsets", params=params)
88
113
 
89
114