cmpnd 0.27.2__tar.gz → 0.27.3__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.3}/PKG-INFO +1 -1
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/__init__.py +5 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/_authored_import.py +0 -1
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/deployment.py +3 -47
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/mcp_servers.py +12 -1
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/packaging.py +128 -23
- {cmpnd-0.27.2 → cmpnd-0.27.3}/docs/sdk-instrumentation.md +17 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/pyproject.toml +1 -1
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/test_traces_read.py +19 -5
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/integration/hosted_mcp_tool_optimize_probe.py +66 -8
- cmpnd-0.27.3/tests/stress/_readiness.py +139 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/stress/test_optimize_loop_wasm.py +53 -25
- cmpnd-0.27.3/tests/stress/test_readiness.py +301 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/stress/test_trace_search.py +25 -10
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_deploy_mcp_tool_references.py +53 -25
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_packaging_mcp_tools.py +100 -1
- {cmpnd-0.27.2 → cmpnd-0.27.3}/uv.lock +1 -1
- {cmpnd-0.27.2 → cmpnd-0.27.3}/.gitignore +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/AGENTS.md +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/CLAUDE.md +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/README.md +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/_assessments.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/_exporter_pool.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/_gepa_patch.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/_http.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/_program_patch.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/_rlm_patch.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/_timespec.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/_traces.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/_valsets.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/authored.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/callback.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/cli/__init__.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/cli/api_client.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/cli/commands/__init__.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/cli/commands/admin.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/cli/commands/assessments.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/cli/commands/auth.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/cli/commands/datasets.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/cli/commands/deployments.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/cli/commands/evals.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/cli/commands/login.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/cli/commands/optimizations.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/cli/commands/org.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/cli/commands/programs.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/cli/commands/provider_credentials.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/cli/commands/signatures.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/cli/commands/tool_servers.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/cli/commands/top.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/cli/commands/traces.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/cli/commands/valsets.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/cli/credentials.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/cli/deprecation.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/cli/pagination.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/cli/shell.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/cli/trace_render.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/cli/tree_help.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/cli/verbs.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/client.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/configuration.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/context.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/dataset_sync.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/datasets.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/decorators.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/eval_handler.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/execution.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/exporter.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/exporter_http.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/helpers.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/identity.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/imaging.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/ir/__init__.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/ir/decode.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/ir/encode.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/ir/hash.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/ir/program.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/ir/reflection.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/ir/types.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/ir/xxh64.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/lm_configuration.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/models.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/optimization.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/optimizers/__init__.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/optimizers/gepa.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/optimizers/gepa_callback.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/optimizers/resume.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/optimizers/tracker.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/retry.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/cmpnd/traces.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/docs/2026-04-02-code-review.md +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/docs/cli.md +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/docs/client.md +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/docs/custom-proposer.md +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/docs/plans/2026-04-05-train-val-dataset-capture-design.md +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/docs/plans/2026-04-05-train-val-dataset-capture.md +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/docs/reading-traces.md +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/examples/generalizing_proposer.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/examples/local_ollama.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/ingest/LINEAGE.md +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/ingest/README.md +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/ingest/cmpnd_ingest/__init__.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/ingest/cmpnd_ingest/bodies.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/ingest/cmpnd_ingest/exporter_http.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/ingest/cmpnd_ingest/protocols.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/ingest/cmpnd_ingest/py.typed +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/ingest/cmpnd_ingest/request_log.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/ingest/cmpnd_ingest/retry.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/ingest/pyproject.toml +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/scripts/audit-sdist.sh +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/scripts/capture_review_run.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/scripts/check_release_artifacts.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/scripts/fixtures/review_run.json.gz +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/scripts/gepa_resume_bug.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/scripts/release-stress.sh +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/scripts/require_version_bump.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/scripts/seed_review_run.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/scripts/test-in-linux.sh +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/stubs/dspy/__init__.pyi +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/stubs/dspy/primitives/__init__.pyi +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/stubs/dspy/primitives/prediction.pyi +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/stubs/dspy/utils/__init__.pyi +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/stubs/dspy/utils/callback.pyi +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/__init__.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/_exporter_helpers.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/_fake_clock.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/_identity_helpers.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/conftest.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/__init__.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/committee_task.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/conftest.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/data/committee_train.ndjson.gz +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/data/committee_val.ndjson.gz +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/fake_lm.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/helpers.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/target_capabilities.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/test_assessments_cli.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/test_capture_prompts.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/test_composite_module.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/test_dataset_auto_creation.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/test_deploy_execute.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/test_deploy_programs.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/test_edge_cases.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/test_example_hash.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/test_gepa_optimization.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/test_interpreter_spans.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/test_lm_accuracy.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/test_module_tracing.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/test_optimization_lineage.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/test_optimization_metric_identity.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/test_optimize_failures.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/test_optimize_loop.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/test_optimize_review_committee.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/test_optimize_server_owned_completion.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/test_parallel_export.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/test_project_attribution.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/test_react_tool_spans.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/test_save_load.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/test_trace_structure.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/test_user_attribution.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/e2e/test_valsets_cli.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/identity_vectors/01_predict_qa.json +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/identity_vectors/02_predict_renamed_field.json +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/identity_vectors/03_chain_of_thought_qa.json +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/identity_vectors/04_predict_richer_types.json +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/identity_vectors/05_composite_two_predicts.json +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/identity_vectors/06_multichaincomparison.json +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/identity_vectors/07_program_of_thought.json +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/identity_vectors/08_opaque_module.json +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/identity_vectors/09_predict_pydantic_field.json +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/identity_vectors/10_react.json +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/identity_vectors/11_refine.json +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/identity_vectors/12_retrieve.json +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/identity_vectors/13_knn.json +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/identity_vectors/14_parallel.json +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/identity_vectors/README.md +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/ingest_conformance.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/integration/__init__.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/integration/authored_optimize/judge.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/integration/authored_optimize/optimize_hosted.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/integration/conftest.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/integration/hosted_optimize_probe.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/integration/optimization_run_probe.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/integration/test_cli_deploy_smoketest.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/integration/test_logs_renders_traces.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/integration/test_optimization_run.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/integration/test_optimizing_progress.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/integration/test_unhappy_path.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/load/__init__.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/load/__main__.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/load/drain.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/load/ingest.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/load/latency_transport.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/load/rlm_shape.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/load/sweep.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/load/test_rlm_shape.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/parity/__init__.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/parity/conftest.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/parity/seed_parity.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/parity/test_auth.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/parity/test_datasets.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/parity/test_deployments.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/parity/test_evals.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/parity/test_health.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/parity/test_modules.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/parity/test_optimizations.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/parity/test_programs.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/parity/test_signatures.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/parity/test_trace_streaming.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/parity/test_traces.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/stress/__init__.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/stress/_provider.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/stress/_reliability.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/stress/_result.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/stress/mint_bedrock_token.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/stress/test_deploy_programs.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/stress/test_multi_client_stress.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/stress/test_optimize_client_committee.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/stress/test_optimize_loop.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/stress/test_optimize_stress.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/stress/test_provider_roles.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/stress/test_real_provider_twins.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/stress/test_reliability.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/stress/test_reliability_classify.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/stress/test_rlm_wasm.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_assessments_api.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_authored.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_authored_bundle_collisions.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_authored_metric_contract.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_authored_named_models.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_authored_optimize.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_callback.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_candidate_lineage.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_cli/__init__.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_cli/conftest.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_cli/test_admin.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_cli/test_api_client.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_cli/test_assessments.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_cli/test_auth.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_cli/test_credentials.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_cli/test_datasets.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_cli/test_deployments.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_cli/test_evals.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_cli/test_login.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_cli/test_main.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_cli/test_optimizations.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_cli/test_org.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_cli/test_pagination.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_cli/test_parity.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_cli/test_programs.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_cli/test_provider_credentials.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_cli/test_schema_sample.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_cli/test_shell.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_cli/test_signatures.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_cli/test_tool_servers.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_cli/test_top.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_cli/test_traces.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_cli/test_traces_search_wire.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_cli/test_tree_help.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_cli/test_valsets.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_client.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_clustering/__init__.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_clustering/conftest.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_clustering/test_edge_cases.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_clustering/test_hash_consistency.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_clustering/test_type_conversion.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_cmp551_grouping_identity.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_configuration.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_context.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_context_propagation.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_dataset_polling.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_dataset_sync.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_datasets.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_decorators.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_deploy.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_eval.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_eval_example_attribution.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_eval_start.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_eval_trace_grouping.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_exception_paths.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_execute.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_exporter.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_exporter_queue.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_exporter_real.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_fake_lm.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_gepa_callback.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_gepa_integration.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_gepa_resume.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_identity.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_imaging.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_import_weight.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_ingest_conformance.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_ingest_second_producer.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_integration.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_interpreter_callback.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_ir_encoder_properties.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_ir_golden_vectors.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_ir_roundtrip.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_lm_configuration.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_lm_usage_shapes.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_mcp_tools.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_models.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_optimization_progress.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_optimization_start.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_optimize.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_optimize_budget.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_optimize_lift.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_optimize_lms.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_optimize_proposer.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_optimize_reflection_lm.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_optimize_terminal_failure.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_packaging.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_packaging_callable_source.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_packaging_program_source.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_poll_until.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_program_lineage_persist.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_project_payload.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_reflector_properties.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_reflector_smoke.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_release_stress_script.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_repro_reactv2_bugs.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_require_version_bump.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_retry.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_reversible_failures.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_rlm_patch.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_schemas.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_seed_review_run.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_span_sent_once.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_trace_render.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_traces_read.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_tracked_gepa.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_tracker_enrich.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_truncation.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_valsets_api.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/tests/test_xxh64.py +0 -0
- {cmpnd-0.27.2 → cmpnd-0.27.3}/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
|
|
@@ -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:
|
|
@@ -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
|
|
@@ -722,29 +722,11 @@ def _resolve_an_mcp_tool(
|
|
|
722
722
|
|
|
723
723
|
|
|
724
724
|
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
|
-
"""
|
|
725
|
+
"""Generate a callable declaration with the registered tool's pinned signature."""
|
|
743
726
|
body_lines = [f"def {signature.name}({signature.rendered}) -> dict:"]
|
|
744
727
|
body_lines.extend(_docstring_lines(pinned.description))
|
|
745
|
-
body_lines.append(" raise RuntimeError(")
|
|
746
|
-
body_lines.append(f
|
|
747
|
-
body_lines.append(" )")
|
|
728
|
+
body_lines.append(" raise RuntimeError('this MCP tool requires a worker with registered tool support')")
|
|
729
|
+
body_lines.append(f"{signature.name} = __import__('cmpnd')._mcp_tool({signature.name})")
|
|
748
730
|
return "\n".join(body_lines)
|
|
749
731
|
|
|
750
732
|
|
|
@@ -761,7 +743,7 @@ def _docstring_lines(description: str) -> list[str]:
|
|
|
761
743
|
lines.append(f" {line}".rstrip())
|
|
762
744
|
lines.append("")
|
|
763
745
|
lines.append(" Served by an MCP tool server registered to this org. The worker answers this call")
|
|
764
|
-
lines.append("
|
|
746
|
+
lines.append(" through a scoped capability without exposing a credential to the program.")
|
|
765
747
|
lines.append("")
|
|
766
748
|
lines.append(' Returns an MCP tool result: {"content": [{"type": "text", "text": ...}, ...],')
|
|
767
749
|
lines.append(' "isError": bool}. Read the text parts.')
|
|
@@ -796,6 +778,43 @@ def module_tools(module: dspy.Module) -> dict[str, typing.Any]:
|
|
|
796
778
|
return dict(tools) if isinstance(tools, dict) else {}
|
|
797
779
|
|
|
798
780
|
|
|
781
|
+
def declared_module_tools(module: dspy.Module) -> dict[str, typing.Any]:
|
|
782
|
+
"""Collect tools throughout a module and refuse ambiguous worker routing names."""
|
|
783
|
+
import dspy
|
|
784
|
+
|
|
785
|
+
from .mcp_servers import is_bare_mcp_tool, mcp_tool_reference
|
|
786
|
+
|
|
787
|
+
collected: dict[str, typing.Any] = {}
|
|
788
|
+
pending: list[typing.Any] = [module]
|
|
789
|
+
visited: set[int] = set()
|
|
790
|
+
while pending:
|
|
791
|
+
member = pending.pop()
|
|
792
|
+
if id(member) in visited:
|
|
793
|
+
continue
|
|
794
|
+
visited.add(id(member))
|
|
795
|
+
if isinstance(member, dict):
|
|
796
|
+
pending.extend(member.values())
|
|
797
|
+
continue
|
|
798
|
+
if isinstance(member, (list, tuple)):
|
|
799
|
+
pending.extend(member)
|
|
800
|
+
continue
|
|
801
|
+
if not isinstance(member, dspy.Module):
|
|
802
|
+
continue
|
|
803
|
+
pending.extend(vars(member).values())
|
|
804
|
+
for name, tool in module_tools(member).items():
|
|
805
|
+
if is_bare_mcp_tool(tool):
|
|
806
|
+
_resolve_an_mcp_tool(name, tool, {})
|
|
807
|
+
previous = collected.get(name)
|
|
808
|
+
if previous is not None and previous is not tool:
|
|
809
|
+
old_ref = mcp_tool_reference(previous)
|
|
810
|
+
new_ref = mcp_tool_reference(tool)
|
|
811
|
+
if old_ref is not None or new_ref is not None:
|
|
812
|
+
if old_ref != new_ref:
|
|
813
|
+
raise ValueError(f"tool name {name!r} refers to different tools in this program")
|
|
814
|
+
collected[name] = tool
|
|
815
|
+
return collected
|
|
816
|
+
|
|
817
|
+
|
|
799
818
|
def generate_program_source(
|
|
800
819
|
module: dspy.Module,
|
|
801
820
|
mcp_tools: Mapping[tuple[str, str], typing.Any] | None = None,
|
|
@@ -942,10 +961,13 @@ def generate_program_source(
|
|
|
942
961
|
tool_names: list[str] = []
|
|
943
962
|
mcp_declarations: list[dict[str, typing.Any]] = []
|
|
944
963
|
pinned_mcp_tools = mcp_tools or {}
|
|
945
|
-
tools_dict =
|
|
964
|
+
tools_dict = declared_module_tools(module)
|
|
946
965
|
if tools_dict:
|
|
966
|
+
from dspy import Tool
|
|
967
|
+
|
|
947
968
|
from . import mcp_servers
|
|
948
969
|
|
|
970
|
+
mcp_bindings: list[tuple[Any, str]] = []
|
|
949
971
|
for name, tool in tools_dict.items():
|
|
950
972
|
if name == "finish":
|
|
951
973
|
continue
|
|
@@ -957,6 +979,7 @@ def generate_program_source(
|
|
|
957
979
|
# worker maps that name to the token it holds.
|
|
958
980
|
signature = mcp_servers.synthesized_signature(pinned)
|
|
959
981
|
lines.append(_generate_mcp_tool_stub(pinned, signature))
|
|
982
|
+
mcp_bindings.append((mcp_servers.mcp_tool_reference(tool), f"dspy.Tool({signature.name})"))
|
|
960
983
|
mcp_declarations.append(
|
|
961
984
|
{
|
|
962
985
|
"tool": signature.name,
|
|
@@ -972,6 +995,85 @@ def generate_program_source(
|
|
|
972
995
|
_extract_callable_tree(func)
|
|
973
996
|
tool_names.append(name)
|
|
974
997
|
|
|
998
|
+
# A composed constructor refers to its Python bindings, which need not share
|
|
999
|
+
# the registered routing name. Rebuild those bindings from pinned declarations
|
|
1000
|
+
# without carrying the live MCP session or credential into generated source.
|
|
1001
|
+
if is_user_module:
|
|
1002
|
+
bindings: dict[str, str] = {}
|
|
1003
|
+
|
|
1004
|
+
def function_binding(func: Any) -> str:
|
|
1005
|
+
if not inspect.isfunction(func) or not func.__name__.isidentifier():
|
|
1006
|
+
raise ValueError("Cannot reconstruct this tool callable; deploy the authored source file")
|
|
1007
|
+
_extract_callable_tree(func)
|
|
1008
|
+
if func.__name__ not in emitted_sources:
|
|
1009
|
+
raise ValueError("Cannot recover tool source; deploy the authored source file")
|
|
1010
|
+
return func.__name__
|
|
1011
|
+
|
|
1012
|
+
def contains_mcp(value: Any, seen: set[int]) -> bool:
|
|
1013
|
+
if id(value) in seen:
|
|
1014
|
+
return False
|
|
1015
|
+
seen.add(id(value))
|
|
1016
|
+
if mcp_servers.mcp_tool_reference(value) is not None:
|
|
1017
|
+
return True
|
|
1018
|
+
members = value.values() if isinstance(value, dict) else value
|
|
1019
|
+
return isinstance(value, (list, tuple, dict)) and any(contains_mcp(item, seen) for item in members)
|
|
1020
|
+
|
|
1021
|
+
def binding_source(value: Any, ancestors: frozenset[int]) -> str:
|
|
1022
|
+
if id(value) in ancestors:
|
|
1023
|
+
raise ValueError("Cannot reconstruct cyclic MCP tool bindings; deploy the authored source file")
|
|
1024
|
+
ancestors = ancestors | {id(value)}
|
|
1025
|
+
reference = mcp_servers.mcp_tool_reference(value)
|
|
1026
|
+
if reference is not None:
|
|
1027
|
+
for declared, source in mcp_bindings:
|
|
1028
|
+
if reference == declared:
|
|
1029
|
+
return source
|
|
1030
|
+
raise ValueError(
|
|
1031
|
+
"MCP tool binding is not declared by this program; deploy the authored source file"
|
|
1032
|
+
)
|
|
1033
|
+
if isinstance(value, list):
|
|
1034
|
+
return "[" + ", ".join(binding_source(item, ancestors) for item in value) + "]"
|
|
1035
|
+
if isinstance(value, tuple):
|
|
1036
|
+
return "(" + "".join(binding_source(item, ancestors) + ", " for item in value) + ")"
|
|
1037
|
+
if isinstance(value, dict):
|
|
1038
|
+
if not all(isinstance(key, str) for key in value):
|
|
1039
|
+
raise ValueError("MCP tool dictionaries require string keys; deploy the authored source file")
|
|
1040
|
+
return (
|
|
1041
|
+
"{"
|
|
1042
|
+
+ ", ".join(f"{key!r}: {binding_source(item, ancestors)}" for key, item in value.items())
|
|
1043
|
+
+ "}"
|
|
1044
|
+
)
|
|
1045
|
+
if isinstance(value, Tool):
|
|
1046
|
+
name = function_binding(value.func)
|
|
1047
|
+
if not _is_literal_constant(value.args):
|
|
1048
|
+
raise ValueError("Cannot reconstruct tool arguments; deploy the authored source file")
|
|
1049
|
+
return f"dspy.Tool({name}, name={value.name!r}, desc={value.desc!r}, args={value.args!r})"
|
|
1050
|
+
if callable(value):
|
|
1051
|
+
return function_binding(value)
|
|
1052
|
+
if _is_literal_constant(value):
|
|
1053
|
+
return repr(value)
|
|
1054
|
+
raise ValueError("Cannot reconstruct a binding containing MCP tools; deploy the authored source file")
|
|
1055
|
+
|
|
1056
|
+
for method_name in ("__init__", "forward"):
|
|
1057
|
+
method = getattr(cls, method_name, None)
|
|
1058
|
+
if not inspect.isfunction(method):
|
|
1059
|
+
continue
|
|
1060
|
+
closure = inspect.getclosurevars(method)
|
|
1061
|
+
for name, value in {**closure.globals, **closure.nonlocals}.items():
|
|
1062
|
+
if not contains_mcp(value, set()):
|
|
1063
|
+
continue
|
|
1064
|
+
rendered = binding_source(value, frozenset())
|
|
1065
|
+
if name in bindings and bindings[name] != rendered:
|
|
1066
|
+
raise ValueError(
|
|
1067
|
+
f"MCP tool binding {name!r} has conflicting values; deploy the authored source file"
|
|
1068
|
+
)
|
|
1069
|
+
bindings[name] = rendered
|
|
1070
|
+
if bindings:
|
|
1071
|
+
# Evaluate every declaration before rebinding any name: an author's
|
|
1072
|
+
# alias can itself be another registered tool's routing name.
|
|
1073
|
+
names = ", ".join(bindings)
|
|
1074
|
+
values = ", ".join(bindings.values())
|
|
1075
|
+
lines.append(f"({names},) = ({values},)")
|
|
1076
|
+
|
|
975
1077
|
# Reconstruct the synthetic ProgramSignature class up-front (when that
|
|
976
1078
|
# path applies) so its annotation imports land in sig_imports *before*
|
|
977
1079
|
# we emit the import block below. The class text itself is appended
|
|
@@ -1009,6 +1111,9 @@ def generate_program_source(
|
|
|
1009
1111
|
extra_kwargs = ""
|
|
1010
1112
|
if tool_names:
|
|
1011
1113
|
extra_kwargs = f", tools=[{', '.join(tool_names)}]"
|
|
1114
|
+
max_iters = getattr(module, "max_iters", None)
|
|
1115
|
+
if not is_user_module and isinstance(max_iters, int):
|
|
1116
|
+
extra_kwargs += f", max_iters={max_iters!r}"
|
|
1012
1117
|
|
|
1013
1118
|
module_assigned = False
|
|
1014
1119
|
if sig is not None:
|
|
@@ -605,3 +605,20 @@ A new hand-rolled serializer on any of those paths reopens the hole. `tests/test
|
|
|
605
605
|
- [`./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
606
|
- The `cmpnd-testing` skill + [`../AGENTS.md`](../AGENTS.md) § "Running Tests & Checks" — patterns for writing callback tests against real databases, including `clear_context()` usage.
|
|
607
607
|
- [`serve/DB.md`](../../serve/DB.md) — the Postgres schema these payloads land in.
|
|
608
|
+
|
|
609
|
+
### Registered MCP tools in ReAct
|
|
610
|
+
|
|
611
|
+
`cmpnd.mcp_tools` declares registered tools for ReAct and RLM, including modules
|
|
612
|
+
that contain them. Deployment resolves those declarations against the organization's
|
|
613
|
+
registry and pins their schemas. Bare MCP session closures cannot be packaged.
|
|
614
|
+
Conflicting tools with the same routing name are refused.
|
|
615
|
+
Generated composed programs retain global and closure aliases for registered tools,
|
|
616
|
+
including aliases stored in lists, tuples, or dictionaries. The packager rebuilds
|
|
617
|
+
these bindings from pinned declarations without copying live MCP sessions.
|
|
618
|
+
Generated mixed-tool bindings require ordinary tools to be named Python functions.
|
|
619
|
+
Use authored-source deployment for partials, callable instances, lambdas, or bound methods.
|
|
620
|
+
|
|
621
|
+
The WASM kernel calls tools through a typed worker import. The worker holds the
|
|
622
|
+
scoped tokens and returns the MCP result document. Optional arguments omitted by
|
|
623
|
+
the program remain omitted. ReAct's tool callbacks produce the application span;
|
|
624
|
+
the worker adds transport measurements to that span using its correlation ID.
|
|
@@ -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.3"
|
|
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)
|
|
@@ -34,11 +34,10 @@ on it: what this lane is about is whether a tool call a program makes reaches a
|
|
|
34
34
|
server, and nothing here depends on a model having *chosen* to make it. A lane about
|
|
35
35
|
the choosing needs a real model, which is what ``--real-model`` is for.
|
|
36
36
|
|
|
37
|
-
#
|
|
37
|
+
# ReAct and RLM
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
``SUBMIT``) differs from its sibling's and is not gratuitous.
|
|
39
|
+
``--tool-module react`` drives the outer kernel import. The default RLM path drives
|
|
40
|
+
its interpreter. Both use the registered tool and scoped authority on every rollout.
|
|
42
41
|
|
|
43
42
|
CMPND_ENDPOINT=... CMPND_API_KEY=... CMPND_DEV_LM_API_HOST=... \\
|
|
44
43
|
CMPND_TOOL_SERVER_URL=... python hosted_mcp_tool_optimize_probe.py --out result.json
|
|
@@ -50,6 +49,7 @@ import argparse
|
|
|
50
49
|
import json
|
|
51
50
|
import os
|
|
52
51
|
import sys
|
|
52
|
+
import tempfile
|
|
53
53
|
import time
|
|
54
54
|
from pathlib import Path
|
|
55
55
|
from typing import Any
|
|
@@ -75,6 +75,19 @@ EXAMPLES: list[dict[str, str]] = [
|
|
|
75
75
|
# looking entirely healthy.
|
|
76
76
|
TOOL = "catalog_answer"
|
|
77
77
|
|
|
78
|
+
# The packager replaces this runtime tool with its pinned declaration of the same name.
|
|
79
|
+
catalog_answer: Any = None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class Research(dspy.Module):
|
|
83
|
+
def __init__(self):
|
|
84
|
+
super().__init__()
|
|
85
|
+
self.solve = dspy.ReAct("question -> answer", tools=[catalog_answer], max_iters=1)
|
|
86
|
+
|
|
87
|
+
def forward(self, question):
|
|
88
|
+
return self.solve(question=question)
|
|
89
|
+
|
|
90
|
+
|
|
78
91
|
# The litellm model both roles use, matching the sibling probe's default so one sealed
|
|
79
92
|
# credential serves both lanes. Its PREFIX is what the platform files the provider
|
|
80
93
|
# credential under.
|
|
@@ -118,7 +131,7 @@ class ShortAnswer(dspy.Signature):
|
|
|
118
131
|
answer: str = dspy.OutputField()
|
|
119
132
|
|
|
120
133
|
|
|
121
|
-
def build_program(tool_server: str, credential: str, timeout_s: float) -> dspy.Module:
|
|
134
|
+
def build_program(tool_server: str, credential: str, timeout_s: float, module_kind: str = "rlm") -> dspy.Module:
|
|
122
135
|
"""The RLM under optimization, with its tools and the turn that calls one.
|
|
123
136
|
|
|
124
137
|
``cmpnd.mcp_tools`` connects to the server FROM HERE, which is what tells the deploy
|
|
@@ -168,6 +181,21 @@ def build_program(tool_server: str, credential: str, timeout_s: float) -> dspy.M
|
|
|
168
181
|
# tools.
|
|
169
182
|
tools = [tool for tool in listed if tool.name == TOOL]
|
|
170
183
|
|
|
184
|
+
if module_kind == "react":
|
|
185
|
+
global catalog_answer
|
|
186
|
+
catalog_answer = tools[0]
|
|
187
|
+
program = Research()
|
|
188
|
+
program.solve.react.demos = [
|
|
189
|
+
dspy.Example(
|
|
190
|
+
question=EXAMPLES[0]["question"],
|
|
191
|
+
trajectory="",
|
|
192
|
+
next_thought="Look up the catalogue entry",
|
|
193
|
+
next_tool_name=TOOL,
|
|
194
|
+
next_tool_args={"question": EXAMPLES[0]["question"], "style": "sentence"},
|
|
195
|
+
)
|
|
196
|
+
]
|
|
197
|
+
return program
|
|
198
|
+
|
|
171
199
|
# Two turns, not one: the first runs the demo's code and submits. The second exists
|
|
172
200
|
# only so a turn that failed has somewhere to be retried from, which is what turns a
|
|
173
201
|
# transient refusal into a slower rollout rather than a failed one.
|
|
@@ -221,6 +249,8 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
|
221
249
|
p = argparse.ArgumentParser(
|
|
222
250
|
description="Optimize a tool-calling program through serve, the optimizer and the executor."
|
|
223
251
|
)
|
|
252
|
+
p.add_argument("--tool-source", choices=("generated", "authored"), default="generated")
|
|
253
|
+
p.add_argument("--tool-module", choices=("rlm", "react"), default="rlm")
|
|
224
254
|
p.add_argument("--out", required=True, help="write the ids this run produced here, as JSON")
|
|
225
255
|
p.add_argument(
|
|
226
256
|
"--metric-calls",
|
|
@@ -265,11 +295,39 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
265
295
|
|
|
266
296
|
cmpnd.configure(api_key=api_key, endpoint=endpoint, project_tags=["hosted-mcp-tool-optimize-probe"])
|
|
267
297
|
|
|
268
|
-
program = build_program(args.tool_server, credential, args.tool_server_timeout)
|
|
298
|
+
program = build_program(args.tool_server, credential, args.tool_server_timeout, args.tool_module)
|
|
269
299
|
started_at = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
|
270
300
|
|
|
271
|
-
print(f"deploying
|
|
272
|
-
|
|
301
|
+
print(f"deploying {args.tool_module} calling {TOOL} on {args.tool_server} to {endpoint}", flush=True)
|
|
302
|
+
if args.tool_source == "authored":
|
|
303
|
+
if args.tool_module != "react":
|
|
304
|
+
raise ValueError("the authored tool probe uses ReAct")
|
|
305
|
+
from cmpnd import authored, deployment
|
|
306
|
+
|
|
307
|
+
demo = dict(getattr(program, "solve").react.demos[0])
|
|
308
|
+
source = "\n".join(
|
|
309
|
+
[
|
|
310
|
+
"import dspy",
|
|
311
|
+
"import cmpnd",
|
|
312
|
+
f"dspy.configure(lm=dspy.LM({MODEL!r}, api_base={os.environ['CMPND_DEV_LM_API_HOST']!r}, cache=False))",
|
|
313
|
+
f"tools = [tool for tool in cmpnd.mcp_tools({args.tool_server!r}) if tool.name == {TOOL!r}]",
|
|
314
|
+
"class Research(dspy.Module):",
|
|
315
|
+
" def __init__(self):",
|
|
316
|
+
" super().__init__()",
|
|
317
|
+
" self.solve = dspy.ReAct('question -> answer', tools=tools, max_iters=1)",
|
|
318
|
+
" def forward(self, question):",
|
|
319
|
+
" return self.solve(question=question)",
|
|
320
|
+
"program = Research()",
|
|
321
|
+
f"program.solve.react.demos = [dspy.Example(**{demo!r})]",
|
|
322
|
+
]
|
|
323
|
+
)
|
|
324
|
+
with tempfile.TemporaryDirectory(prefix="cmpnd-react-source-") as directory:
|
|
325
|
+
path = Path(directory) / "research.py"
|
|
326
|
+
path.write_text(source)
|
|
327
|
+
prepared = authored.prepare_file(path, api_key=api_key, endpoint=endpoint)
|
|
328
|
+
deployed = deployment.deploy_bundle(prepared.bundle, prepared.facts, timeout=600)
|
|
329
|
+
else:
|
|
330
|
+
deployed = cmpnd.deploy(program, metric=feedback_metric, timeout=600)
|
|
273
331
|
print(f"deployed {deployed}", flush=True)
|
|
274
332
|
|
|
275
333
|
examples = [dspy.Example(**row).with_inputs("question") for row in EXAMPLES]
|