cmpnd 0.27.2__tar.gz → 0.27.4__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.27.2 → cmpnd-0.27.4}/PKG-INFO +1 -1
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/__init__.py +5 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/_authored_import.py +0 -1
- cmpnd-0.27.4/cmpnd/_runtime_tools.py +39 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/deployment.py +3 -47
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/ir/reflection.py +12 -3
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/mcp_servers.py +12 -1
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/packaging.py +171 -36
- {cmpnd-0.27.2 → cmpnd-0.27.4}/docs/cli.md +8 -6
- {cmpnd-0.27.2 → cmpnd-0.27.4}/docs/sdk-instrumentation.md +32 -1
- {cmpnd-0.27.2 → cmpnd-0.27.4}/pyproject.toml +1 -1
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/test_traces_read.py +19 -5
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/integration/hosted_mcp_tool_optimize_probe.py +66 -8
- cmpnd-0.27.4/tests/stress/_readiness.py +139 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/stress/test_optimize_loop_wasm.py +53 -25
- cmpnd-0.27.4/tests/stress/test_readiness.py +301 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/stress/test_trace_search.py +25 -10
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_deploy_mcp_tool_references.py +53 -25
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_exporter.py +5 -3
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_packaging_mcp_tools.py +203 -2
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_reflector_properties.py +55 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_reflector_smoke.py +14 -1
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_retry.py +1 -0
- cmpnd-0.27.4/tests/test_runtime_tools.py +47 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/uv.lock +1 -1
- {cmpnd-0.27.2 → cmpnd-0.27.4}/.gitignore +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/AGENTS.md +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/CLAUDE.md +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/README.md +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/_assessments.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/_exporter_pool.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/_gepa_patch.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/_http.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/_program_patch.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/_rlm_patch.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/_timespec.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/_traces.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/_valsets.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/authored.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/callback.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/cli/__init__.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/cli/api_client.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/cli/commands/__init__.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/cli/commands/admin.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/cli/commands/assessments.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/cli/commands/auth.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/cli/commands/datasets.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/cli/commands/deployments.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/cli/commands/evals.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/cli/commands/login.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/cli/commands/optimizations.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/cli/commands/org.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/cli/commands/programs.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/cli/commands/provider_credentials.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/cli/commands/signatures.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/cli/commands/tool_servers.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/cli/commands/top.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/cli/commands/traces.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/cli/commands/valsets.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/cli/credentials.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/cli/deprecation.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/cli/pagination.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/cli/shell.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/cli/trace_render.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/cli/tree_help.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/cli/verbs.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/client.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/configuration.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/context.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/dataset_sync.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/datasets.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/decorators.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/eval_handler.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/execution.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/exporter.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/exporter_http.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/helpers.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/identity.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/imaging.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/ir/__init__.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/ir/decode.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/ir/encode.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/ir/hash.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/ir/program.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/ir/types.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/ir/xxh64.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/lm_configuration.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/models.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/optimization.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/optimizers/__init__.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/optimizers/gepa.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/optimizers/gepa_callback.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/optimizers/resume.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/optimizers/tracker.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/retry.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/cmpnd/traces.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/docs/2026-04-02-code-review.md +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/docs/client.md +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/docs/custom-proposer.md +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/docs/plans/2026-04-05-train-val-dataset-capture-design.md +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/docs/plans/2026-04-05-train-val-dataset-capture.md +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/docs/reading-traces.md +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/examples/generalizing_proposer.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/examples/local_ollama.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/ingest/LINEAGE.md +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/ingest/README.md +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/ingest/cmpnd_ingest/__init__.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/ingest/cmpnd_ingest/bodies.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/ingest/cmpnd_ingest/exporter_http.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/ingest/cmpnd_ingest/protocols.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/ingest/cmpnd_ingest/py.typed +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/ingest/cmpnd_ingest/request_log.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/ingest/cmpnd_ingest/retry.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/ingest/pyproject.toml +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/scripts/audit-sdist.sh +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/scripts/capture_review_run.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/scripts/check_release_artifacts.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/scripts/fixtures/review_run.json.gz +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/scripts/gepa_resume_bug.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/scripts/release-stress.sh +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/scripts/require_version_bump.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/scripts/seed_review_run.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/scripts/test-in-linux.sh +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/stubs/dspy/__init__.pyi +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/stubs/dspy/primitives/__init__.pyi +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/stubs/dspy/primitives/prediction.pyi +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/stubs/dspy/utils/__init__.pyi +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/stubs/dspy/utils/callback.pyi +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/__init__.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/_exporter_helpers.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/_fake_clock.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/_identity_helpers.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/conftest.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/__init__.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/committee_task.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/conftest.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/data/committee_train.ndjson.gz +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/data/committee_val.ndjson.gz +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/fake_lm.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/helpers.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/target_capabilities.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/test_assessments_cli.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/test_capture_prompts.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/test_composite_module.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/test_dataset_auto_creation.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/test_deploy_execute.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/test_deploy_programs.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/test_edge_cases.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/test_example_hash.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/test_gepa_optimization.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/test_interpreter_spans.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/test_lm_accuracy.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/test_module_tracing.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/test_optimization_lineage.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/test_optimization_metric_identity.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/test_optimize_failures.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/test_optimize_loop.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/test_optimize_review_committee.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/test_optimize_server_owned_completion.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/test_parallel_export.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/test_project_attribution.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/test_react_tool_spans.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/test_save_load.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/test_trace_structure.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/test_user_attribution.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/e2e/test_valsets_cli.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/identity_vectors/01_predict_qa.json +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/identity_vectors/02_predict_renamed_field.json +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/identity_vectors/03_chain_of_thought_qa.json +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/identity_vectors/04_predict_richer_types.json +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/identity_vectors/05_composite_two_predicts.json +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/identity_vectors/06_multichaincomparison.json +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/identity_vectors/07_program_of_thought.json +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/identity_vectors/08_opaque_module.json +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/identity_vectors/09_predict_pydantic_field.json +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/identity_vectors/10_react.json +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/identity_vectors/11_refine.json +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/identity_vectors/12_retrieve.json +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/identity_vectors/13_knn.json +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/identity_vectors/14_parallel.json +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/identity_vectors/README.md +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/ingest_conformance.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/integration/__init__.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/integration/authored_optimize/judge.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/integration/authored_optimize/optimize_hosted.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/integration/conftest.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/integration/hosted_optimize_probe.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/integration/optimization_run_probe.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/integration/test_cli_deploy_smoketest.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/integration/test_logs_renders_traces.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/integration/test_optimization_run.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/integration/test_optimizing_progress.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/integration/test_unhappy_path.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/load/__init__.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/load/__main__.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/load/drain.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/load/ingest.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/load/latency_transport.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/load/rlm_shape.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/load/sweep.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/load/test_rlm_shape.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/parity/__init__.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/parity/conftest.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/parity/seed_parity.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/parity/test_auth.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/parity/test_datasets.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/parity/test_deployments.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/parity/test_evals.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/parity/test_health.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/parity/test_modules.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/parity/test_optimizations.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/parity/test_programs.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/parity/test_signatures.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/parity/test_trace_streaming.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/parity/test_traces.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/stress/__init__.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/stress/_provider.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/stress/_reliability.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/stress/_result.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/stress/mint_bedrock_token.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/stress/test_deploy_programs.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/stress/test_multi_client_stress.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/stress/test_optimize_client_committee.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/stress/test_optimize_loop.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/stress/test_optimize_stress.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/stress/test_provider_roles.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/stress/test_real_provider_twins.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/stress/test_reliability.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/stress/test_reliability_classify.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/stress/test_rlm_wasm.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_assessments_api.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_authored.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_authored_bundle_collisions.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_authored_metric_contract.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_authored_named_models.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_authored_optimize.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_callback.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_candidate_lineage.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_cli/__init__.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_cli/conftest.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_cli/test_admin.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_cli/test_api_client.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_cli/test_assessments.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_cli/test_auth.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_cli/test_credentials.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_cli/test_datasets.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_cli/test_deployments.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_cli/test_evals.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_cli/test_login.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_cli/test_main.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_cli/test_optimizations.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_cli/test_org.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_cli/test_pagination.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_cli/test_parity.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_cli/test_programs.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_cli/test_provider_credentials.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_cli/test_schema_sample.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_cli/test_shell.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_cli/test_signatures.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_cli/test_tool_servers.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_cli/test_top.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_cli/test_traces.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_cli/test_traces_search_wire.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_cli/test_tree_help.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_cli/test_valsets.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_client.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_clustering/__init__.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_clustering/conftest.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_clustering/test_edge_cases.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_clustering/test_hash_consistency.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_clustering/test_type_conversion.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_cmp551_grouping_identity.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_configuration.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_context.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_context_propagation.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_dataset_polling.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_dataset_sync.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_datasets.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_decorators.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_deploy.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_eval.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_eval_example_attribution.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_eval_start.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_eval_trace_grouping.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_exception_paths.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_execute.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_exporter_queue.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_exporter_real.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_fake_lm.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_gepa_callback.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_gepa_integration.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_gepa_resume.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_identity.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_imaging.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_import_weight.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_ingest_conformance.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_ingest_second_producer.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_integration.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_interpreter_callback.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_ir_encoder_properties.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_ir_golden_vectors.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_ir_roundtrip.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_lm_configuration.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_lm_usage_shapes.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_mcp_tools.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_models.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_optimization_progress.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_optimization_start.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_optimize.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_optimize_budget.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_optimize_lift.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_optimize_lms.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_optimize_proposer.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_optimize_reflection_lm.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_optimize_terminal_failure.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_packaging.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_packaging_callable_source.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_packaging_program_source.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_poll_until.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_program_lineage_persist.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_project_payload.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_release_stress_script.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_repro_reactv2_bugs.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_require_version_bump.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_reversible_failures.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_rlm_patch.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_schemas.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_seed_review_run.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_span_sent_once.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_trace_render.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_traces_read.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_tracked_gepa.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_tracker_enrich.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_truncation.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_valsets_api.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/tests/test_xxh64.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.4}/todo.md +0 -0
|
@@ -214,3 +214,8 @@ def de_instrument() -> None:
|
|
|
214
214
|
_gepa_patch.unpatch_gepa()
|
|
215
215
|
_program_patch.unpatch_program_state()
|
|
216
216
|
_rlm_patch.unpatch_rlm_tools()
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _mcp_tool(declaration): # pyright: ignore[reportUnusedFunction] -- generated bundles call this hook
|
|
220
|
+
"""Keep generated declarations importable outside a worker without granting access."""
|
|
221
|
+
return declaration
|
|
@@ -122,7 +122,6 @@ def _program_facts(
|
|
|
122
122
|
from .authored import ProgramFacts
|
|
123
123
|
|
|
124
124
|
references = deployment._mcp_tool_references(program)
|
|
125
|
-
deployment._refuse_mcp_tools_outside_rlm(program, references)
|
|
126
125
|
pinned: dict[tuple[str, str], Any] = {}
|
|
127
126
|
if references:
|
|
128
127
|
import httpx
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Private provenance for tools that CMPND adds while a program runs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, TypeVar
|
|
6
|
+
|
|
7
|
+
_RUNTIME_TOOL_MARKER = "_cmpnd_runtime_tool"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class _RuntimeToolProvenance:
|
|
11
|
+
"""An in-process token that keeps its identity through DSPy module copies."""
|
|
12
|
+
|
|
13
|
+
def __copy__(self) -> _RuntimeToolProvenance:
|
|
14
|
+
return self
|
|
15
|
+
|
|
16
|
+
def __deepcopy__(self, memo: dict[int, Any]) -> _RuntimeToolProvenance:
|
|
17
|
+
del memo
|
|
18
|
+
return self
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
_RUNTIME_TOOL_PROVENANCE = _RuntimeToolProvenance()
|
|
22
|
+
_T = TypeVar("_T")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def mark_runtime_tool(tool: _T) -> _T:
|
|
26
|
+
"""Mark a platform-created tool without trusting a caller-chosen boolean."""
|
|
27
|
+
setattr(tool, _RUNTIME_TOOL_MARKER, _RUNTIME_TOOL_PROVENANCE)
|
|
28
|
+
function = getattr(tool, "func", None)
|
|
29
|
+
if function is not None:
|
|
30
|
+
setattr(function, _RUNTIME_TOOL_MARKER, _RUNTIME_TOOL_PROVENANCE)
|
|
31
|
+
return tool
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def is_runtime_tool(tool: object) -> bool:
|
|
35
|
+
"""Return whether this process marked the tool or its wrapped callable."""
|
|
36
|
+
if getattr(tool, _RUNTIME_TOOL_MARKER, None) is _RUNTIME_TOOL_PROVENANCE:
|
|
37
|
+
return True
|
|
38
|
+
function = getattr(tool, "func", None)
|
|
39
|
+
return getattr(function, _RUNTIME_TOOL_MARKER, None) is _RUNTIME_TOOL_PROVENANCE
|
|
@@ -250,7 +250,7 @@ def _mcp_tool_references(module) -> list[Any]:
|
|
|
250
250
|
"""Every MCP reference the program's tools carry, deduplicated, in a stable order."""
|
|
251
251
|
references: list[Any] = []
|
|
252
252
|
seen: set[tuple[str, str]] = set()
|
|
253
|
-
for tool in packaging.
|
|
253
|
+
for tool in packaging.declared_module_tools(module).values():
|
|
254
254
|
reference = mcp_servers.mcp_tool_reference(tool)
|
|
255
255
|
if reference is None:
|
|
256
256
|
continue
|
|
@@ -262,48 +262,6 @@ def _mcp_tool_references(module) -> list[Any]:
|
|
|
262
262
|
return references
|
|
263
263
|
|
|
264
264
|
|
|
265
|
-
def _refuse_mcp_tools_outside_rlm(module, references: list[Any]) -> None:
|
|
266
|
-
"""Refuse a program that declares MCP tools and is not a `dspy.RLM`.
|
|
267
|
-
|
|
268
|
-
**At deploy, because the alternative arrives mid-turn in a deployed program.** The
|
|
269
|
-
worker answers an MCP-backed tool where `dspy.RLM`'s inner interpreter parks waiting
|
|
270
|
-
for one; that is the only place it can intervene without the kernel holding a token it
|
|
271
|
-
must never see. A `dspy.ReAct` program calls its tools from kernel Python directly, so
|
|
272
|
-
the call reaches the `def` the packager generated and raises — loud, but at run time,
|
|
273
|
-
in front of whoever is reading the answer rather than whoever can change the program.
|
|
274
|
-
|
|
275
|
-
**A state check and not a name check.** `isinstance` against the live class is what
|
|
276
|
-
makes a subclass, a rename and a re-export all answer correctly; matching
|
|
277
|
-
`type(module).__name__` against a list would pass the first and refuse the last. It is
|
|
278
|
-
the reason this lives here and not in serve: the create body's `module_class` is a
|
|
279
|
-
display string a client fills in, and a guard over it would fail open in both
|
|
280
|
-
directions.
|
|
281
|
-
|
|
282
|
-
Absent `dspy.RLM` refuses too, following `cmpnd.callback`'s idiom. A dspy with no RLM
|
|
283
|
-
is a dspy with no path that can serve these tools at all, so refusing is the honest
|
|
284
|
-
answer rather than a gap.
|
|
285
|
-
"""
|
|
286
|
-
if not references:
|
|
287
|
-
return
|
|
288
|
-
# Deferred, as every other dspy use in this module is: a deploy of a program with no
|
|
289
|
-
# MCP tools returns above and never pays for the import.
|
|
290
|
-
import dspy
|
|
291
|
-
|
|
292
|
-
if isinstance(module, getattr(dspy, "RLM", type(None))):
|
|
293
|
-
return
|
|
294
|
-
names = ", ".join(sorted(reference.tool_name for reference in references))
|
|
295
|
-
noun = "the MCP tool" if len(references) == 1 else "the MCP tools"
|
|
296
|
-
raise ValueError(
|
|
297
|
-
f"{type(module).__name__} declares {noun} {names}, and only dspy.RLM can call "
|
|
298
|
-
"them once deployed. The platform answers an MCP tool call underneath the sandbox, "
|
|
299
|
-
"at the point where RLM's interpreter waits for a tool result, and no other module "
|
|
300
|
-
"reaches that point — its tool call runs inside the sandbox, where the call would "
|
|
301
|
-
f"reach a stub and raise. Either wrap this program's work in dspy.RLM, or give "
|
|
302
|
-
f"{type(module).__name__} ordinary Python tools, which deploy as source and run in "
|
|
303
|
-
"the sandbox."
|
|
304
|
-
)
|
|
305
|
-
|
|
306
|
-
|
|
307
265
|
def _server_identity(url: str) -> str:
|
|
308
266
|
"""A tool server's URL in the one form two spellings of it compare equal in.
|
|
309
267
|
|
|
@@ -381,7 +339,7 @@ def _resolve_mcp_tool_references(client: httpx.Client, config, references: list[
|
|
|
381
339
|
server_url=reference.server_url,
|
|
382
340
|
tool_name=reference.tool_name,
|
|
383
341
|
schema_hash=str(tool.get("schema_hash") or ""),
|
|
384
|
-
input_schema=
|
|
342
|
+
input_schema=mcp_servers.registry_schema(tool),
|
|
385
343
|
description=str(tool.get("description") or ""),
|
|
386
344
|
)
|
|
387
345
|
return pinned
|
|
@@ -424,10 +382,8 @@ def deploy(module, metric=None, timeout=300, metadata=None):
|
|
|
424
382
|
# Resolve the program's MCP tool references against the org's registry, before
|
|
425
383
|
# anything is packaged and before a deployment row exists. A program with no MCP
|
|
426
384
|
# tools asks the registry nothing.
|
|
385
|
+
# Validate declarations before contacting the registry or creating a deployment.
|
|
427
386
|
references = _mcp_tool_references(module)
|
|
428
|
-
# Before the round trip, because it needs nothing from the registry: a module that
|
|
429
|
-
# cannot call these tools at all is refused whatever the registry says about them.
|
|
430
|
-
_refuse_mcp_tools_outside_rlm(module, references)
|
|
431
387
|
pinned_mcp_tools: dict[tuple[str, str], Any] = {}
|
|
432
388
|
if references:
|
|
433
389
|
with httpx.Client(timeout=httpx.Timeout(_TIMEOUT_SECONDS, connect=_CONNECT_TIMEOUT_SECONDS)) as client:
|
|
@@ -14,6 +14,7 @@ from typing import Any
|
|
|
14
14
|
|
|
15
15
|
import dspy
|
|
16
16
|
|
|
17
|
+
from .._runtime_tools import is_runtime_tool
|
|
17
18
|
from . import program, types
|
|
18
19
|
|
|
19
20
|
_PRIM_BY_PYTYPE: dict[Any, types.PrimKind] = {
|
|
@@ -123,9 +124,17 @@ def reflect(m: Any) -> program.Module:
|
|
|
123
124
|
react_cls = _opt("ReAct")
|
|
124
125
|
if react_cls is not None and isinstance(m, react_cls):
|
|
125
126
|
tools = getattr(m, "tools", None) or {}
|
|
126
|
-
# Exclude DSPy
|
|
127
|
-
#
|
|
128
|
-
|
|
127
|
+
# Exclude runtime control tools. DSPy supplies `finish`; a hosted worker may
|
|
128
|
+
# add marked platform tools that are absent from the authored program. Check
|
|
129
|
+
# the explicit marker rather than a name, so a user's similarly named tool
|
|
130
|
+
# remains part of the program's identity.
|
|
131
|
+
names = tuple(
|
|
132
|
+
sorted(
|
|
133
|
+
name
|
|
134
|
+
for name, tool in tools.items()
|
|
135
|
+
if name != "finish" and not (name.startswith("cmpnd_") and is_runtime_tool(tool))
|
|
136
|
+
)
|
|
137
|
+
)
|
|
129
138
|
return program.ReAct(signature=project_sig(m.signature), tools=names)
|
|
130
139
|
|
|
131
140
|
bestofn_cls = _opt("BestOfN")
|
|
@@ -696,7 +696,7 @@ def _registered_tools(url: str, timeout_s: float) -> list[PinnedTool] | None | o
|
|
|
696
696
|
server_url=url,
|
|
697
697
|
tool_name=str(tool.get("name", "")),
|
|
698
698
|
schema_hash=str(tool.get("schema_hash", "")),
|
|
699
|
-
input_schema=tool
|
|
699
|
+
input_schema=registry_schema(tool),
|
|
700
700
|
description=str(tool.get("description", "")),
|
|
701
701
|
)
|
|
702
702
|
for tool in (detail.json() or {}).get("tools") or []
|
|
@@ -704,6 +704,17 @@ def _registered_tools(url: str, timeout_s: float) -> list[PinnedTool] | None | o
|
|
|
704
704
|
]
|
|
705
705
|
|
|
706
706
|
|
|
707
|
+
def registry_schema(tool: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
708
|
+
"""Require the registry's pinned schema before constructing a callable declaration."""
|
|
709
|
+
schema = tool.get("input_schema")
|
|
710
|
+
if not isinstance(schema, dict):
|
|
711
|
+
raise ValueError(
|
|
712
|
+
f"the registry could not supply the pinned schema for tool {tool.get('name')!r}; "
|
|
713
|
+
"restore the tool registry and refresh the server before deploying"
|
|
714
|
+
)
|
|
715
|
+
return schema
|
|
716
|
+
|
|
717
|
+
|
|
707
718
|
def _tool_from_pinned(pinned: PinnedTool, reference: McpToolReference) -> dspy.Tool:
|
|
708
719
|
"""Build the local declaration DSPy needs without a live MCP session."""
|
|
709
720
|
import dspy
|
|
@@ -30,6 +30,8 @@ from typing import TYPE_CHECKING, Any
|
|
|
30
30
|
|
|
31
31
|
import typing_extensions
|
|
32
32
|
|
|
33
|
+
from ._runtime_tools import is_runtime_tool
|
|
34
|
+
|
|
33
35
|
if TYPE_CHECKING:
|
|
34
36
|
import dspy
|
|
35
37
|
|
|
@@ -722,29 +724,11 @@ def _resolve_an_mcp_tool(
|
|
|
722
724
|
|
|
723
725
|
|
|
724
726
|
def _generate_mcp_tool_stub(pinned: typing.Any, signature: typing.Any) -> str:
|
|
725
|
-
"""
|
|
726
|
-
|
|
727
|
-
A declaration plus a signature, and a body that is a refusal. The deployed program
|
|
728
|
-
runs in the kernel, which has no MCP client and must never hold a credential, so the
|
|
729
|
-
worker intercepts the call before this function's body runs. Reaching the body means
|
|
730
|
-
no tool token was dispatched for it, and a body that returned something instead would
|
|
731
|
-
answer the model with a lie.
|
|
732
|
-
|
|
733
|
-
The signature is synthesized rather than `*args, **kwargs` because `dspy.RLM` builds
|
|
734
|
-
each callable stub and each prompt entry from `inspect.signature(tool.func)` — a tool
|
|
735
|
-
the model cannot see a signature for is a tool it cannot call.
|
|
736
|
-
|
|
737
|
-
The docstring's reader is the model, which is why it carries two generated sentences
|
|
738
|
-
after the server's own description. The first says who answers the call. The second
|
|
739
|
-
states the shape the result comes back as, because a deployed call returns the
|
|
740
|
-
protocol's own result object while a LOCAL run through DSPy's conversion flattens it
|
|
741
|
-
to text — so a model told only what the server says would read the dict as a string.
|
|
742
|
-
"""
|
|
727
|
+
"""Generate a callable declaration with the registered tool's pinned signature."""
|
|
743
728
|
body_lines = [f"def {signature.name}({signature.rendered}) -> dict:"]
|
|
744
729
|
body_lines.extend(_docstring_lines(pinned.description))
|
|
745
|
-
body_lines.append(" raise RuntimeError(")
|
|
746
|
-
body_lines.append(f
|
|
747
|
-
body_lines.append(" )")
|
|
730
|
+
body_lines.append(" raise RuntimeError('this MCP tool requires a worker with registered tool support')")
|
|
731
|
+
body_lines.append(f"{signature.name} = __import__('cmpnd')._mcp_tool({signature.name})")
|
|
748
732
|
return "\n".join(body_lines)
|
|
749
733
|
|
|
750
734
|
|
|
@@ -761,7 +745,7 @@ def _docstring_lines(description: str) -> list[str]:
|
|
|
761
745
|
lines.append(f" {line}".rstrip())
|
|
762
746
|
lines.append("")
|
|
763
747
|
lines.append(" Served by an MCP tool server registered to this org. The worker answers this call")
|
|
764
|
-
lines.append("
|
|
748
|
+
lines.append(" through a scoped capability without exposing a credential to the program.")
|
|
765
749
|
lines.append("")
|
|
766
750
|
lines.append(' Returns an MCP tool result: {"content": [{"type": "text", "text": ...}, ...],')
|
|
767
751
|
lines.append(' "isError": bool}. Read the text parts.')
|
|
@@ -796,9 +780,54 @@ def module_tools(module: dspy.Module) -> dict[str, typing.Any]:
|
|
|
796
780
|
return dict(tools) if isinstance(tools, dict) else {}
|
|
797
781
|
|
|
798
782
|
|
|
783
|
+
def declared_module_tools(module: dspy.Module) -> dict[str, typing.Any]:
|
|
784
|
+
"""Collect tools throughout a module and refuse ambiguous worker routing names."""
|
|
785
|
+
import dspy
|
|
786
|
+
|
|
787
|
+
from .mcp_servers import is_bare_mcp_tool, mcp_tool_reference
|
|
788
|
+
|
|
789
|
+
collected: dict[str, typing.Any] = {}
|
|
790
|
+
pending: list[typing.Any] = [module]
|
|
791
|
+
visited: set[int] = set()
|
|
792
|
+
while pending:
|
|
793
|
+
member = pending.pop()
|
|
794
|
+
if id(member) in visited:
|
|
795
|
+
continue
|
|
796
|
+
visited.add(id(member))
|
|
797
|
+
if isinstance(member, dict):
|
|
798
|
+
pending.extend(member.values())
|
|
799
|
+
continue
|
|
800
|
+
if isinstance(member, (list, tuple)):
|
|
801
|
+
pending.extend(member)
|
|
802
|
+
continue
|
|
803
|
+
if not isinstance(member, dspy.Module):
|
|
804
|
+
continue
|
|
805
|
+
pending.extend(vars(member).values())
|
|
806
|
+
for name, tool in module_tools(member).items():
|
|
807
|
+
if name.startswith("cmpnd_") and is_runtime_tool(tool):
|
|
808
|
+
continue
|
|
809
|
+
if name.startswith("cmpnd_"):
|
|
810
|
+
raise ValueError(
|
|
811
|
+
f"tool name {name!r} uses the reserved cmpnd_ prefix; rename the tool before deploying"
|
|
812
|
+
)
|
|
813
|
+
if is_bare_mcp_tool(tool):
|
|
814
|
+
_resolve_an_mcp_tool(name, tool, {})
|
|
815
|
+
previous = collected.get(name)
|
|
816
|
+
if previous is not None and previous is not tool:
|
|
817
|
+
old_ref = mcp_tool_reference(previous)
|
|
818
|
+
new_ref = mcp_tool_reference(tool)
|
|
819
|
+
if old_ref is not None or new_ref is not None:
|
|
820
|
+
if old_ref != new_ref:
|
|
821
|
+
raise ValueError(f"tool name {name!r} refers to different tools in this program")
|
|
822
|
+
collected[name] = tool
|
|
823
|
+
return collected
|
|
824
|
+
|
|
825
|
+
|
|
799
826
|
def generate_program_source(
|
|
800
827
|
module: dspy.Module,
|
|
801
828
|
mcp_tools: Mapping[tuple[str, str], typing.Any] | None = None,
|
|
829
|
+
*,
|
|
830
|
+
_declarations: list[dict[str, typing.Any]] | None = None,
|
|
802
831
|
) -> str:
|
|
803
832
|
"""Return raw Python source for module.
|
|
804
833
|
|
|
@@ -942,10 +971,19 @@ def generate_program_source(
|
|
|
942
971
|
tool_names: list[str] = []
|
|
943
972
|
mcp_declarations: list[dict[str, typing.Any]] = []
|
|
944
973
|
pinned_mcp_tools = mcp_tools or {}
|
|
945
|
-
tools_dict =
|
|
974
|
+
tools_dict = declared_module_tools(module)
|
|
946
975
|
if tools_dict:
|
|
976
|
+
from dspy import Tool
|
|
977
|
+
|
|
947
978
|
from . import mcp_servers
|
|
948
979
|
|
|
980
|
+
mcp_bindings: list[tuple[Any, str]] = []
|
|
981
|
+
|
|
982
|
+
def mcp_tool_expression(route: str, alias: str, *, wrap: bool = False) -> str:
|
|
983
|
+
if wrap or alias != route:
|
|
984
|
+
return f"dspy.Tool({route}, name={alias!r})"
|
|
985
|
+
return route
|
|
986
|
+
|
|
949
987
|
for name, tool in tools_dict.items():
|
|
950
988
|
if name == "finish":
|
|
951
989
|
continue
|
|
@@ -955,23 +993,108 @@ def generate_program_source(
|
|
|
955
993
|
# instead of a body — see [_generate_mcp_tool_stub]. The `def`'s name is
|
|
956
994
|
# the MCP tool's own, because the declaration names the tool and the
|
|
957
995
|
# worker maps that name to the token it holds.
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
996
|
+
reference = mcp_servers.mcp_tool_reference(tool)
|
|
997
|
+
route = next((route for declared, route in mcp_bindings if declared == reference), None)
|
|
998
|
+
if route is None:
|
|
999
|
+
signature = mcp_servers.synthesized_signature(pinned)
|
|
1000
|
+
route = signature.name
|
|
1001
|
+
lines.append(_generate_mcp_tool_stub(pinned, signature))
|
|
1002
|
+
mcp_bindings.append((reference, route))
|
|
1003
|
+
mcp_declarations.append(
|
|
1004
|
+
{
|
|
1005
|
+
"tool": route,
|
|
1006
|
+
"server": pinned.server_url,
|
|
1007
|
+
"schema_hash": pinned.schema_hash,
|
|
1008
|
+
"parameters": signature.parameter_names,
|
|
1009
|
+
}
|
|
1010
|
+
)
|
|
1011
|
+
tool_names.append(mcp_tool_expression(route, name))
|
|
969
1012
|
continue
|
|
970
1013
|
func = getattr(tool, "func", None)
|
|
971
1014
|
if func is not None:
|
|
972
1015
|
_extract_callable_tree(func)
|
|
973
1016
|
tool_names.append(name)
|
|
974
1017
|
|
|
1018
|
+
# A composed constructor refers to its Python bindings, which need not share
|
|
1019
|
+
# the registered routing name. Rebuild those bindings from pinned declarations
|
|
1020
|
+
# without carrying the live MCP session or credential into generated source.
|
|
1021
|
+
if is_user_module:
|
|
1022
|
+
bindings: dict[str, str] = {}
|
|
1023
|
+
|
|
1024
|
+
def function_binding(func: Any) -> str:
|
|
1025
|
+
if not inspect.isfunction(func) or not func.__name__.isidentifier():
|
|
1026
|
+
raise ValueError("Cannot reconstruct this tool callable; deploy the authored source file")
|
|
1027
|
+
_extract_callable_tree(func)
|
|
1028
|
+
if func.__name__ not in emitted_sources:
|
|
1029
|
+
raise ValueError("Cannot recover tool source; deploy the authored source file")
|
|
1030
|
+
return func.__name__
|
|
1031
|
+
|
|
1032
|
+
def contains_mcp(value: Any, seen: set[int]) -> bool:
|
|
1033
|
+
if id(value) in seen:
|
|
1034
|
+
return False
|
|
1035
|
+
seen.add(id(value))
|
|
1036
|
+
if mcp_servers.mcp_tool_reference(value) is not None:
|
|
1037
|
+
return True
|
|
1038
|
+
members = value.values() if isinstance(value, dict) else value
|
|
1039
|
+
return isinstance(value, (list, tuple, dict)) and any(contains_mcp(item, seen) for item in members)
|
|
1040
|
+
|
|
1041
|
+
def binding_source(value: Any, ancestors: frozenset[int]) -> str:
|
|
1042
|
+
if id(value) in ancestors:
|
|
1043
|
+
raise ValueError("Cannot reconstruct cyclic MCP tool bindings; deploy the authored source file")
|
|
1044
|
+
ancestors = ancestors | {id(value)}
|
|
1045
|
+
reference = mcp_servers.mcp_tool_reference(value)
|
|
1046
|
+
if reference is not None:
|
|
1047
|
+
for declared, route in mcp_bindings:
|
|
1048
|
+
if reference == declared:
|
|
1049
|
+
alias = str(getattr(value, "name", route))
|
|
1050
|
+
return mcp_tool_expression(route, alias, wrap=True)
|
|
1051
|
+
raise ValueError(
|
|
1052
|
+
"MCP tool binding is not declared by this program; deploy the authored source file"
|
|
1053
|
+
)
|
|
1054
|
+
if isinstance(value, list):
|
|
1055
|
+
return "[" + ", ".join(binding_source(item, ancestors) for item in value) + "]"
|
|
1056
|
+
if isinstance(value, tuple):
|
|
1057
|
+
return "(" + "".join(binding_source(item, ancestors) + ", " for item in value) + ")"
|
|
1058
|
+
if isinstance(value, dict):
|
|
1059
|
+
if not all(isinstance(key, str) for key in value):
|
|
1060
|
+
raise ValueError("MCP tool dictionaries require string keys; deploy the authored source file")
|
|
1061
|
+
return (
|
|
1062
|
+
"{"
|
|
1063
|
+
+ ", ".join(f"{key!r}: {binding_source(item, ancestors)}" for key, item in value.items())
|
|
1064
|
+
+ "}"
|
|
1065
|
+
)
|
|
1066
|
+
if isinstance(value, Tool):
|
|
1067
|
+
name = function_binding(value.func)
|
|
1068
|
+
if not _is_literal_constant(value.args):
|
|
1069
|
+
raise ValueError("Cannot reconstruct tool arguments; deploy the authored source file")
|
|
1070
|
+
return f"dspy.Tool({name}, name={value.name!r}, desc={value.desc!r}, args={value.args!r})"
|
|
1071
|
+
if callable(value):
|
|
1072
|
+
return function_binding(value)
|
|
1073
|
+
if _is_literal_constant(value):
|
|
1074
|
+
return repr(value)
|
|
1075
|
+
raise ValueError("Cannot reconstruct a binding containing MCP tools; deploy the authored source file")
|
|
1076
|
+
|
|
1077
|
+
for method_name in ("__init__", "forward"):
|
|
1078
|
+
method = getattr(cls, method_name, None)
|
|
1079
|
+
if not inspect.isfunction(method):
|
|
1080
|
+
continue
|
|
1081
|
+
closure = inspect.getclosurevars(method)
|
|
1082
|
+
for name, value in {**closure.globals, **closure.nonlocals}.items():
|
|
1083
|
+
if not contains_mcp(value, set()):
|
|
1084
|
+
continue
|
|
1085
|
+
rendered = binding_source(value, frozenset())
|
|
1086
|
+
if name in bindings and bindings[name] != rendered:
|
|
1087
|
+
raise ValueError(
|
|
1088
|
+
f"MCP tool binding {name!r} has conflicting values; deploy the authored source file"
|
|
1089
|
+
)
|
|
1090
|
+
bindings[name] = rendered
|
|
1091
|
+
if bindings:
|
|
1092
|
+
# Evaluate every declaration before rebinding any name: an author's
|
|
1093
|
+
# alias can itself be another registered tool's routing name.
|
|
1094
|
+
names = ", ".join(bindings)
|
|
1095
|
+
values = ", ".join(bindings.values())
|
|
1096
|
+
lines.append(f"({names},) = ({values},)")
|
|
1097
|
+
|
|
975
1098
|
# Reconstruct the synthetic ProgramSignature class up-front (when that
|
|
976
1099
|
# path applies) so its annotation imports land in sig_imports *before*
|
|
977
1100
|
# we emit the import block below. The class text itself is appended
|
|
@@ -1009,6 +1132,9 @@ def generate_program_source(
|
|
|
1009
1132
|
extra_kwargs = ""
|
|
1010
1133
|
if tool_names:
|
|
1011
1134
|
extra_kwargs = f", tools=[{', '.join(tool_names)}]"
|
|
1135
|
+
max_iters = getattr(module, "max_iters", None)
|
|
1136
|
+
if not is_user_module and isinstance(max_iters, int):
|
|
1137
|
+
extra_kwargs += f", max_iters={max_iters!r}"
|
|
1012
1138
|
|
|
1013
1139
|
module_assigned = False
|
|
1014
1140
|
if sig is not None:
|
|
@@ -1073,6 +1199,8 @@ def generate_program_source(
|
|
|
1073
1199
|
# decide whether an empty list means "none" or "not resolved".
|
|
1074
1200
|
if mcp_declarations:
|
|
1075
1201
|
lines.append(f"{MCP_TOOLS_DECLARATION} = {mcp_declarations!r}")
|
|
1202
|
+
if _declarations is not None:
|
|
1203
|
+
_declarations.extend(mcp_declarations)
|
|
1076
1204
|
|
|
1077
1205
|
return "\n\n".join(lines)
|
|
1078
1206
|
|
|
@@ -1241,7 +1369,12 @@ def generate_source_zip(
|
|
|
1241
1369
|
"""
|
|
1242
1370
|
import dspy
|
|
1243
1371
|
|
|
1244
|
-
|
|
1372
|
+
tool_declarations: list[dict[str, typing.Any]] = []
|
|
1373
|
+
program_source = generate_program_source(
|
|
1374
|
+
module,
|
|
1375
|
+
mcp_tools=mcp_tools,
|
|
1376
|
+
_declarations=tool_declarations,
|
|
1377
|
+
)
|
|
1245
1378
|
|
|
1246
1379
|
metric_filename = None
|
|
1247
1380
|
metric_source = None
|
|
@@ -1254,7 +1387,7 @@ def generate_source_zip(
|
|
|
1254
1387
|
|
|
1255
1388
|
lm = _extract_lm_object(module)
|
|
1256
1389
|
declarations = lm_configuration.resolve({"default-lm": lm} if lm is not None else {}, require_all=False)
|
|
1257
|
-
manifest = {
|
|
1390
|
+
manifest: dict[str, typing.Any] = {
|
|
1258
1391
|
"lm_capabilities": declarations,
|
|
1259
1392
|
"dependency_versions": {
|
|
1260
1393
|
"python": platform.python_version(),
|
|
@@ -1263,6 +1396,8 @@ def generate_source_zip(
|
|
|
1263
1396
|
"program": "program_source.py",
|
|
1264
1397
|
"metric": metric_filename,
|
|
1265
1398
|
}
|
|
1399
|
+
if tool_declarations:
|
|
1400
|
+
manifest["tools"] = tool_declarations
|
|
1266
1401
|
|
|
1267
1402
|
buf = io.BytesIO()
|
|
1268
1403
|
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
|
|
@@ -282,12 +282,14 @@ immediately, because the credential goes with it. The confirmation says so.
|
|
|
282
282
|
A program built with `cmpnd.mcp_tools` deploys against exactly this registry.
|
|
283
283
|
`cmpnd deploy` resolves each of the program's tool references against it before
|
|
284
284
|
packaging anything — one listing read plus one read per server, and none at all
|
|
285
|
-
for a program with no MCP tools — so the bundle
|
|
286
|
-
|
|
287
|
-
registry
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
285
|
+
for a program with no MCP tools — so the bundle records declarations built from the
|
|
286
|
+
current reduced schema and its hash. Serve checks that hash against the current
|
|
287
|
+
registry at deploy time. mcp-tools validates each call against the current stored
|
|
288
|
+
schema, so a refresh can change call validation after a program was built. An
|
|
289
|
+
unregistered server and a tool the registry does not list are both refused at the
|
|
290
|
+
terminal, naming the tool, the server and the command that fixes it; the platform
|
|
291
|
+
refuses the same two authoritatively, plus a tool that is registered but not
|
|
292
|
+
allow-listed and a schema that has changed since the program was built. See
|
|
291
293
|
[`sdk-instrumentation.md`](sdk-instrumentation.md) for what the bundle carries in
|
|
292
294
|
place of a tool's body.
|
|
293
295
|
|
|
@@ -401,7 +401,8 @@ def web_search_exa(query: str, numResults: float | None = None) -> dict:
|
|
|
401
401
|
|
|
402
402
|
|
|
403
403
|
__cmpnd_mcp_tools__ = [{'tool': 'web_search_exa', 'server': 'https://mcp.exa.ai/mcp',
|
|
404
|
-
'schema_hash': '…',
|
|
404
|
+
'schema_hash': '…',
|
|
405
|
+
'parameters': ['query', 'numResults']}]
|
|
405
406
|
```
|
|
406
407
|
|
|
407
408
|
The body is a refusal because the deployed program runs in the kernel, which has no MCP
|
|
@@ -425,6 +426,13 @@ annotation at all for a type name the table does not carry. A property name that
|
|
|
425
426
|
Python identifier, or is a keyword, refuses the deploy — it is never mangled, because the
|
|
426
427
|
name has to reach the MCP server unchanged.
|
|
427
428
|
|
|
429
|
+
The SDK uses the registry's pinned, secret-free `input_schema` to construct the generated
|
|
430
|
+
function signature. The source marker and generated bundle manifest retain `schema_hash`
|
|
431
|
+
and `parameters`. Serve checks the hash against the registry at deploy time, and the
|
|
432
|
+
worker uses the parameters for positional binding. Runtime schema inspection reads the
|
|
433
|
+
current stored schema through credentialed-egress and mcp-tools, so it does not rely on a
|
|
434
|
+
schema copy in the program bundle.
|
|
435
|
+
|
|
428
436
|
### Direct local MCP discovery supports two client generations
|
|
429
437
|
|
|
430
438
|
When registry lookup falls back to a direct local connection, the SDK lists tools
|
|
@@ -605,3 +613,26 @@ A new hand-rolled serializer on any of those paths reopens the hole. `tests/test
|
|
|
605
613
|
- [`./optimization-tracking.md`](../../docs/optimization-tracking.md) — how `auto_instrument`'s GEPA patch and `cmpnd/optimizers/tracker.py` turn a `GEPA.compile` call into an `OptimizationRun` with linked evals.
|
|
606
614
|
- The `cmpnd-testing` skill + [`../AGENTS.md`](../AGENTS.md) § "Running Tests & Checks" — patterns for writing callback tests against real databases, including `clear_context()` usage.
|
|
607
615
|
- [`serve/DB.md`](../../serve/DB.md) — the Postgres schema these payloads land in.
|
|
616
|
+
|
|
617
|
+
### Registered MCP tools in ReAct
|
|
618
|
+
|
|
619
|
+
`cmpnd.mcp_tools` declares registered tools for ReAct and RLM, including modules
|
|
620
|
+
that contain them. Deployment resolves those declarations against the organization's
|
|
621
|
+
registry and pins their schemas. Bare MCP session closures cannot be packaged.
|
|
622
|
+
Conflicting tools with the same routing name are refused.
|
|
623
|
+
Generated composed programs retain global and closure aliases for registered tools,
|
|
624
|
+
including aliases stored in lists, tuples, or dictionaries. The packager rebuilds
|
|
625
|
+
these bindings from pinned declarations without copying live MCP sessions.
|
|
626
|
+
Generated mixed-tool bindings require ordinary tools to be named Python functions.
|
|
627
|
+
Use authored-source deployment for partials, callable instances, lambdas, or bound methods.
|
|
628
|
+
|
|
629
|
+
The WASM kernel calls tools through a typed worker import. The worker holds the
|
|
630
|
+
scoped tokens and returns the MCP result document. Optional arguments omitted by
|
|
631
|
+
the program remain omitted. ReAct's tool callbacks produce the application span;
|
|
632
|
+
the worker adds transport measurements to that span using its correlation ID.
|
|
633
|
+
|
|
634
|
+
The worker reserves the `cmpnd_` prefix for platform tools. Deployment refuses an
|
|
635
|
+
authored tool that uses this prefix and asks the author to rename it. Platform tools
|
|
636
|
+
that the worker adds while a program runs carry a private, copy-stable provenance token.
|
|
637
|
+
Only tools carrying that token stay out of the program's identity and generated bundle.
|
|
638
|
+
Setting a similarly named attribute does not grant platform provenance.
|
|
@@ -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.27.
|
|
8
|
+
version = "0.27.4"
|
|
9
9
|
description = "DSPy observability and deployment SDK for cmpnd"
|
|
10
10
|
readme = "README.md"
|
|
11
11
|
requires-python = ">=3.10"
|
|
@@ -9,6 +9,8 @@ mismatch a mocked transport cannot. Skipped unless CMPND_E2E_API_KEY is set
|
|
|
9
9
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
|
+
import time
|
|
13
|
+
|
|
12
14
|
import dspy
|
|
13
15
|
import pytest
|
|
14
16
|
|
|
@@ -26,16 +28,28 @@ class TestTracesReadE2E:
|
|
|
26
28
|
assert trace_id is not None, "no trace captured"
|
|
27
29
|
program_hash = server_client.get_trace(trace_id)["trace"]["program_signature_hash"]
|
|
28
30
|
|
|
29
|
-
#
|
|
30
|
-
page = cmpnd.traces.list(hash=str(program_hash), limit=50, api_key=CMPND_E2E_API_KEY, endpoint=E2E_ENDPOINT)
|
|
31
|
-
assert trace_id in [t["trace_id"] for t in page["traces"]]
|
|
32
|
-
|
|
33
|
-
# show returns the trace and its spans.
|
|
31
|
+
# Detail reads the ingested source without waiting for collection projection.
|
|
34
32
|
detail = cmpnd.traces.show(trace_id, api_key=CMPND_E2E_API_KEY, endpoint=E2E_ENDPOINT)
|
|
35
33
|
assert detail["trace"]["trace_id"] == trace_id
|
|
36
34
|
assert isinstance(detail["spans"], list)
|
|
37
35
|
assert len(detail["spans"]) >= 1
|
|
38
36
|
|
|
37
|
+
# Collection publication is asynchronous. Allow its 30-second idle
|
|
38
|
+
# cooldown plus processing time, polling the same trace rather than
|
|
39
|
+
# rerunning the program and restarting the freshness window.
|
|
40
|
+
deadline = time.monotonic() + 90
|
|
41
|
+
while True:
|
|
42
|
+
page = cmpnd.traces.list(hash=str(program_hash), limit=50, api_key=CMPND_E2E_API_KEY, endpoint=E2E_ENDPOINT)
|
|
43
|
+
listed_ids = [t["trace_id"] for t in page["traces"]]
|
|
44
|
+
if trace_id in listed_ids:
|
|
45
|
+
break
|
|
46
|
+
remaining = deadline - time.monotonic()
|
|
47
|
+
assert remaining > 0, (
|
|
48
|
+
f"Trace {trace_id} is readable in detail but absent from its program's collection after 90s; "
|
|
49
|
+
f"program={program_hash}, last listed trace IDs={listed_ids}"
|
|
50
|
+
)
|
|
51
|
+
time.sleep(min(2, remaining))
|
|
52
|
+
|
|
39
53
|
def test_show_missing_trace_raises(self):
|
|
40
54
|
with pytest.raises(cmpnd.ApiError):
|
|
41
55
|
cmpnd.traces.show("00000000-0000-0000-0000-000000000000", api_key=CMPND_E2E_API_KEY, endpoint=E2E_ENDPOINT)
|