cmpnd 0.32.0__tar.gz → 0.33.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 (357) hide show
  1. {cmpnd-0.32.0 → cmpnd-0.33.0}/PKG-INFO +1 -1
  2. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/callback.py +8 -4
  3. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/cli/commands/traces.py +7 -1
  4. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/models.py +149 -79
  5. {cmpnd-0.32.0 → cmpnd-0.33.0}/docs/sdk-instrumentation.md +18 -2
  6. {cmpnd-0.32.0 → cmpnd-0.33.0}/ingest/README.md +15 -1
  7. {cmpnd-0.32.0 → cmpnd-0.33.0}/ingest/cmpnd_ingest/bodies.py +24 -34
  8. {cmpnd-0.32.0 → cmpnd-0.33.0}/ingest/cmpnd_ingest/exporter_http.py +20 -6
  9. cmpnd-0.33.0/ingest/cmpnd_ingest/trace_facts.py +144 -0
  10. {cmpnd-0.32.0 → cmpnd-0.33.0}/pyproject.toml +1 -1
  11. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/e2e/test_user_attribution.py +15 -28
  12. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_cli/test_traces.py +4 -1
  13. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_import_weight.py +1 -0
  14. cmpnd-0.33.0/tests/test_trace_fact_wire_contract.py +69 -0
  15. cmpnd-0.33.0/tests/test_trace_facts.py +363 -0
  16. {cmpnd-0.32.0 → cmpnd-0.33.0}/uv.lock +1 -1
  17. {cmpnd-0.32.0 → cmpnd-0.33.0}/.gitignore +0 -0
  18. {cmpnd-0.32.0 → cmpnd-0.33.0}/AGENTS.md +0 -0
  19. {cmpnd-0.32.0 → cmpnd-0.33.0}/CLAUDE.md +0 -0
  20. {cmpnd-0.32.0 → cmpnd-0.33.0}/README.md +0 -0
  21. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/__init__.py +0 -0
  22. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/_assessments.py +0 -0
  23. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/_authored_import.py +0 -0
  24. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/_deployments.py +0 -0
  25. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/_exporter_pool.py +0 -0
  26. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/_gepa_patch.py +0 -0
  27. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/_http.py +0 -0
  28. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/_program_patch.py +0 -0
  29. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/_rlm_patch.py +0 -0
  30. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/_runtime_tools.py +0 -0
  31. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/_timespec.py +0 -0
  32. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/_traces.py +0 -0
  33. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/_valsets.py +0 -0
  34. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/authored.py +0 -0
  35. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/cli/__init__.py +0 -0
  36. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/cli/api_client.py +0 -0
  37. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/cli/commands/__init__.py +0 -0
  38. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/cli/commands/admin.py +0 -0
  39. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/cli/commands/assessments.py +0 -0
  40. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/cli/commands/auth.py +0 -0
  41. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/cli/commands/datasets.py +0 -0
  42. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/cli/commands/deployments.py +0 -0
  43. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/cli/commands/evals.py +0 -0
  44. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/cli/commands/login.py +0 -0
  45. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/cli/commands/optimizations.py +0 -0
  46. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/cli/commands/org.py +0 -0
  47. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/cli/commands/programs.py +0 -0
  48. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/cli/commands/provider_credentials.py +0 -0
  49. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/cli/commands/signatures.py +0 -0
  50. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/cli/commands/tool_servers.py +0 -0
  51. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/cli/commands/top.py +0 -0
  52. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/cli/commands/valsets.py +0 -0
  53. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/cli/credentials.py +0 -0
  54. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/cli/deprecation.py +0 -0
  55. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/cli/pagination.py +0 -0
  56. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/cli/shell.py +0 -0
  57. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/cli/trace_render.py +0 -0
  58. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/cli/tree_help.py +0 -0
  59. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/cli/verbs.py +0 -0
  60. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/client.py +0 -0
  61. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/configuration.py +0 -0
  62. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/context.py +0 -0
  63. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/dataset_sync.py +0 -0
  64. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/datasets.py +0 -0
  65. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/decorators.py +0 -0
  66. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/deployment.py +0 -0
  67. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/deployment_metadata.py +0 -0
  68. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/eval_handler.py +0 -0
  69. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/execution.py +0 -0
  70. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/exporter.py +0 -0
  71. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/exporter_http.py +0 -0
  72. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/helpers.py +0 -0
  73. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/identity.py +0 -0
  74. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/imaging.py +0 -0
  75. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/ir/__init__.py +0 -0
  76. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/ir/decode.py +0 -0
  77. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/ir/encode.py +0 -0
  78. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/ir/hash.py +0 -0
  79. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/ir/program.py +0 -0
  80. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/ir/reflection.py +0 -0
  81. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/ir/types.py +0 -0
  82. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/ir/xxh64.py +0 -0
  83. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/lm_configuration.py +0 -0
  84. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/mcp_servers.py +0 -0
  85. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/optimization.py +0 -0
  86. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/optimizers/__init__.py +0 -0
  87. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/optimizers/gepa.py +0 -0
  88. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/optimizers/gepa_callback.py +0 -0
  89. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/optimizers/resume.py +0 -0
  90. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/optimizers/tracker.py +0 -0
  91. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/packaging.py +0 -0
  92. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/references.py +0 -0
  93. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/retry.py +0 -0
  94. {cmpnd-0.32.0 → cmpnd-0.33.0}/cmpnd/traces.py +0 -0
  95. {cmpnd-0.32.0 → cmpnd-0.33.0}/docs/2026-04-02-code-review.md +0 -0
  96. {cmpnd-0.32.0 → cmpnd-0.33.0}/docs/cli.md +0 -0
  97. {cmpnd-0.32.0 → cmpnd-0.33.0}/docs/client.md +0 -0
  98. {cmpnd-0.32.0 → cmpnd-0.33.0}/docs/custom-proposer.md +0 -0
  99. {cmpnd-0.32.0 → cmpnd-0.33.0}/docs/plans/2026-04-05-train-val-dataset-capture-design.md +0 -0
  100. {cmpnd-0.32.0 → cmpnd-0.33.0}/docs/plans/2026-04-05-train-val-dataset-capture.md +0 -0
  101. {cmpnd-0.32.0 → cmpnd-0.33.0}/docs/reading-traces.md +0 -0
  102. {cmpnd-0.32.0 → cmpnd-0.33.0}/examples/generalizing_proposer.py +0 -0
  103. {cmpnd-0.32.0 → cmpnd-0.33.0}/examples/local_ollama.py +0 -0
  104. {cmpnd-0.32.0 → cmpnd-0.33.0}/ingest/LINEAGE.md +0 -0
  105. {cmpnd-0.32.0 → cmpnd-0.33.0}/ingest/cmpnd_ingest/__init__.py +0 -0
  106. {cmpnd-0.32.0 → cmpnd-0.33.0}/ingest/cmpnd_ingest/protocols.py +0 -0
  107. {cmpnd-0.32.0 → cmpnd-0.33.0}/ingest/cmpnd_ingest/py.typed +0 -0
  108. {cmpnd-0.32.0 → cmpnd-0.33.0}/ingest/cmpnd_ingest/request_log.py +0 -0
  109. {cmpnd-0.32.0 → cmpnd-0.33.0}/ingest/cmpnd_ingest/retry.py +0 -0
  110. {cmpnd-0.32.0 → cmpnd-0.33.0}/ingest/pyproject.toml +0 -0
  111. {cmpnd-0.32.0 → cmpnd-0.33.0}/scripts/audit-sdist.sh +0 -0
  112. {cmpnd-0.32.0 → cmpnd-0.33.0}/scripts/capture_review_run.py +0 -0
  113. {cmpnd-0.32.0 → cmpnd-0.33.0}/scripts/check_release_artifacts.py +0 -0
  114. {cmpnd-0.32.0 → cmpnd-0.33.0}/scripts/fixtures/review_run.json.gz +0 -0
  115. {cmpnd-0.32.0 → cmpnd-0.33.0}/scripts/gepa_resume_bug.py +0 -0
  116. {cmpnd-0.32.0 → cmpnd-0.33.0}/scripts/release-stress.sh +0 -0
  117. {cmpnd-0.32.0 → cmpnd-0.33.0}/scripts/require_version_bump.py +0 -0
  118. {cmpnd-0.32.0 → cmpnd-0.33.0}/scripts/seed_review_run.py +0 -0
  119. {cmpnd-0.32.0 → cmpnd-0.33.0}/scripts/test-in-linux.sh +0 -0
  120. {cmpnd-0.32.0 → cmpnd-0.33.0}/stubs/dspy/__init__.pyi +0 -0
  121. {cmpnd-0.32.0 → cmpnd-0.33.0}/stubs/dspy/primitives/__init__.pyi +0 -0
  122. {cmpnd-0.32.0 → cmpnd-0.33.0}/stubs/dspy/primitives/prediction.pyi +0 -0
  123. {cmpnd-0.32.0 → cmpnd-0.33.0}/stubs/dspy/utils/__init__.pyi +0 -0
  124. {cmpnd-0.32.0 → cmpnd-0.33.0}/stubs/dspy/utils/callback.pyi +0 -0
  125. {cmpnd-0.32.0 → cmpnd-0.33.0}/stubs/dspy/utils/dummies.pyi +0 -0
  126. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/__init__.py +0 -0
  127. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/_deployment_signature_helpers.py +0 -0
  128. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/_exporter_helpers.py +0 -0
  129. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/_fake_clock.py +0 -0
  130. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/_identity_helpers.py +0 -0
  131. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/_resolve_refs.py +0 -0
  132. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/_traced_run.py +0 -0
  133. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/conftest.py +0 -0
  134. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/e2e/__init__.py +0 -0
  135. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/e2e/committee_task.py +0 -0
  136. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/e2e/conftest.py +0 -0
  137. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/e2e/data/committee_train.ndjson.gz +0 -0
  138. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/e2e/data/committee_val.ndjson.gz +0 -0
  139. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/e2e/fake_lm.py +0 -0
  140. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/e2e/helpers.py +0 -0
  141. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/e2e/target_capabilities.py +0 -0
  142. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/e2e/test_assessments_cli.py +0 -0
  143. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/e2e/test_capture_prompts.py +0 -0
  144. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/e2e/test_composite_module.py +0 -0
  145. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/e2e/test_dataset_auto_creation.py +0 -0
  146. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/e2e/test_deploy_execute.py +0 -0
  147. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/e2e/test_deploy_programs.py +0 -0
  148. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/e2e/test_edge_cases.py +0 -0
  149. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/e2e/test_example_hash.py +0 -0
  150. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/e2e/test_gepa_optimization.py +0 -0
  151. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/e2e/test_interpreter_spans.py +0 -0
  152. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/e2e/test_lm_accuracy.py +0 -0
  153. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/e2e/test_module_tracing.py +0 -0
  154. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/e2e/test_optimization_lineage.py +0 -0
  155. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/e2e/test_optimization_metric_identity.py +0 -0
  156. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/e2e/test_optimize_failures.py +0 -0
  157. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/e2e/test_optimize_loop.py +0 -0
  158. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/e2e/test_optimize_review_committee.py +0 -0
  159. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/e2e/test_optimize_server_owned_completion.py +0 -0
  160. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/e2e/test_parallel_export.py +0 -0
  161. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/e2e/test_project_attribution.py +0 -0
  162. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/e2e/test_react_tool_spans.py +0 -0
  163. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/e2e/test_save_load.py +0 -0
  164. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/e2e/test_trace_structure.py +0 -0
  165. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/e2e/test_traces_read.py +0 -0
  166. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/e2e/test_valsets_cli.py +0 -0
  167. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/identity_vectors/01_predict_qa.json +0 -0
  168. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/identity_vectors/02_predict_renamed_field.json +0 -0
  169. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/identity_vectors/03_chain_of_thought_qa.json +0 -0
  170. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/identity_vectors/04_predict_richer_types.json +0 -0
  171. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/identity_vectors/05_composite_two_predicts.json +0 -0
  172. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/identity_vectors/06_multichaincomparison.json +0 -0
  173. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/identity_vectors/07_program_of_thought.json +0 -0
  174. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/identity_vectors/08_opaque_module.json +0 -0
  175. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/identity_vectors/09_predict_pydantic_field.json +0 -0
  176. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/identity_vectors/10_react.json +0 -0
  177. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/identity_vectors/11_refine.json +0 -0
  178. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/identity_vectors/12_retrieve.json +0 -0
  179. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/identity_vectors/13_knn.json +0 -0
  180. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/identity_vectors/14_parallel.json +0 -0
  181. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/identity_vectors/README.md +0 -0
  182. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/ingest_conformance.py +0 -0
  183. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/integration/__init__.py +0 -0
  184. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/integration/authored_optimize/judge.py +0 -0
  185. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/integration/authored_optimize/optimize_hosted.py +0 -0
  186. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/integration/conftest.py +0 -0
  187. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/integration/hosted_mcp_tool_optimize_probe.py +0 -0
  188. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/integration/hosted_optimize_probe.py +0 -0
  189. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/integration/optimization_run_probe.py +0 -0
  190. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/integration/test_cli_deploy_smoketest.py +0 -0
  191. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/integration/test_logs_renders_traces.py +0 -0
  192. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/integration/test_optimization_run.py +0 -0
  193. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/integration/test_optimizing_progress.py +0 -0
  194. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/integration/test_unhappy_path.py +0 -0
  195. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/load/__init__.py +0 -0
  196. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/load/__main__.py +0 -0
  197. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/load/drain.py +0 -0
  198. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/load/ingest.py +0 -0
  199. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/load/latency_transport.py +0 -0
  200. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/load/rlm_shape.py +0 -0
  201. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/load/rlm_size_report.py +0 -0
  202. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/load/sweep.py +0 -0
  203. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/load/test_rlm_shape.py +0 -0
  204. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/load/test_rlm_size_report.py +0 -0
  205. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/parity/__init__.py +0 -0
  206. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/parity/conftest.py +0 -0
  207. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/parity/seed_parity.py +0 -0
  208. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/parity/test_auth.py +0 -0
  209. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/parity/test_datasets.py +0 -0
  210. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/parity/test_deployments.py +0 -0
  211. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/parity/test_evals.py +0 -0
  212. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/parity/test_health.py +0 -0
  213. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/parity/test_modules.py +0 -0
  214. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/parity/test_optimizations.py +0 -0
  215. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/parity/test_programs.py +0 -0
  216. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/parity/test_signatures.py +0 -0
  217. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/parity/test_trace_streaming.py +0 -0
  218. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/parity/test_traces.py +0 -0
  219. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/span_reference_fixture.py +0 -0
  220. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/stress/__init__.py +0 -0
  221. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/stress/_provider.py +0 -0
  222. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/stress/_readiness.py +0 -0
  223. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/stress/_reliability.py +0 -0
  224. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/stress/_result.py +0 -0
  225. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/stress/mint_bedrock_token.py +0 -0
  226. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/stress/test_deploy_programs.py +0 -0
  227. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/stress/test_multi_client_stress.py +0 -0
  228. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/stress/test_optimize_client_committee.py +0 -0
  229. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/stress/test_optimize_loop.py +0 -0
  230. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/stress/test_optimize_loop_wasm.py +0 -0
  231. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/stress/test_optimize_stress.py +0 -0
  232. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/stress/test_provider_roles.py +0 -0
  233. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/stress/test_readiness.py +0 -0
  234. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/stress/test_real_provider_twins.py +0 -0
  235. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/stress/test_reliability.py +0 -0
  236. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/stress/test_reliability_classify.py +0 -0
  237. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/stress/test_rlm_wasm.py +0 -0
  238. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/stress/test_trace_search.py +0 -0
  239. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_adapter_reentrancy.py +0 -0
  240. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_assessments_api.py +0 -0
  241. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_authored.py +0 -0
  242. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_authored_bundle_collisions.py +0 -0
  243. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_authored_metric_contract.py +0 -0
  244. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_authored_named_models.py +0 -0
  245. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_authored_optimize.py +0 -0
  246. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_callback.py +0 -0
  247. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_candidate_lineage.py +0 -0
  248. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_cli/__init__.py +0 -0
  249. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_cli/conftest.py +0 -0
  250. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_cli/test_admin.py +0 -0
  251. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_cli/test_api_client.py +0 -0
  252. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_cli/test_assessments.py +0 -0
  253. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_cli/test_auth.py +0 -0
  254. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_cli/test_credentials.py +0 -0
  255. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_cli/test_datasets.py +0 -0
  256. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_cli/test_deployments.py +0 -0
  257. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_cli/test_evals.py +0 -0
  258. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_cli/test_login.py +0 -0
  259. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_cli/test_main.py +0 -0
  260. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_cli/test_optimizations.py +0 -0
  261. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_cli/test_org.py +0 -0
  262. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_cli/test_pagination.py +0 -0
  263. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_cli/test_parity.py +0 -0
  264. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_cli/test_programs.py +0 -0
  265. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_cli/test_provider_credentials.py +0 -0
  266. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_cli/test_schema_sample.py +0 -0
  267. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_cli/test_shell.py +0 -0
  268. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_cli/test_signatures.py +0 -0
  269. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_cli/test_tool_servers.py +0 -0
  270. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_cli/test_top.py +0 -0
  271. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_cli/test_traces_search_wire.py +0 -0
  272. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_cli/test_tree_help.py +0 -0
  273. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_cli/test_valsets.py +0 -0
  274. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_client.py +0 -0
  275. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_clustering/__init__.py +0 -0
  276. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_clustering/conftest.py +0 -0
  277. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_clustering/test_edge_cases.py +0 -0
  278. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_clustering/test_hash_consistency.py +0 -0
  279. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_clustering/test_type_conversion.py +0 -0
  280. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_cmp551_grouping_identity.py +0 -0
  281. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_configuration.py +0 -0
  282. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_context.py +0 -0
  283. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_context_propagation.py +0 -0
  284. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_dataset_polling.py +0 -0
  285. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_dataset_sync.py +0 -0
  286. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_datasets.py +0 -0
  287. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_decorators.py +0 -0
  288. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_deploy.py +0 -0
  289. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_deploy_mcp_tool_references.py +0 -0
  290. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_deployment_metadata.py +0 -0
  291. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_deployments_read.py +0 -0
  292. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_eval.py +0 -0
  293. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_eval_example_attribution.py +0 -0
  294. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_eval_start.py +0 -0
  295. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_eval_trace_grouping.py +0 -0
  296. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_exception_paths.py +0 -0
  297. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_execute.py +0 -0
  298. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_execute_run_identity.py +0 -0
  299. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_exporter.py +0 -0
  300. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_exporter_queue.py +0 -0
  301. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_exporter_real.py +0 -0
  302. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_fake_lm.py +0 -0
  303. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_gepa_callback.py +0 -0
  304. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_gepa_integration.py +0 -0
  305. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_gepa_output_normalization.py +0 -0
  306. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_gepa_resume.py +0 -0
  307. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_identity.py +0 -0
  308. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_imaging.py +0 -0
  309. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_ingest_conformance.py +0 -0
  310. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_ingest_second_producer.py +0 -0
  311. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_integration.py +0 -0
  312. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_interpreter_callback.py +0 -0
  313. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_ir_encoder_properties.py +0 -0
  314. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_ir_golden_vectors.py +0 -0
  315. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_ir_roundtrip.py +0 -0
  316. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_lm_configuration.py +0 -0
  317. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_lm_usage_shapes.py +0 -0
  318. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_mcp_tools.py +0 -0
  319. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_models.py +0 -0
  320. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_optimization_progress.py +0 -0
  321. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_optimization_start.py +0 -0
  322. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_optimize.py +0 -0
  323. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_optimize_budget.py +0 -0
  324. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_optimize_lift.py +0 -0
  325. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_optimize_lms.py +0 -0
  326. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_optimize_proposer.py +0 -0
  327. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_optimize_reflection_lm.py +0 -0
  328. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_optimize_terminal_failure.py +0 -0
  329. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_packaging.py +0 -0
  330. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_packaging_callable_source.py +0 -0
  331. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_packaging_mcp_tools.py +0 -0
  332. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_packaging_program_source.py +0 -0
  333. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_poll_until.py +0 -0
  334. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_program_lineage_persist.py +0 -0
  335. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_project_payload.py +0 -0
  336. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_reflector_properties.py +0 -0
  337. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_reflector_smoke.py +0 -0
  338. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_release_stress_script.py +0 -0
  339. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_repro_reactv2_bugs.py +0 -0
  340. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_require_version_bump.py +0 -0
  341. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_retry.py +0 -0
  342. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_reversible_failures.py +0 -0
  343. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_rlm_patch.py +0 -0
  344. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_run_attribution_paths.py +0 -0
  345. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_runtime_tools.py +0 -0
  346. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_schemas.py +0 -0
  347. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_seed_review_run.py +0 -0
  348. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_span_references.py +0 -0
  349. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_span_sent_once.py +0 -0
  350. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_trace_render.py +0 -0
  351. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_traces_read.py +0 -0
  352. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_tracked_gepa.py +0 -0
  353. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_tracker_enrich.py +0 -0
  354. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_truncation.py +0 -0
  355. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_valsets_api.py +0 -0
  356. {cmpnd-0.32.0 → cmpnd-0.33.0}/tests/test_xxh64.py +0 -0
  357. {cmpnd-0.32.0 → cmpnd-0.33.0}/todo.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cmpnd
