cmpnd 0.30.0__tar.gz → 0.31.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.
- {cmpnd-0.30.0 → cmpnd-0.31.0}/AGENTS.md +25 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/PKG-INFO +1 -1
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/_assessments.py +6 -4
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/callback.py +27 -11
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/cli/commands/assessments.py +20 -6
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/client.py +9 -2
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/helpers.py +31 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/docs/cli.md +10 -4
- {cmpnd-0.30.0 → cmpnd-0.31.0}/docs/client.md +4 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/docs/sdk-instrumentation.md +5 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/pyproject.toml +1 -1
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/e2e/test_assessments_cli.py +9 -3
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/e2e/test_valsets_cli.py +6 -1
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/stress/_readiness.py +57 -7
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/stress/test_readiness.py +82 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/stress/test_trace_search.py +25 -4
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_assessments_api.py +14 -3
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_cli/test_assessments.py +80 -4
- cmpnd-0.31.0/tests/test_gepa_output_normalization.py +306 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/uv.lock +1 -1
- {cmpnd-0.30.0 → cmpnd-0.31.0}/.gitignore +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/CLAUDE.md +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/README.md +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/__init__.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/_authored_import.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/_deployments.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/_exporter_pool.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/_gepa_patch.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/_http.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/_program_patch.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/_rlm_patch.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/_runtime_tools.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/_timespec.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/_traces.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/_valsets.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/authored.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/cli/__init__.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/cli/api_client.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/cli/commands/__init__.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/cli/commands/admin.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/cli/commands/auth.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/cli/commands/datasets.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/cli/commands/deployments.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/cli/commands/evals.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/cli/commands/login.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/cli/commands/optimizations.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/cli/commands/org.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/cli/commands/programs.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/cli/commands/provider_credentials.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/cli/commands/signatures.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/cli/commands/tool_servers.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/cli/commands/top.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/cli/commands/traces.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/cli/commands/valsets.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/cli/credentials.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/cli/deprecation.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/cli/pagination.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/cli/shell.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/cli/trace_render.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/cli/tree_help.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/cli/verbs.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/configuration.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/context.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/dataset_sync.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/datasets.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/decorators.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/deployment.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/deployment_metadata.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/eval_handler.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/execution.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/exporter.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/exporter_http.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/identity.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/imaging.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/ir/__init__.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/ir/decode.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/ir/encode.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/ir/hash.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/ir/program.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/ir/reflection.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/ir/types.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/ir/xxh64.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/lm_configuration.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/mcp_servers.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/models.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/optimization.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/optimizers/__init__.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/optimizers/gepa.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/optimizers/gepa_callback.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/optimizers/resume.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/optimizers/tracker.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/packaging.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/references.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/retry.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/cmpnd/traces.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/docs/2026-04-02-code-review.md +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/docs/custom-proposer.md +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/docs/plans/2026-04-05-train-val-dataset-capture-design.md +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/docs/plans/2026-04-05-train-val-dataset-capture.md +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/docs/reading-traces.md +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/examples/generalizing_proposer.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/examples/local_ollama.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/ingest/LINEAGE.md +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/ingest/README.md +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/ingest/cmpnd_ingest/__init__.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/ingest/cmpnd_ingest/bodies.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/ingest/cmpnd_ingest/exporter_http.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/ingest/cmpnd_ingest/protocols.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/ingest/cmpnd_ingest/py.typed +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/ingest/cmpnd_ingest/request_log.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/ingest/cmpnd_ingest/retry.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/ingest/pyproject.toml +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/scripts/audit-sdist.sh +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/scripts/capture_review_run.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/scripts/check_release_artifacts.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/scripts/fixtures/review_run.json.gz +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/scripts/gepa_resume_bug.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/scripts/release-stress.sh +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/scripts/require_version_bump.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/scripts/seed_review_run.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/scripts/test-in-linux.sh +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/stubs/dspy/__init__.pyi +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/stubs/dspy/primitives/__init__.pyi +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/stubs/dspy/primitives/prediction.pyi +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/stubs/dspy/utils/__init__.pyi +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/stubs/dspy/utils/callback.pyi +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/stubs/dspy/utils/dummies.pyi +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/__init__.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/_deployment_signature_helpers.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/_exporter_helpers.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/_fake_clock.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/_identity_helpers.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/_resolve_refs.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/_traced_run.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/conftest.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/e2e/__init__.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/e2e/committee_task.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/e2e/conftest.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/e2e/data/committee_train.ndjson.gz +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/e2e/data/committee_val.ndjson.gz +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/e2e/fake_lm.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/e2e/helpers.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/e2e/target_capabilities.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/e2e/test_capture_prompts.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/e2e/test_composite_module.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/e2e/test_dataset_auto_creation.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/e2e/test_deploy_execute.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/e2e/test_deploy_programs.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/e2e/test_edge_cases.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/e2e/test_example_hash.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/e2e/test_gepa_optimization.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/e2e/test_interpreter_spans.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/e2e/test_lm_accuracy.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/e2e/test_module_tracing.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/e2e/test_optimization_lineage.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/e2e/test_optimization_metric_identity.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/e2e/test_optimize_failures.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/e2e/test_optimize_loop.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/e2e/test_optimize_review_committee.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/e2e/test_optimize_server_owned_completion.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/e2e/test_parallel_export.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/e2e/test_project_attribution.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/e2e/test_react_tool_spans.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/e2e/test_save_load.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/e2e/test_trace_structure.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/e2e/test_traces_read.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/e2e/test_user_attribution.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/identity_vectors/01_predict_qa.json +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/identity_vectors/02_predict_renamed_field.json +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/identity_vectors/03_chain_of_thought_qa.json +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/identity_vectors/04_predict_richer_types.json +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/identity_vectors/05_composite_two_predicts.json +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/identity_vectors/06_multichaincomparison.json +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/identity_vectors/07_program_of_thought.json +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/identity_vectors/08_opaque_module.json +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/identity_vectors/09_predict_pydantic_field.json +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/identity_vectors/10_react.json +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/identity_vectors/11_refine.json +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/identity_vectors/12_retrieve.json +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/identity_vectors/13_knn.json +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/identity_vectors/14_parallel.json +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/identity_vectors/README.md +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/ingest_conformance.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/integration/__init__.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/integration/authored_optimize/judge.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/integration/authored_optimize/optimize_hosted.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/integration/conftest.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/integration/hosted_mcp_tool_optimize_probe.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/integration/hosted_optimize_probe.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/integration/optimization_run_probe.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/integration/test_cli_deploy_smoketest.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/integration/test_logs_renders_traces.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/integration/test_optimization_run.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/integration/test_optimizing_progress.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/integration/test_unhappy_path.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/load/__init__.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/load/__main__.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/load/drain.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/load/ingest.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/load/latency_transport.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/load/rlm_shape.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/load/rlm_size_report.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/load/sweep.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/load/test_rlm_shape.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/load/test_rlm_size_report.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/parity/__init__.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/parity/conftest.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/parity/seed_parity.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/parity/test_auth.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/parity/test_datasets.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/parity/test_deployments.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/parity/test_evals.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/parity/test_health.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/parity/test_modules.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/parity/test_optimizations.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/parity/test_programs.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/parity/test_signatures.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/parity/test_trace_streaming.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/parity/test_traces.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/span_reference_fixture.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/stress/__init__.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/stress/_provider.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/stress/_reliability.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/stress/_result.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/stress/mint_bedrock_token.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/stress/test_deploy_programs.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/stress/test_multi_client_stress.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/stress/test_optimize_client_committee.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/stress/test_optimize_loop.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/stress/test_optimize_loop_wasm.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/stress/test_optimize_stress.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/stress/test_provider_roles.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/stress/test_real_provider_twins.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/stress/test_reliability.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/stress/test_reliability_classify.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/stress/test_rlm_wasm.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_adapter_reentrancy.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_authored.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_authored_bundle_collisions.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_authored_metric_contract.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_authored_named_models.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_authored_optimize.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_callback.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_candidate_lineage.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_cli/__init__.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_cli/conftest.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_cli/test_admin.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_cli/test_api_client.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_cli/test_auth.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_cli/test_credentials.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_cli/test_datasets.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_cli/test_deployments.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_cli/test_evals.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_cli/test_login.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_cli/test_main.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_cli/test_optimizations.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_cli/test_org.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_cli/test_pagination.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_cli/test_parity.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_cli/test_programs.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_cli/test_provider_credentials.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_cli/test_schema_sample.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_cli/test_shell.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_cli/test_signatures.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_cli/test_tool_servers.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_cli/test_top.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_cli/test_traces.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_cli/test_traces_search_wire.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_cli/test_tree_help.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_cli/test_valsets.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_client.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_clustering/__init__.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_clustering/conftest.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_clustering/test_edge_cases.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_clustering/test_hash_consistency.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_clustering/test_type_conversion.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_cmp551_grouping_identity.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_configuration.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_context.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_context_propagation.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_dataset_polling.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_dataset_sync.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_datasets.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_decorators.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_deploy.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_deploy_mcp_tool_references.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_deployment_metadata.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_deployments_read.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_eval.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_eval_example_attribution.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_eval_start.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_eval_trace_grouping.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_exception_paths.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_execute.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_exporter.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_exporter_queue.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_exporter_real.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_fake_lm.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_gepa_callback.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_gepa_integration.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_gepa_resume.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_identity.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_imaging.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_import_weight.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_ingest_conformance.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_ingest_second_producer.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_integration.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_interpreter_callback.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_ir_encoder_properties.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_ir_golden_vectors.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_ir_roundtrip.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_lm_configuration.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_lm_usage_shapes.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_mcp_tools.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_models.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_optimization_progress.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_optimization_start.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_optimize.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_optimize_budget.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_optimize_lift.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_optimize_lms.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_optimize_proposer.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_optimize_reflection_lm.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_optimize_terminal_failure.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_packaging.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_packaging_callable_source.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_packaging_mcp_tools.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_packaging_program_source.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_poll_until.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_program_lineage_persist.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_project_payload.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_reflector_properties.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_reflector_smoke.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_release_stress_script.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_repro_reactv2_bugs.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_require_version_bump.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_retry.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_reversible_failures.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_rlm_patch.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_runtime_tools.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_schemas.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_seed_review_run.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_span_references.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_span_sent_once.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_trace_render.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_traces_read.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_tracked_gepa.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_tracker_enrich.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_truncation.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_valsets_api.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/tests/test_xxh64.py +0 -0
- {cmpnd-0.30.0 → cmpnd-0.31.0}/todo.md +0 -0
|
@@ -119,6 +119,31 @@ uv run ruff check . # lint
|
|
|
119
119
|
uv run ruff format --check . # format check
|
|
120
120
|
```
|
|
121
121
|
|
|
122
|
+
**Never run pytest processes in parallel** — no `pytest-xdist`, no backgrounding
|
|
123
|
+
one run while starting another. DSPy is memory-heavy on import, and parallel
|
|
124
|
+
pytest crashes the machine. Run suites serially.
|
|
125
|
+
|
|
126
|
+
**Spec every mock, and assert wire shape over a real client.** A spec-less
|
|
127
|
+
`MagicMock` answers any method with any arguments, so an assertion about how a
|
|
128
|
+
dependency was called is worth exactly the mock's fidelity to it. That shipped
|
|
129
|
+
a live bug: the CLI dispatched a revocation as `httpx.Client.delete(url,
|
|
130
|
+
json=body)`, `httpx` gives `delete` no `json` parameter, the body never left the
|
|
131
|
+
process, and the test asserting the call passed. Two rules, and both are needed:
|
|
132
|
+
|
|
133
|
+
- Build every double from the real signature — `create_autospec(Thing,
|
|
134
|
+
instance=True)` or `mock.patch(..., autospec=True)`, as
|
|
135
|
+
`tests/test_cli/conftest.py` does for `ApiClient`.
|
|
136
|
+
- Where the property under test is what goes on the wire, drive a real
|
|
137
|
+
`httpx.Client` over an `httpx.MockTransport` that captures the request and
|
|
138
|
+
assert on the captured bytes — `tests/test_cli/test_api_client.py` is the
|
|
139
|
+
pattern. Autospec guards only the seam you point it at; the bug above lived
|
|
140
|
+
one layer below an autospec'd client.
|
|
141
|
+
|
|
142
|
+
**E2E tests hit a live server.** `tests/e2e/` runs against the real platform
|
|
143
|
+
with real LLMs (`CMPND_E2E_*` keys), never a mock or an in-process fake: its
|
|
144
|
+
job is the integration bugs between the SDK and the real API surface. It is
|
|
145
|
+
local-only by design.
|
|
146
|
+
|
|
122
147
|
## Two packages, one published distribution
|
|
123
148
|
|
|
124
149
|
`cmpnd` (`cmpnd/`) is the SDK. `cmpnd_ingest` (`ingest/cmpnd_ingest/`) is the
|
|
@@ -25,15 +25,17 @@ def create(
|
|
|
25
25
|
*,
|
|
26
26
|
conclusion: Conclusion = "neutral",
|
|
27
27
|
body: str | None = None,
|
|
28
|
-
annotations: Sequence[tuple[str, str]] | None = None,
|
|
28
|
+
annotations: Sequence[tuple[str, str, str]] | None = None,
|
|
29
29
|
api_key: str | None = None,
|
|
30
30
|
endpoint: str | None = None,
|
|
31
31
|
) -> dict[str, Any]:
|
|
32
32
|
"""Record one judgement of a trace and return the created assessment.
|
|
33
33
|
|
|
34
34
|
``conclusion`` affirms (``"up"``), disputes (``"down"``), or neither
|
|
35
|
-
(``"neutral"``, the default). ``annotations`` are ``(
|
|
36
|
-
|
|
35
|
+
(``"neutral"``, the default). ``annotations`` are ``(kind, ref, note)``
|
|
36
|
+
tuples where ``kind`` is ``"span"`` (``ref`` is a span id) or ``"output"``
|
|
37
|
+
(``ref`` is an RFC 6901 JSON pointer such as ``/trends/2``); the tuple order
|
|
38
|
+
matches the CLI's ``--annotate KIND REF NOTE``. The server rejects a target
|
|
37
39
|
that does not belong to the trace. ``api_key`` / ``endpoint`` override the
|
|
38
40
|
configured credentials for this call.
|
|
39
41
|
"""
|
|
@@ -41,7 +43,7 @@ def create(
|
|
|
41
43
|
if body is not None:
|
|
42
44
|
payload["body"] = body
|
|
43
45
|
if annotations:
|
|
44
|
-
payload["annotations"] = [{"
|
|
46
|
+
payload["annotations"] = [{"kind": kind, "ref": ref, "note": note} for kind, ref, note in annotations]
|
|
45
47
|
return _client(api_key, endpoint).post("/api/v1/assessments", json=payload)
|
|
46
48
|
|
|
47
49
|
|
|
@@ -499,6 +499,11 @@ class _CallbackBase:
|
|
|
499
499
|
# Track trace roots
|
|
500
500
|
self._root_call_ids: set[str] = set()
|
|
501
501
|
|
|
502
|
+
# GEPA's bootstrap tracer temporarily patches the evaluated program's
|
|
503
|
+
# forward method to return (prediction, bootstrap trace). Track that
|
|
504
|
+
# wrapper itself so ordinary tuples at roots or under Evaluate stay intact.
|
|
505
|
+
self._gepa_bootstrap_call_ids: set[str] = set()
|
|
506
|
+
|
|
502
507
|
# Track spans by trace_id for metric aggregation
|
|
503
508
|
self._trace_spans: dict[str, list[models.SpanBuilder]] = {}
|
|
504
509
|
|
|
@@ -682,6 +687,8 @@ class _ModuleMixin(_CallbackBase):
|
|
|
682
687
|
# of them, so a module call beneath one roots its own trace. An open
|
|
683
688
|
# trace still wins: a user's `cmpnd.trace` around an Evaluate owns it.
|
|
684
689
|
parent_span = context.get_current_span()
|
|
690
|
+
if helpers.is_gepa_bootstrap_wrapper(instance):
|
|
691
|
+
self._gepa_bootstrap_call_ids.add(call_id)
|
|
685
692
|
is_root = parent_span is None or (
|
|
686
693
|
parent_span.span_type == models.SpanType.EVALUATION and context.get_current_trace() is None
|
|
687
694
|
)
|
|
@@ -784,11 +791,22 @@ class _ModuleMixin(_CallbackBase):
|
|
|
784
791
|
|
|
785
792
|
span = self._call_id_to_span.pop(call_id, None)
|
|
786
793
|
instance = self._call_id_to_instance.pop(call_id, None)
|
|
794
|
+
is_gepa_capture_boundary = call_id in self._gepa_bootstrap_call_ids
|
|
795
|
+
self._gepa_bootstrap_call_ids.discard(call_id)
|
|
787
796
|
|
|
788
797
|
if not span:
|
|
789
798
|
logger.warning(f"No span found for call_id: {call_id}")
|
|
790
799
|
return
|
|
791
800
|
|
|
801
|
+
in_optimization = (
|
|
802
|
+
self._active_optimization_run_id is not None or context.get_current_optimization_run_id() is not None
|
|
803
|
+
)
|
|
804
|
+
captured_outputs = (
|
|
805
|
+
helpers.normalize_gepa_bootstrap_output(outputs, dspy, in_optimization=in_optimization)
|
|
806
|
+
if is_gepa_capture_boundary
|
|
807
|
+
else outputs
|
|
808
|
+
)
|
|
809
|
+
|
|
792
810
|
# Set end time and status
|
|
793
811
|
span.end_time = datetime.now(timezone.utc)
|
|
794
812
|
|
|
@@ -798,12 +816,12 @@ class _ModuleMixin(_CallbackBase):
|
|
|
798
816
|
span.status = models.TraceStatus.OK
|
|
799
817
|
|
|
800
818
|
# Capture outputs
|
|
801
|
-
if self._capture_outputs and
|
|
802
|
-
cleaned_outputs = helpers.clean_outputs(
|
|
819
|
+
if self._capture_outputs and captured_outputs is not None:
|
|
820
|
+
cleaned_outputs = helpers.clean_outputs(captured_outputs, instance, dspy, self._max_output_field_size)
|
|
803
821
|
span.set_outputs(cleaned_outputs)
|
|
804
822
|
|
|
805
823
|
# Extract token usage if available
|
|
806
|
-
self._extract_usage_from_prediction(span,
|
|
824
|
+
self._extract_usage_from_prediction(span, captured_outputs, dspy)
|
|
807
825
|
|
|
808
826
|
# Pop from context stack
|
|
809
827
|
context.pop_span()
|
|
@@ -851,10 +869,10 @@ class _ModuleMixin(_CallbackBase):
|
|
|
851
869
|
# Skip "FINAL: ..." outputs — these contain the entire RLM result
|
|
852
870
|
# dict and are not meaningful code cell outputs.
|
|
853
871
|
if span.span_type == models.SpanType.RLM:
|
|
854
|
-
if rlm_parent and rlm_parent.repl_history and
|
|
872
|
+
if rlm_parent and rlm_parent.repl_history and captured_outputs is not None:
|
|
855
873
|
try:
|
|
856
874
|
last_output = None
|
|
857
|
-
traj =
|
|
875
|
+
traj = captured_outputs.trajectory if hasattr(captured_outputs, "trajectory") else None
|
|
858
876
|
if traj and hasattr(traj, "entries") and traj.entries:
|
|
859
877
|
last_output = traj.entries[-1].output
|
|
860
878
|
elif isinstance(traj, list) and traj:
|
|
@@ -893,10 +911,8 @@ class _ModuleMixin(_CallbackBase):
|
|
|
893
911
|
trace.error_message = str(exception)
|
|
894
912
|
|
|
895
913
|
# Set response preview
|
|
896
|
-
if
|
|
897
|
-
trace.response_preview = helpers.create_preview(
|
|
898
|
-
helpers.clean_outputs(outputs, instance, dspy, self._max_output_field_size)
|
|
899
|
-
)
|
|
914
|
+
if captured_outputs is not None and self._capture_outputs:
|
|
915
|
+
trace.response_preview = helpers.create_preview(span.outputs or {})
|
|
900
916
|
|
|
901
917
|
# Aggregate span metrics into trace
|
|
902
918
|
trace_id_str = str(trace.trace_id)
|
|
@@ -950,9 +966,9 @@ class _ModuleMixin(_CallbackBase):
|
|
|
950
966
|
# store and surface as a phantom output field. Read it back via the
|
|
951
967
|
# public `cmpnd.trace_id(pred)` accessor. Best-effort: a non-Prediction
|
|
952
968
|
# or immutable return must never break the call.
|
|
953
|
-
if
|
|
969
|
+
if captured_outputs is not None:
|
|
954
970
|
try:
|
|
955
|
-
|
|
971
|
+
captured_outputs._cmpnd_trace_id = trace_id_str
|
|
956
972
|
except (AttributeError, TypeError):
|
|
957
973
|
pass
|
|
958
974
|
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
"""Assessments commands.
|
|
2
2
|
|
|
3
3
|
An assessment is a person's judgement of a trace: an up/down/neutral
|
|
4
|
-
conclusion, a prose body, and
|
|
4
|
+
conclusion, a prose body, and annotations. An annotation attaches a note to a
|
|
5
|
+
typed target, given as ``--annotate KIND REF NOTE``. KIND is ``span`` (REF is a
|
|
6
|
+
span id) or ``output`` (REF is an RFC 6901 JSON pointer such as /trends/2). See
|
|
7
|
+
the assessments API.
|
|
5
8
|
"""
|
|
6
9
|
|
|
7
10
|
from __future__ import annotations
|
|
@@ -11,6 +14,9 @@ from typing import Any
|
|
|
11
14
|
|
|
12
15
|
from .. import api_client, pagination
|
|
13
16
|
|
|
17
|
+
# The annotation kinds --annotate accepts; sent through to the wire unchanged.
|
|
18
|
+
_ANNOTATION_KINDS = {"span", "output"}
|
|
19
|
+
|
|
14
20
|
|
|
15
21
|
def register(subparsers: _SubParsersAction[ArgumentParser]) -> None:
|
|
16
22
|
parser = subparsers.add_parser("assessments", help="Record and read judgements of traces")
|
|
@@ -38,9 +44,10 @@ def register(subparsers: _SubParsersAction[ArgumentParser]) -> None:
|
|
|
38
44
|
"--annotate",
|
|
39
45
|
dest="annotations",
|
|
40
46
|
action="append",
|
|
41
|
-
nargs=
|
|
42
|
-
metavar=("
|
|
43
|
-
help="Attach a note to
|
|
47
|
+
nargs=3,
|
|
48
|
+
metavar=("KIND", "REF", "NOTE"),
|
|
49
|
+
help="Attach a note to a target: KIND is 'span' (REF is a span id) or "
|
|
50
|
+
"'output' (REF is an RFC 6901 JSON pointer like /trends/2). Repeat for multiple.",
|
|
44
51
|
)
|
|
45
52
|
create.set_defaults(func=handle_create)
|
|
46
53
|
|
|
@@ -68,8 +75,15 @@ def handle_create(client: api_client.ApiClient, args: Any) -> Any:
|
|
|
68
75
|
body: dict[str, Any] = {"trace_id": args.trace_id, "conclusion": args.conclusion}
|
|
69
76
|
if args.body:
|
|
70
77
|
body["body"] = args.body
|
|
71
|
-
|
|
72
|
-
|
|
78
|
+
annotations: list[dict[str, str]] = []
|
|
79
|
+
for kind, ref, note in args.annotations or []:
|
|
80
|
+
if kind not in _ANNOTATION_KINDS:
|
|
81
|
+
raise api_client.CliError(
|
|
82
|
+
f"--annotate KIND must be one of: {', '.join(sorted(_ANNOTATION_KINDS))} (got {kind!r})"
|
|
83
|
+
)
|
|
84
|
+
annotations.append({"kind": kind, "ref": ref, "note": note})
|
|
85
|
+
if annotations:
|
|
86
|
+
body["annotations"] = annotations
|
|
73
87
|
return client.post("/api/v1/assessments", json=body)
|
|
74
88
|
|
|
75
89
|
|
|
@@ -181,9 +181,16 @@ class Client:
|
|
|
181
181
|
*,
|
|
182
182
|
conclusion: Literal["up", "down", "neutral"] = "neutral",
|
|
183
183
|
body: str | None = None,
|
|
184
|
-
annotations: list[tuple[str, str]] | None = None,
|
|
184
|
+
annotations: list[tuple[str, str, str]] | None = None,
|
|
185
185
|
) -> dict[str, Any]:
|
|
186
|
-
"""Record a judgement of a trace. An omitted conclusion is neutral.
|
|
186
|
+
"""Record a judgement of a trace. An omitted conclusion is neutral.
|
|
187
|
+
|
|
188
|
+
``annotations`` attach notes to typed targets, each a ``(kind, ref,
|
|
189
|
+
note)`` tuple — ``kind`` is ``"span"`` (``ref`` is a span id) or
|
|
190
|
+
``"output"`` (``ref`` is an RFC 6901 JSON pointer like ``/trends/2``),
|
|
191
|
+
e.g. ``annotations=[("output", "/trends/2", "the third trend"),
|
|
192
|
+
("span", "a1b2c3", "wrong field")]``.
|
|
193
|
+
"""
|
|
187
194
|
self._check_active()
|
|
188
195
|
_exporter_pool.flush(self._config) # the referenced trace must be durable first
|
|
189
196
|
return _assessments.create(
|
|
@@ -45,6 +45,37 @@ def clean_inputs(inputs: dict[str, Any], _dspy: Any, max_field_size: int) -> dic
|
|
|
45
45
|
return result
|
|
46
46
|
|
|
47
47
|
|
|
48
|
+
def is_gepa_bootstrap_wrapper(instance: Any) -> bool:
|
|
49
|
+
"""Return whether DSPy's bootstrap tracer temporarily patched this module."""
|
|
50
|
+
forward = getattr(instance, "forward", None)
|
|
51
|
+
function = getattr(forward, "__func__", forward)
|
|
52
|
+
return (
|
|
53
|
+
getattr(function, "__module__", None) == "dspy.teleprompt.bootstrap_trace"
|
|
54
|
+
and getattr(function, "__name__", None) == "patched_forward"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def normalize_gepa_bootstrap_output(outputs: Any, dspy: Any, *, in_optimization: bool) -> Any:
|
|
59
|
+
"""Return the prediction from a GEPA ``(prediction, trace)`` result pair."""
|
|
60
|
+
if not in_optimization or not isinstance(outputs, tuple) or len(outputs) != 2:
|
|
61
|
+
return outputs
|
|
62
|
+
|
|
63
|
+
prediction, trace = outputs
|
|
64
|
+
if not isinstance(trace, list):
|
|
65
|
+
return outputs
|
|
66
|
+
|
|
67
|
+
for entry in trace:
|
|
68
|
+
if (
|
|
69
|
+
not isinstance(entry, tuple)
|
|
70
|
+
or len(entry) != 3
|
|
71
|
+
or not isinstance(entry[0], dspy.Module)
|
|
72
|
+
or not isinstance(entry[1], dict)
|
|
73
|
+
):
|
|
74
|
+
return outputs
|
|
75
|
+
|
|
76
|
+
return prediction
|
|
77
|
+
|
|
78
|
+
|
|
48
79
|
def clean_outputs(outputs: Any, _instance: Any, dspy: Any, max_field_size: int) -> dict[str, Any]:
|
|
49
80
|
"""Clean and prepare outputs for storage, with per-field truncation."""
|
|
50
81
|
if isinstance(outputs, dspy.Prediction):
|
|
@@ -123,19 +123,25 @@ warn. On the page-number commands `--offset` must be a whole multiple of
|
|
|
123
123
|
## Assessments
|
|
124
124
|
|
|
125
125
|
`cmpnd assessments` records and reads a person's judgement of a trace: an
|
|
126
|
-
up/down/neutral conclusion, an optional prose body, and
|
|
126
|
+
up/down/neutral conclusion, an optional prose body, and annotations. An
|
|
127
|
+
annotation attaches a note to a typed target — a span, or a piece of the output
|
|
128
|
+
addressed by a JSON pointer.
|
|
127
129
|
|
|
128
130
|
> **In development — behind `CMPND_CLI_DEV=1`.** The group is registered only
|
|
129
131
|
> when that env var is set (mirroring `CMPND_CLI_ADMIN`), so it stays hidden from
|
|
130
132
|
> `cmpnd --help` until the flow is finished. The env var only hides the commands;
|
|
131
133
|
> the server still gates each action on the `assessments:*` scope.
|
|
132
134
|
|
|
133
|
-
- `cmpnd assessments create <trace-id> [--up|--down|--neutral] [-m "prose"] [--annotate
|
|
135
|
+
- `cmpnd assessments create <trace-id> [--up|--down|--neutral] [-m "prose"] [--annotate KIND REF NOTE …]`
|
|
134
136
|
— record one judgement. The conclusion flag is optional and mutually exclusive
|
|
135
137
|
(`--up` affirms the trace's output, `--down` disputes it, `--neutral` — the
|
|
136
138
|
default when none is given — does neither). `-m`/`--message` sets the prose
|
|
137
|
-
body. `--annotate` takes
|
|
138
|
-
|
|
139
|
+
body. `--annotate` takes three values: `KIND` is `span` (`REF` is a span id) or
|
|
140
|
+
`output` (`REF` is an RFC 6901 JSON pointer such as `/trends/2` into the trace's
|
|
141
|
+
root-span outputs), and `NOTE` is the prose. It repeats, so a judgement can
|
|
142
|
+
carry several annotations of either kind in the order given. The server rejects
|
|
143
|
+
a span id absent from the trace and a pointer that does not resolve against the
|
|
144
|
+
outputs.
|
|
139
145
|
- `cmpnd assessments list (<program> | --trace <trace-id>) [--limit N] [--offset N]`
|
|
140
146
|
— the assessments recorded against a program (the program signature hash is
|
|
141
147
|
positional, in the 16-char hex form `cmpnd signatures list` prints) or against
|
|
@@ -85,6 +85,10 @@ platform's data, so the whole surface is one `dir(sdk)` away:
|
|
|
85
85
|
- **Assessments** — `sdk.create_assessment(trace_id, conclusion=..., body=..., annotations=...)`,
|
|
86
86
|
`sdk.list_assessments(program=... | trace_id=..., limit=..., offset=...)`,
|
|
87
87
|
`sdk.get_assessment(assessment_id)`, `sdk.delete_assessment(assessment_id)`.
|
|
88
|
+
`annotations` are `(kind, ref, note)` tuples — `kind` is `"span"` (`ref` is a
|
|
89
|
+
span id) or `"output"` (`ref` is an RFC 6901 pointer such as `/trends/2`), e.g.
|
|
90
|
+
`annotations=[("output", "/trends/2", "the third trend"), ("span", "a1b2c3",
|
|
91
|
+
"wrong field")]`.
|
|
88
92
|
- **Valsets** — `sdk.create_valset(program, name=...)`,
|
|
89
93
|
`sdk.list_valsets(program, limit=..., offset=...)`, `sdk.get_valset(valset_id)`,
|
|
90
94
|
`sdk.list_valset_entries(valset_id, limit=..., offset=...)`,
|
|
@@ -86,6 +86,11 @@ The hook methods, in the order you'd expect them to fire for a typical `dspy.Cha
|
|
|
86
86
|
- **Interpreter (code execution)** — `_InterpreterMixin` (`callback.py:1119`), for the DSPy `CodeInterpreter` hooks DSPy >= 3.3.1 fires around a program running code in the interpreter (`RLM`, `ProgramOfThought`, `CodeAct`, `Flex` — every module reaching it through `_create_interpreter`; `ReAct` / `ReActV2` use no interpreter and fire none). `on_interpreter_execute_start` / `_end` emit one `INTERPRETER_EXECUTE` span per REPL cell — the code in, its captured stdout out, and the `SUBMIT()` result (a DSPy `FinalOutput`) recorded distinctly from stdout. `on_interpreter_tool_call_start` / `_end` synthesize a `TOOL` span for a tool called from interpreter code **only when no real `dspy.Tool` span covered the call** (a user tool routes through `Tool.__call__` and fires `on_tool_*`, so its row is suppressed to avoid a duplicate; the `llm_query` builtins fire no tool hook, so they get the synthesized span). `on_interpreter_startup_*` / `on_interpreter_shutdown_*` emit no span — their duration folds into an `interpreter.startup_ms` / `interpreter.shutdown_ms` attribute on the enclosing span, and an event with no enclosing span (a pooled pre-warm) is dropped.
|
|
87
87
|
- **Evaluate** — `on_evaluate_start` (`callback.py:773`) / `on_evaluate_end` (`callback.py:782`). These are thin forwarders that delegate to `EvalHandler` (see "Eval handling" below).
|
|
88
88
|
|
|
89
|
+
During a GEPA evaluation, DSPy wraps the program prediction with its internal
|
|
90
|
+
bootstrap trace. The callback recognizes this pair only while an optimization
|
|
91
|
+
is active. It records the prediction on the parent span and leaves the internal
|
|
92
|
+
trace available to GEPA.
|
|
93
|
+
|
|
89
94
|
Not inheriting the base class costs the free no-op every hook would otherwise
|
|
90
95
|
have. DSPy resolves a handler with a bare `getattr(callback, "on_…")` and does
|
|
91
96
|
not guard for a callback that omits one, so a DSPy release that adds hooks turns
|
|
@@ -5,7 +5,7 @@ name = "cmpnd"
|
|
|
5
5
|
# it's new) — no git tags. Bump with `uv version --bump {minor,patch}` or by
|
|
6
6
|
# hand. Runtime `cmpnd.__version__` reads installed metadata, so it tracks
|
|
7
7
|
# this automatically. See docs/plans/2026-07-02-sdk-pypi-tag-free-publish.md.
|
|
8
|
-
version = "0.
|
|
8
|
+
version = "0.31.0"
|
|
9
9
|
description = "DSPy observability and deployment SDK for cmpnd"
|
|
10
10
|
readme = "README.md"
|
|
11
11
|
requires-python = ">=3.10"
|
|
@@ -44,14 +44,18 @@ class TestAssessmentsCliE2E:
|
|
|
44
44
|
|
|
45
45
|
client = api_client.ApiClient(CMPND_E2E_API_KEY, E2E_ENDPOINT)
|
|
46
46
|
|
|
47
|
-
# create — a down judgement with one span annotation
|
|
47
|
+
# create — a down judgement with one span annotation and one output
|
|
48
|
+
# pointer into the root span's `answer` output.
|
|
48
49
|
created = assessments.handle_create(
|
|
49
50
|
client,
|
|
50
51
|
_ns(
|
|
51
52
|
trace_id=trace_id,
|
|
52
53
|
conclusion="down",
|
|
53
54
|
body="the answer names the office, not the representative",
|
|
54
|
-
annotations=[
|
|
55
|
+
annotations=[
|
|
56
|
+
["span", span_id, "this predict returned the wrong field"],
|
|
57
|
+
["output", "/answer", "the answer field is the wrong value"],
|
|
58
|
+
],
|
|
55
59
|
),
|
|
56
60
|
)
|
|
57
61
|
assert created["id"].startswith("as_")
|
|
@@ -59,7 +63,9 @@ class TestAssessmentsCliE2E:
|
|
|
59
63
|
# The program hash the server copied off the trace round-trips as the same
|
|
60
64
|
# 16-hex string the trace read returned — the CLI passes it through verbatim.
|
|
61
65
|
assert created["program_signature_hash"] == program_hash
|
|
62
|
-
|
|
66
|
+
anns = {(a["kind"], a["ref"]): a for a in created["annotations"]}
|
|
67
|
+
assert ("span", span_id) in anns
|
|
68
|
+
assert ("output", "/answer") in anns
|
|
63
69
|
|
|
64
70
|
# list by program (the 16-hex form) finds the assessment we just created.
|
|
65
71
|
listed = assessments.handle_list(client, _ns(program=program_hash, trace_id=None))
|
|
@@ -84,7 +84,12 @@ class TestValsetsCliE2E:
|
|
|
84
84
|
# An assessment of the trace, then an entry created from it.
|
|
85
85
|
assessment = assessments.handle_create(
|
|
86
86
|
client,
|
|
87
|
-
_ns(
|
|
87
|
+
_ns(
|
|
88
|
+
trace_id=trace_id,
|
|
89
|
+
conclusion="down",
|
|
90
|
+
body="corrected",
|
|
91
|
+
annotations=[["span", span_id, "wrong field"]],
|
|
92
|
+
),
|
|
88
93
|
)
|
|
89
94
|
vs = valsets.handle_create(
|
|
90
95
|
client, _ns(program_hash=program_hash, name=f"e2e from-assessment {uuid.uuid4().hex[:8]}")
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import time
|
|
6
|
+
import urllib.error
|
|
6
7
|
from collections.abc import Callable, Collection, Mapping, Sequence
|
|
7
8
|
from dataclasses import dataclass
|
|
8
9
|
from typing import Generic, TypeAlias, TypeVar
|
|
@@ -99,21 +100,64 @@ def each_scope_has_real_model(
|
|
|
99
100
|
)
|
|
100
101
|
|
|
101
102
|
|
|
103
|
+
def read_tolerating_timeout(
|
|
104
|
+
read: Callable[[RemainingTimeout], T],
|
|
105
|
+
unpublished: T,
|
|
106
|
+
) -> Callable[[RemainingTimeout], T]:
|
|
107
|
+
"""Wrap a read so a request timeout counts as "not published yet".
|
|
108
|
+
|
|
109
|
+
Only a timeout is tolerated: a read-phase ``TimeoutError`` (a socket timeout
|
|
110
|
+
after the request was sent) and a connect-phase ``urllib.error.URLError``
|
|
111
|
+
whose ``.reason`` is a timeout. Inside a bounded poll that is not a failure —
|
|
112
|
+
the value is simply not visible yet — so return ``unpublished`` and let
|
|
113
|
+
``wait_for_publication`` poll again to the deadline, rather than crashing the
|
|
114
|
+
whole run on one slow request. Other ``URLError`` reasons (DNS, TLS,
|
|
115
|
+
connection refused) propagate: they are real misconfigurations to surface, not
|
|
116
|
+
"not published yet", so they must fail rather than be retried to the deadline.
|
|
117
|
+
Every other exception still surfaces too, so a real HTTP error (which the
|
|
118
|
+
callers raise as ``AssertionError``) still fails fast. ``socket.timeout`` is an
|
|
119
|
+
alias of ``TimeoutError`` on Python 3.10+, so the checks cover it. Pair with
|
|
120
|
+
``wait_for_publication``'s ``read_timeout`` cap, which bounds how long each
|
|
121
|
+
such request can block before it is retried.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
def wrapped(remaining_timeout: RemainingTimeout) -> T:
|
|
125
|
+
try:
|
|
126
|
+
return read(remaining_timeout)
|
|
127
|
+
except TimeoutError:
|
|
128
|
+
return unpublished
|
|
129
|
+
except urllib.error.URLError as exc:
|
|
130
|
+
# URLError also wraps DNS / TLS / connection-refused failures, which
|
|
131
|
+
# are real problems to surface — not "not published yet". Only a
|
|
132
|
+
# connect-phase timeout (its reason is a timeout) is tolerated.
|
|
133
|
+
if isinstance(exc.reason, TimeoutError):
|
|
134
|
+
return unpublished
|
|
135
|
+
raise
|
|
136
|
+
|
|
137
|
+
return wrapped
|
|
138
|
+
|
|
139
|
+
|
|
102
140
|
def wait_for_publication(
|
|
103
141
|
read: Callable[[RemainingTimeout], T],
|
|
104
142
|
ready: Callable[[T], bool],
|
|
105
143
|
*,
|
|
106
144
|
timeout: float = 90.0,
|
|
107
145
|
interval: float = 3.0,
|
|
146
|
+
read_timeout: float = 10.0,
|
|
108
147
|
) -> PublicationResult[T]:
|
|
109
148
|
"""Read until ``ready`` observes publication within one monotonic deadline.
|
|
110
149
|
|
|
111
|
-
``read`` receives ``remaining_timeout`` and must use a fresh
|
|
112
|
-
for each blocking HTTP request it makes
|
|
113
|
-
network-operation timeouts.
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
150
|
+
``read`` receives a ``remaining_timeout`` callable and must use a fresh
|
|
151
|
+
positive value for each blocking HTTP request it makes; the HTTP client uses
|
|
152
|
+
that value for network-operation timeouts. That value is capped at
|
|
153
|
+
``read_timeout`` so one slow request cannot consume the whole window — the loop
|
|
154
|
+
keeps polling to the deadline instead of blocking for the full budget in a
|
|
155
|
+
single attempt. ``read`` should treat a request timeout as "not published yet"
|
|
156
|
+
(wrap it with ``read_tolerating_timeout``) so a slow read is retried rather
|
|
157
|
+
than fatal; other exceptions from either callback surface immediately. This
|
|
158
|
+
helper rejects late results but does not cancel in-flight requests. The
|
|
159
|
+
returned value remains available for a scoped timeout diagnostic, but a result
|
|
160
|
+
observed after the deadline is not accepted.
|
|
117
161
|
"""
|
|
118
162
|
deadline = time.monotonic() + timeout
|
|
119
163
|
last_value: T | None = None
|
|
@@ -122,8 +166,14 @@ def wait_for_publication(
|
|
|
122
166
|
remaining = deadline - time.monotonic()
|
|
123
167
|
return remaining if remaining > 0 else None
|
|
124
168
|
|
|
169
|
+
def read_budget() -> float | None:
|
|
170
|
+
# Bound each request so a single slow read is capped and the loop can poll
|
|
171
|
+
# again, rather than spending the entire deadline waiting on one request.
|
|
172
|
+
remaining = remaining_timeout()
|
|
173
|
+
return None if remaining is None else min(remaining, read_timeout)
|
|
174
|
+
|
|
125
175
|
while remaining_timeout() is not None:
|
|
126
|
-
last_value = read(
|
|
176
|
+
last_value = read(read_budget)
|
|
127
177
|
if remaining_timeout() is None:
|
|
128
178
|
return PublicationResult(published=False, value=last_value)
|
|
129
179
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import time
|
|
6
|
+
import urllib.error
|
|
6
7
|
|
|
7
8
|
import pytest
|
|
8
9
|
|
|
@@ -51,6 +52,87 @@ def test_wait_for_publication_returns_a_late_published_read(monkeypatch):
|
|
|
51
52
|
assert clock.sleeps == [2.0]
|
|
52
53
|
|
|
53
54
|
|
|
55
|
+
def test_wait_for_publication_caps_each_read_below_the_full_budget(monkeypatch):
|
|
56
|
+
clock = install_fake_clock(monkeypatch)
|
|
57
|
+
response_timeouts: list[float | None] = []
|
|
58
|
+
responses = iter([[], [{"trace_id": "trace-1"}]])
|
|
59
|
+
|
|
60
|
+
def read(remaining_timeout):
|
|
61
|
+
response_timeouts.append(remaining_timeout())
|
|
62
|
+
return next(responses)
|
|
63
|
+
|
|
64
|
+
result = _readiness.wait_for_publication(read, bool, timeout=90.0, interval=3.0, read_timeout=10.0)
|
|
65
|
+
|
|
66
|
+
assert result.published
|
|
67
|
+
# Each request is bounded by read_timeout (10.0), never the whole remaining
|
|
68
|
+
# budget (90.0, then 87.0), so one slow read cannot spend the window.
|
|
69
|
+
assert response_timeouts == [10.0, 10.0]
|
|
70
|
+
assert clock.sleeps == [3.0]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_read_tolerating_timeout_treats_a_read_timeout_as_unpublished():
|
|
74
|
+
def read(_remaining_timeout):
|
|
75
|
+
raise TimeoutError("the read operation timed out")
|
|
76
|
+
|
|
77
|
+
wrapped = _readiness.read_tolerating_timeout(read, [])
|
|
78
|
+
|
|
79
|
+
assert wrapped(lambda: 5.0) == []
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_read_tolerating_timeout_treats_a_connect_phase_timeout_as_unpublished():
|
|
83
|
+
def read(_remaining_timeout):
|
|
84
|
+
raise urllib.error.URLError(TimeoutError("timed out"))
|
|
85
|
+
|
|
86
|
+
assert _readiness.read_tolerating_timeout(read, [])(lambda: 5.0) == []
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_read_tolerating_timeout_surfaces_a_non_timeout_urlerror():
|
|
90
|
+
def read(_remaining_timeout):
|
|
91
|
+
raise urllib.error.URLError(ConnectionRefusedError("refused"))
|
|
92
|
+
|
|
93
|
+
with pytest.raises(urllib.error.URLError):
|
|
94
|
+
_readiness.read_tolerating_timeout(read, [])(lambda: 5.0)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_read_tolerating_timeout_passes_a_successful_read_through():
|
|
98
|
+
def read(_remaining_timeout):
|
|
99
|
+
return ["value"]
|
|
100
|
+
|
|
101
|
+
assert _readiness.read_tolerating_timeout(read, [])(lambda: 5.0) == ["value"]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_read_tolerating_timeout_does_not_swallow_other_errors():
|
|
105
|
+
def read(_remaining_timeout):
|
|
106
|
+
raise RuntimeError("POST /api/v1/traces/search -> HTTP 500")
|
|
107
|
+
|
|
108
|
+
with pytest.raises(RuntimeError, match="HTTP 500"):
|
|
109
|
+
_readiness.read_tolerating_timeout(read, [])(lambda: 5.0)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_wait_for_publication_keeps_polling_after_a_read_timeout(monkeypatch):
|
|
113
|
+
clock = install_fake_clock(monkeypatch)
|
|
114
|
+
attempts = {"n": 0}
|
|
115
|
+
|
|
116
|
+
def read(_remaining_timeout):
|
|
117
|
+
attempts["n"] += 1
|
|
118
|
+
if attempts["n"] == 1:
|
|
119
|
+
raise TimeoutError("the read operation timed out")
|
|
120
|
+
return [{"trace_id": "trace-1"}]
|
|
121
|
+
|
|
122
|
+
result = _readiness.wait_for_publication(
|
|
123
|
+
_readiness.read_tolerating_timeout(read, []),
|
|
124
|
+
bool,
|
|
125
|
+
timeout=90.0,
|
|
126
|
+
interval=3.0,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# A timed-out first poll is retried to publication, not fatal.
|
|
130
|
+
assert result.published
|
|
131
|
+
assert result.value == [{"trace_id": "trace-1"}]
|
|
132
|
+
assert attempts["n"] == 2
|
|
133
|
+
assert clock.sleeps == [3.0]
|
|
134
|
+
|
|
135
|
+
|
|
54
136
|
def test_wait_for_publication_returns_the_last_missing_value_at_deadline(monkeypatch):
|
|
55
137
|
clock = install_fake_clock(monkeypatch)
|
|
56
138
|
response_timeouts: list[float] = []
|
|
@@ -110,9 +110,12 @@ def test_cross_trace_q_search_finds_the_span_token():
|
|
|
110
110
|
return _search(token, tag, timeout=timeout)
|
|
111
111
|
|
|
112
112
|
# The projected search reader can finish a request before it folds this
|
|
113
|
-
# trace. Poll the same scoped read without writing another trace.
|
|
113
|
+
# trace. Poll the same scoped read without writing another trace. A slow or
|
|
114
|
+
# unresponsive search read on a loaded staging box is "not published yet",
|
|
115
|
+
# not a failure: read_tolerating_timeout retries it to the deadline rather
|
|
116
|
+
# than letting one timed-out request eject the run.
|
|
114
117
|
publication = _readiness.wait_for_publication(
|
|
115
|
-
search_published_trace,
|
|
118
|
+
_readiness.read_tolerating_timeout(search_published_trace, []),
|
|
116
119
|
lambda traces: [trace["trace_id"] for trace in traces] == [trace_id],
|
|
117
120
|
)
|
|
118
121
|
found = publication.value or []
|
|
@@ -126,5 +129,23 @@ def test_cross_trace_q_search_finds_the_span_token():
|
|
|
126
129
|
|
|
127
130
|
# A token no span carries returns nothing under the same tag — proof the match
|
|
128
131
|
# is real token filtering, not the tag scope handing back every tagged trace.
|
|
129
|
-
|
|
130
|
-
|
|
132
|
+
# Bound it like the positive poll so a slow staging read cannot eject the lane
|
|
133
|
+
# with an uncaught timeout. Sentinel None (not []) marks a timed-out read,
|
|
134
|
+
# since [] is the success value; any real response ends the poll, and a
|
|
135
|
+
# deadline of only-timeouts fails readably rather than falsely confirming
|
|
136
|
+
# absence.
|
|
137
|
+
def search_absent_token(remaining_timeout: _readiness.RemainingTimeout) -> list[typing.Any] | None:
|
|
138
|
+
timeout = remaining_timeout()
|
|
139
|
+
if timeout is None:
|
|
140
|
+
return None
|
|
141
|
+
return _search(f"nomatch{run}", tag, timeout=timeout)
|
|
142
|
+
|
|
143
|
+
negative = _readiness.wait_for_publication(
|
|
144
|
+
_readiness.read_tolerating_timeout(search_absent_token, None),
|
|
145
|
+
lambda result: result is not None,
|
|
146
|
+
)
|
|
147
|
+
assert negative.published, (
|
|
148
|
+
"negative-token search did not return within 90 seconds (search read timed "
|
|
149
|
+
"out under load); token absence could not be confirmed"
|
|
150
|
+
)
|
|
151
|
+
assert negative.value == [], f"q= matched a token no span carries: {negative.value}"
|