3
- Version: 0.32.0
3
+ Version: 0.33.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>
@@ -708,16 +708,16 @@ class _ModuleMixin(_CallbackBase):
708
708
  if span_type == models.SpanType.RLM:
709
709
  trace.streaming = True
710
710
  self._streaming_traces[str(trace.trace_id)] = trace.streaming
711
+ trace.seal_start()
711
712
 
712
713
  # Emit an in_progress start row for EVERY root trace, not
713
714
  # just streaming/RLM ones, so a non-RLM program is visible the moment
714
715
  # it starts (`traces list --status in_progress`, a lookupable id)
715
716
  # instead of only appearing at finalize. `trace.streaming` still
716
717
  # governs per-span vs batch export below — only the start emission is
717
- # generalized. The batch finalize upserts this same row (serve
718
- # traceUpsertSQL, ON CONFLICT org_id,trace_id,start_time), and the
719
- # exporter coalesces a start with its batch trace in one flush, so a
720
- # fast trace pays a single insert, not two.
718
+ # generalized. The start and full body identify the same logical
719
+ # trace, with independent completion and annotation reports. The
720
+ # exporter coalesces them when the full body preserves every fact.
721
721
  if self._stream_trace_starts:
722
722
  exp = self._current_exporter()
723
723
  if exp:
@@ -953,6 +953,7 @@ class _ModuleMixin(_CallbackBase):
953
953
  f"Trace collected for eval: tokens={trace.total_tokens}, cost={trace.total_cost:.4f}"
954
954
  )
955
955
 
956
+ trace.seal_completion()
956
957
  exp = self._current_exporter()
957
958
  if exp:
958
959
  if trace.streaming:
@@ -1542,6 +1543,9 @@ class _LMMixin(_CallbackBase):
1542
1543
  trace.total_tokens = span.total_tokens or 0
1543
1544
  trace.total_cost = span.cost or 0.0
1544
1545
  trace.span_count = 1
1546
+ trace.root_span_type = span.span_type.value
1547
+ trace.seal_start()
1548
+ trace.seal_completion()
1545
1549
 
1546
1550
  exp = self._current_exporter()
1547
1551
  if exp:
@@ -8,6 +8,8 @@ import time
8
8
  from argparse import ArgumentParser, ArgumentTypeError, _SubParsersAction
9
9
  from typing import Any
10
10
 
11
+ from cmpnd_ingest.trace_facts import MetadataFact, StringMapChange
12
+
11
13
  from ... import _timespec
12
14
  from .. import api_client, deprecation, pagination, trace_render
13
15
 
@@ -288,7 +290,11 @@ def handle_update(client: api_client.ApiClient, args: Any) -> Any:
288
290
  it exists for: stamping a correlation id that only became known after the
289
291
  run, without disturbing whatever the program recorded at the time.
290
292
  """
291
- return client.patch(f"/api/v1/traces/{args.id}/metadata", json={"metadata": dict(args.metadata)})
293
+ metadata = dict(args.metadata)
294
+ fact = MetadataFact(metadata=StringMapChange(set_items=tuple(sorted(metadata.items()))))
295
+ return client.patch(
296
+ f"/api/v1/traces/{args.id}/metadata", json={"metadata": metadata, "metadata_fact": fact.to_dict()}
297
+ )
292
298
 
293
299
 
294
300
  def handle_delete(client: api_client.ApiClient, args: Any) -> Any:
@@ -11,6 +11,8 @@ from enum import Enum
11
11
  from typing import Any
12
12
  from uuid import UUID, uuid4
13
13
 
14
+ from cmpnd_ingest.trace_facts import CompletionFact, MetadataFact, StartedFact, StringMapChange
15
+
14
16
  from . import identity, imaging
15
17
 
16
18
  # U+0000 (NUL) and lone UTF-16 surrogates are valid JSON, and valid in a Python
@@ -910,93 +912,161 @@ class TraceBuilder:
910
912
  # Snapshot project_tags at construction time to avoid reconfigure races in async export
911
913
  project_tags: list[str] | None = field(default_factory=configuration._current_project_tags)
912
914
 
915
+ _started_fact: StartedFact | None = field(default=None, init=False, repr=False)
916
+ _completion_fact: CompletionFact | None = field(default=None, init=False, repr=False)
917
+ _metadata_facts: list[MetadataFact] = field(default_factory=list, init=False, repr=False)
918
+ _sealed_tags: dict[str, str] = field(default_factory=dict, init=False, repr=False)
919
+ _sealed_metadata: dict[str, str] = field(default_factory=dict, init=False, repr=False)
920
+ _sealed_project_tags: tuple[str, ...] = field(default=(), init=False, repr=False)
921
+
922
+ def seal_start(self) -> None:
923
+ """Retain the first creation snapshot and its independent annotations."""
924
+ if self._started_fact is None:
925
+ self._started_fact = StartedFact.from_payload(self.to_start_dict())
926
+ self.seal_metadata()
927
+
928
+ def seal_metadata(self) -> None:
929
+ """Seal changes since the previous annotation report, including removals."""
930
+ tags: dict[str, str] = _safe_serialize(self.tags)
931
+ metadata: dict[str, str] = _safe_serialize(self.metadata)
932
+ projects = tuple(self.project_tags or ())
933
+ tag_change = StringMapChange.between(self._sealed_tags, tags)
934
+ metadata_change = StringMapChange.between(self._sealed_metadata, metadata)
935
+ project_change = projects if projects != self._sealed_project_tags else None
936
+ if tag_change is not None or metadata_change is not None or project_change is not None:
937
+ self._metadata_facts.append(MetadataFact(tag_change, metadata_change, project_change))
938
+ self._sealed_tags, self._sealed_metadata, self._sealed_project_tags = tags, metadata, projects
939
+
940
+ def seal_completion(self) -> None:
941
+ """Seal one new completion report; serialization reuses it unchanged."""
942
+ if self.end_time is None:
943
+ raise ValueError("a completion report requires end_time")
944
+ self.seal_start()
945
+ assert self._started_fact is not None
946
+ start = datetime.fromisoformat(self._started_fact.start_time) if self._started_fact.start_time else None
947
+ duration = self.end_time - start if start else None
948
+ duration_ns = (
949
+ (duration.days * 86400 + duration.seconds) * 1_000_000_000 + duration.microseconds * 1000
950
+ if duration is not None
951
+ else 0
952
+ )
953
+ self._completion_fact = CompletionFact(
954
+ producer_sequence=self._completion_fact.producer_sequence + 1 if self._completion_fact else 1,
955
+ end_time=self.end_time.isoformat(),
956
+ status=self.status.value,
957
+ error_message=_strip_unstorable(self.error_message or ""),
958
+ duration_ns=duration_ns,
959
+ )
960
+ self.seal_metadata()
961
+
962
+ def _with_facts(self, payload: dict[str, Any], *, completion: bool = False, started: bool = True) -> dict[str, Any]:
963
+ if self._started_fact is not None:
964
+ creation = self._started_fact.creation_dict()
965
+ payload.update(creation if started else {key: value for key, value in creation.items() if key in payload})
966
+ if started:
967
+ payload["started_fact"] = self._started_fact.to_dict()
968
+ if completion and self._completion_fact is not None:
969
+ payload["completion_fact"] = self._completion_fact.to_dict()
970
+ if self._started_fact is not None or self._completion_fact is not None or self._metadata_facts:
971
+ payload["metadata_facts"] = [fact.to_dict() for fact in self._metadata_facts]
972
+ return payload
973
+
913
974
  def to_api_dict(self) -> dict[str, Any]:
914
975
  """Convert to API-compatible dictionary."""
915
- return {
916
- "trace_id": str(self.trace_id),
917
- "start_time": self.start_time.isoformat() if self.start_time else None,
918
- "end_time": self.end_time.isoformat() if self.end_time else None,
919
- "status": self.status.value,
920
- "error_message": _strip_unstorable_opt(self.error_message),
921
- "program_name": self.program_name,
922
- "program_version": self.program_version,
923
- "signature_name": self.signature_name,
924
- "signature_input_fields": self.signature_input_fields,
925
- "signature_output_fields": self.signature_output_fields,
926
- "signature_input_field_types": self.signature_input_field_types,
927
- "signature_output_field_types": self.signature_output_field_types,
928
- "signature_input_field_schemas": self.signature_input_field_schemas,
929
- "signature_output_field_schemas": self.signature_output_field_schemas,
930
- "program_signature_hash": identity.format_signature_hash(self.program_signature_hash),
931
- "signature_summary": self.signature_summary,
932
- # The server forwards this blob verbatim into a jsonb column, and its
933
- # program registration is best-effort (a rejection is only logged), so
934
- # an un-storable code point here would silently lose the whole
935
- # component list. Scrub at the source — the only place that can.
936
- "component_signatures": _safe_serialize(self.component_signatures),
937
- "compiled_from_run_id": self.compiled_from_run_id,
938
- "demo_ids": self.demo_ids,
939
- "request_preview": (self.request_preview or "")[:500],
940
- "response_preview": (self.response_preview or "")[:500],
941
- "eval_run_id": str(self.eval_run_id) if self.eval_run_id else None,
942
- "optimization_run_id": (str(self.optimization_run_id) if self.optimization_run_id else None),
943
- "adapter_type": self.adapter_type,
944
- "tags": _safe_serialize(self.tags),
945
- "metadata": _safe_serialize(self.metadata),
946
- "project": self.project_tags[0] if self.project_tags else None,
947
- "project_tags": self.project_tags if self.project_tags else [],
948
- "spans": [s.to_api_dict() for s in self._batch_spans] if self._batch_spans else [],
949
- }
976
+ return self._with_facts(
977
+ {
978
+ "trace_id": str(self.trace_id),
979
+ "start_time": self.start_time.isoformat() if self.start_time else None,
980
+ "end_time": self.end_time.isoformat() if self.end_time else None,
981
+ "status": self.status.value,
982
+ "error_message": _strip_unstorable_opt(self.error_message),
983
+ "program_name": self.program_name,
984
+ "program_version": self.program_version,
985
+ "signature_name": self.signature_name,
986
+ "signature_input_fields": self.signature_input_fields,
987
+ "signature_output_fields": self.signature_output_fields,
988
+ "signature_input_field_types": self.signature_input_field_types,
989
+ "signature_output_field_types": self.signature_output_field_types,
990
+ "signature_input_field_schemas": self.signature_input_field_schemas,
991
+ "signature_output_field_schemas": self.signature_output_field_schemas,
992
+ "program_signature_hash": identity.format_signature_hash(self.program_signature_hash),
993
+ "signature_summary": self.signature_summary,
994
+ # The server forwards this blob verbatim into a jsonb column, and its
995
+ # program registration is best-effort (a rejection is only logged), so
996
+ # an un-storable code point here would silently lose the whole
997
+ # component list. Scrub at the source — the only place that can.
998
+ "component_signatures": _safe_serialize(self.component_signatures),
999
+ "compiled_from_run_id": self.compiled_from_run_id,
1000
+ "demo_ids": self.demo_ids,
1001
+ "request_preview": (self.request_preview or "")[:500],
1002
+ "response_preview": (self.response_preview or "")[:500],
1003
+ "eval_run_id": str(self.eval_run_id) if self.eval_run_id else None,
1004
+ "optimization_run_id": (str(self.optimization_run_id) if self.optimization_run_id else None),
1005
+ "adapter_type": self.adapter_type,
1006
+ "tags": _safe_serialize(self.tags),
1007
+ "metadata": _safe_serialize(self.metadata),
1008
+ "project": self.project_tags[0] if self.project_tags else None,
1009
+ "project_tags": self.project_tags if self.project_tags else [],
1010
+ "spans": [s.to_api_dict() for s in self._batch_spans] if self._batch_spans else [],
1011
+ },
1012
+ completion=True,
1013
+ )
950
1014
 
951
1015
  def to_start_dict(self) -> dict[str, Any]:
952
1016
  """Lightweight payload for trace identity, signature and run attribution."""
953
- return {
954
- "start_time": self.start_time.isoformat() if self.start_time else None,
955
- "program_name": self.program_name,
956
- "program_version": self.program_version,
957
- "signature_name": self.signature_name,
958
- "signature_input_fields": self.signature_input_fields,
959
- "signature_output_fields": self.signature_output_fields,
960
- "signature_input_field_types": self.signature_input_field_types,
961
- "signature_output_field_types": self.signature_output_field_types,
962
- "program_signature_hash": identity.format_signature_hash(self.program_signature_hash),
963
- "signature_summary": self.signature_summary,
964
- "component_signatures": _safe_serialize(self.component_signatures),
965
- "root_span_type": self.root_span_type,
966
- "compiled_from_run_id": self.compiled_from_run_id,
967
- "project": self.project_tags[0] if self.project_tags else None,
968
- "project_tags": self.project_tags if self.project_tags else [],
969
- # Known at start (the callback attaches them in _create_trace), so
970
- # the start row carries them from the first send instead of the
971
- # finalize row re-attributing them later.
972
- "eval_run_id": str(self.eval_run_id) if self.eval_run_id else None,
973
- "optimization_run_id": (str(self.optimization_run_id) if self.optimization_run_id else None),
974
- }
1017
+ return self._with_facts(
1018
+ {
1019
+ "start_time": self.start_time.isoformat() if self.start_time else None,
1020
+ "program_name": self.program_name,
1021
+ "program_version": self.program_version,
1022
+ "signature_name": self.signature_name,
1023
+ "signature_input_fields": self.signature_input_fields,
1024
+ "signature_output_fields": self.signature_output_fields,
1025
+ "signature_input_field_types": self.signature_input_field_types,
1026
+ "signature_output_field_types": self.signature_output_field_types,
1027
+ "program_signature_hash": identity.format_signature_hash(self.program_signature_hash),
1028
+ "signature_summary": self.signature_summary,
1029
+ "component_signatures": _safe_serialize(self.component_signatures),
1030
+ "root_span_type": self.root_span_type,
1031
+ "compiled_from_run_id": self.compiled_from_run_id,
1032
+ "project": self.project_tags[0] if self.project_tags else None,
1033
+ "project_tags": self.project_tags if self.project_tags else [],
1034
+ # Known at start (the callback attaches them in _create_trace), so
1035
+ # the start row carries them from the first send instead of the
1036
+ # finalize row re-attributing them later.
1037
+ "eval_run_id": str(self.eval_run_id) if self.eval_run_id else None,
1038
+ "optimization_run_id": (str(self.optimization_run_id) if self.optimization_run_id else None),
1039
+ }
1040
+ )
975
1041
 
976
1042
  def to_finalize_dict(self) -> dict[str, Any]:
977
1043
  """Payload for trace finalization — timing, metrics, previews."""
978
- return {
979
- "start_time": self.start_time.isoformat() if self.start_time else None,
980
- "end_time": self.end_time.isoformat() if self.end_time else None,
981
- "status": self.status.value,
982
- "error_message": _strip_unstorable_opt(self.error_message),
983
- "request_preview": (self.request_preview or "")[:500],
984
- "response_preview": (self.response_preview or "")[:500],
985
- "total_tokens": self.total_tokens,
986
- "prompt_tokens": self.prompt_tokens,
987
- "completion_tokens": self.completion_tokens,
988
- "total_cost": self.total_cost,
989
- "span_count": self.span_count,
990
- "adapter_type": self.adapter_type,
991
- "compiled_from_run_id": self.compiled_from_run_id,
992
- "demo_ids": self.demo_ids,
993
- "eval_run_id": str(self.eval_run_id) if self.eval_run_id else None,
994
- "optimization_run_id": (str(self.optimization_run_id) if self.optimization_run_id else None),
995
- "tags": _safe_serialize(self.tags),
996
- "metadata": _safe_serialize(self.metadata),
997
- "project": self.project_tags[0] if self.project_tags else None,
998
- "project_tags": self.project_tags if self.project_tags else [],
999
- }
1044
+ return self._with_facts(
1045
+ {
1046
+ "start_time": self.start_time.isoformat() if self.start_time else None,
1047
+ "end_time": self.end_time.isoformat() if self.end_time else None,
1048
+ "status": self.status.value,
1049
+ "error_message": _strip_unstorable_opt(self.error_message),
1050
+ "request_preview": (self.request_preview or "")[:500],
1051
+ "response_preview": (self.response_preview or "")[:500],
1052
+ "total_tokens": self.total_tokens,
1053
+ "prompt_tokens": self.prompt_tokens,
1054
+ "completion_tokens": self.completion_tokens,
1055
+ "total_cost": self.total_cost,
1056
+ "span_count": self.span_count,
1057
+ "adapter_type": self.adapter_type,
1058
+ "compiled_from_run_id": self.compiled_from_run_id,
1059
+ "demo_ids": self.demo_ids,
1060
+ "eval_run_id": str(self.eval_run_id) if self.eval_run_id else None,
1061
+ "optimization_run_id": (str(self.optimization_run_id) if self.optimization_run_id else None),
1062
+ "tags": _safe_serialize(self.tags),
1063
+ "metadata": _safe_serialize(self.metadata),
1064
+ "project": self.project_tags[0] if self.project_tags else None,
1065
+ "project_tags": self.project_tags if self.project_tags else [],
1066
+ },
1067
+ completion=True,
1068
+ started=False,
1069
+ )
1000
1070
 
1001
1071
 
1002
1072
  class EvalRunStatus(str, Enum):
@@ -583,10 +583,26 @@ Its first payload carries the optimization id, plus the active eval id for a
583
583
  metric call. A reflection call outside evaluation has no eval id. Manual
584
584
  decorators and `start_span` create spans rather than another automatic trace.
585
585
 
586
+ The callback seals `started_fact` after it sets the root type, before the
587
+ module executes. It keeps that snapshot even when start emission is disabled.
588
+ The completion callback seals one independently identified `completion_fact`
589
+ with a positive producer sequence. Repeated serialization and HTTP retries
590
+ reuse both objects. A standalone optimizer LM trace seals its creation and
591
+ completion before its first export. The late adapter type stays in the flat
592
+ compatibility fields and ended adapter spans.
593
+
594
+ Each metadata change gets its own `metadata_facts` entry with explicit map
595
+ assignments and removals. The callback seals initial annotations at creation
596
+ and later changes at completion. Every modern body includes the array, even
597
+ when it is empty, so a retry cannot reapply its flat metadata view as a legacy
598
+ mutation. A streaming finalize carries completion and metadata facts. A full
599
+ body also carries its original started fact. `cmpnd traces update` sends one
600
+ independent `metadata_fact` alongside its existing metadata PATCH envelope.
601
+
586
602
  Two things keep the extra start cheap and correct:
587
603
 
588
- - **Upsert, not duplicate.** The batch finalize (`export_trace`) and the start write the same row: serve's `traceUpsertSQL` keys `ON CONFLICT (org_id, trace_id, start_time)`, and the SDK's `start_time` is immutable across a trace's life, so the batch overwrites the `in_progress` row with the final status. A trace that never finalizes (crash / cancel) correctly stays `in_progress`.
589
- - **Coalescing.** When a trace's `trace_start` and its full batch `trace` land in the **same** flush, `_flush_batch` drops the redundant `/start` POST (the batch upserts the final row directly), so a fast trace pays one insert, not two. A start whose finalize lands in a later flush still ships — that visibility is the point. Set `stream_trace_starts=False` to opt out entirely (traces then reappear only at finalize, the pre-CMP-257 behavior).
604
+ - **Shared identity.** The start and full body identify the same logical trace and retain the same creation fields. Independent completion reports update its lifecycle without changing the started fact. A trace without a completion report keeps its open execution state until the server infers abandonment.
605
+ - **Coalescing.** When a trace's start and full body land in the same flush, the exporter suppresses the start only if the full body preserves its sealed start and every independent metadata fact. A start in an earlier flush still ships. Set `stream_trace_starts=False` to emit the full body only at completion.
590
606
 
591
607
  ---
592
608
 
@@ -44,12 +44,26 @@ Nothing nominal. `BatchExporter` takes a config and record objects described by
44
44
  satisfy them by shape, and so may any other producer's records. That is the
45
45
  import boundary: the transport knows how to ship a record, not how one is built.
46
46
 
47
- A producer whose records are **already in wire shape** — plain dicts from a guest, or
47
+ A producer whose records are **already in wire shape** — plain dicts from a kernel, or
48
48
  from its own record writer — wraps them in `bodies.py`: `TraceBody`, `SpanBody`,
49
49
  `OptimizationRunBody` satisfy those same protocols over a dict it hands over. One place
50
50
  holds that mapping, so a third producer does not re-derive it from the transport's
51
51
  internals.
52
52
 
53
+ ## Sealing trace facts before delivery
54
+
55
+ The exporter snapshots a trace when it enters the queue. `TraceBody` also isolates
56
+ nested values at construction and returns fresh copies, so a caller's later edits
57
+ cannot change a pending request or its replay identity. Existing structural records
58
+ and flat legacy payloads remain accepted.
59
+
60
+ Producers can use `trace_facts.py` without DSPy or HTTP imports. `StartedFact` seals
61
+ creation fields. `CompletionFact` identifies one report with a positive producer
62
+ sequence and a duration in nanoseconds. `MetadataFact` carries independent string-map
63
+ assignments and removals or a project-tag replacement. Serializers reuse these values
64
+ without minting another ID. A start POST is coalesced into a full body only when that
65
+ body retains its identified started and metadata evidence.
66
+
53
67
  ## Writing a run to an id you were handed
54
68
 
55
69
  `export_optimization_start` reserves an id and then writes the record to it, which is
@@ -1,45 +1,35 @@
1
- """Records a producer already holds in wire shape, made shippable.
2
-
3
- `protocols.py` asks a producer for *objects*: something with `to_api_dict()`,
4
- `to_start_dict()`, a `trace_id`. The SDK has them, because it builds a trace and a run
5
- from a live `dspy.Module` and serializes at the end. A second producer often does not: the
6
- wasm worker's GEPA child receives finished trace dicts from a guest and writes its run
7
- record as a dict in the shape the contract specifies
8
- (`search-process/cmpnd_search_process/run_record.py`). It holds the body, not the
9
- object that built one.
10
-
11
- **These are adapters, not a second protocol.** Widening `protocols.py` to `dict | Record`
12
- was the alternative, and it was rejected: every member the transport reads would grow a
13
- branch, the SDK's typed path would start paying for a shape it never has, and a producer
14
- whose records are neither — the third one the contract anticipates — would still be
15
- outside. A structural `Protocol` is already the widest possible door; what was missing was
16
- a body-shaped key for it, in one place, so a third producer does not re-derive the mapping
17
- from the transport's internals.
18
-
19
- Each class wraps **one** body and answers every serialization the matching protocol
20
- declares with it. That is deliberate rather than lax: a producer holding a dict knows
21
- which of the contract's routes that dict is a body for, and pairs it with the matching
22
- `export_*` call. The transport asks for exactly one serialization per call, so the pairing
23
- is what decides which — the same freedom, and the same responsibility, a producer has when
24
- it writes its own record class.
25
-
26
- Every serialization returns a **shallow copy**, because the transport writes into what it
27
- is given: it stamps the run id into a run body, and `_flush_batch` splices buffered spans
28
- into a trace body. A producer's dict is its own.
1
+ """Adapters expose already serialized bodies through the producer protocols.
2
+
3
+ Each adapter answers the serialization methods for one body. The producer pairs it
4
+ with the matching export call, so independent workers can send wire records without
5
+ importing DSPy. The transport does not create another protocol for dictionary bodies.
6
+
7
+ TraceBody seals nested values at construction and returns independent copies. A queued
8
+ trace and its replay identities survive later changes to the producer's input or a
9
+ returned serialization. Other adapters copy their outer dictionary before transport
10
+ stamping, and optimization bodies keep the producer's delivery acknowledgement callback.
29
11
  """
30
12
 
31
13
  from __future__ import annotations
32
14
 
33
15
  from collections.abc import Callable
16
+ from copy import deepcopy
34
17
  from dataclasses import dataclass, field
35
18
  from typing import TYPE_CHECKING, Any
36
19
 
37
20
 
38
- @dataclass(frozen=True)
21
+ @dataclass(frozen=True, init=False)
39
22
  class TraceBody:
40
23
  """A trace a producer already holds as the body of one of the trace routes."""
41
24
 
42
- payload: dict[str, Any]
25
+ _payload: dict[str, Any]
26
+
27
+ def __init__(self, payload: dict[str, Any]) -> None:
28
+ object.__setattr__(self, "_payload", deepcopy(payload))
29
+
30
+ @property
31
+ def payload(self) -> dict[str, Any]:
32
+ return deepcopy(self._payload)
43
33
 
44
34
  @property
45
35
  def trace_id(self) -> str:
@@ -50,16 +40,16 @@ class TraceBody:
50
40
  post needs no such member: it addresses `/traces`, and the id it is keyed on is
51
41
  the one in the body.)
52
42
  """
53
- return str(self.payload["trace_id"])
43
+ return str(self._payload["trace_id"])
54
44
 
55
45
  def to_api_dict(self) -> dict[str, Any]:
56
- return dict(self.payload)
46
+ return deepcopy(self._payload)
57
47
 
58
48
  def to_start_dict(self) -> dict[str, Any]:
59
- return dict(self.payload)
49
+ return deepcopy(self._payload)
60
50
 
61
51
  def to_finalize_dict(self) -> dict[str, Any]:
62
- return dict(self.payload)
52
+ return deepcopy(self._payload)
63
53
 
64
54
 
65
55
  @dataclass(frozen=True)
@@ -33,7 +33,7 @@ from typing import Any, cast
33
33
  import httpx
34
34
 
35
35
  from . import request_log, retry
36
- from .bodies import EncodedTraceBody
36
+ from .bodies import EncodedTraceBody, TraceBody
37
37
  from .protocols import (
38
38
  EvalRunRecord,
39
39
  ExporterConfig,
@@ -557,7 +557,7 @@ class BatchExporter:
557
557
 
558
558
  def export_trace(self, trace: TraceRecord) -> None:
559
559
  """Queue a trace for export (non-blocking)."""
560
- self._enqueue(ExportItem("trace", trace, time.time()))
560
+ self._enqueue(ExportItem("trace", TraceBody(trace.to_api_dict()), time.time()))
561
561
 
562
562
  def export_encoded_trace(self, trace: EncodedTraceBody) -> None:
563
563
  """Queue a compressed finished trace for export (non-blocking).
@@ -594,11 +594,13 @@ class BatchExporter:
594
594
 
595
595
  def export_trace_start(self, trace: TraceRecord) -> None:
596
596
  """Queue a trace start for export (non-blocking)."""
597
- self._enqueue(ExportItem("trace_start", trace, time.time()))
597
+ body = {**trace.to_start_dict(), "trace_id": str(trace.trace_id)}
598
+ self._enqueue(ExportItem("trace_start", TraceBody(body), time.time()))
598
599
 
599
600
  def export_trace_finalize(self, trace: TraceRecord) -> None:
600
601
  """Queue a trace finalization for export (non-blocking)."""
601
- self._enqueue(ExportItem("trace_finalize", trace, time.time()))
602
+ body = {**trace.to_finalize_dict(), "trace_id": str(trace.trace_id)}
603
+ self._enqueue(ExportItem("trace_finalize", TraceBody(body), time.time()))
602
604
 
603
605
  def export_optimization_start(self, optimization_run: OptimizationRunRecord) -> str:
604
606
  """Create the optimization record server-side and return the
@@ -1197,8 +1199,20 @@ class BatchExporter:
1197
1199
  # whose finalize lands in a LATER flush still ships, which is the whole
1198
1200
  # point: the trace is visible while it runs.
1199
1201
  if trace_starts and traces_to_send:
1200
- batched_ids = {str(t.get("trace_id")) for t in traces_to_send}
1201
- trace_starts = [pair for pair in trace_starts if pair[0] not in batched_ids]
1202
+
1203
+ def start_is_in_body(trace_id: str, start: dict[str, Any]) -> bool:
1204
+ for full in traces_to_send:
1205
+ if str(full.get("trace_id")) != trace_id:
1206
+ continue
1207
+ if not all(fact in full.get("metadata_facts", []) for fact in start.get("metadata_facts", [])):
1208
+ continue
1209
+ if "started_fact" not in start:
1210
+ return True
1211
+ if full.get("started_fact") == start["started_fact"]:
1212
+ return True
1213
+ return False
1214
+
1215
+ trace_starts = [pair for pair in trace_starts if not start_is_in_body(*pair)]
1202
1216
 
1203
1217
  # A span is sent exactly once. When its trace ships in this same flush it
1204
1218
  # rides that trace's body and is taken off the spans route; otherwise the