agentinputlab 0.1.0a1__py3-none-any.whl
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.
- agentinputlab/__init__.py +28 -0
- agentinputlab/algorithms/__init__.py +6 -0
- agentinputlab/algorithms/acquire/__init__.py +145 -0
- agentinputlab/algorithms/acquire/adaptive_rag/__init__.py +43 -0
- agentinputlab/algorithms/acquire/adaptive_rag/replay.py +367 -0
- agentinputlab/algorithms/acquire/adaptive_rag/runtime.py +1202 -0
- agentinputlab/algorithms/acquire/adaptive_rag_common_support.py +542 -0
- agentinputlab/algorithms/acquire/hyde.py +296 -0
- agentinputlab/algorithms/acquire/hyde_controlled.py +200 -0
- agentinputlab/algorithms/acquire/hyde_external.py +148 -0
- agentinputlab/algorithms/acquire/ircot.py +28 -0
- agentinputlab/algorithms/acquire/ircot_common_support.py +478 -0
- agentinputlab/algorithms/acquire/ircot_native.py +355 -0
- agentinputlab/algorithms/acquire/ircot_replay.py +327 -0
- agentinputlab/algorithms/acquire/qrag/__init__.py +79 -0
- agentinputlab/algorithms/acquire/qrag/bridge.py +828 -0
- agentinputlab/algorithms/acquire/qrag/common_support.py +891 -0
- agentinputlab/algorithms/acquire/qrag/native_axis.py +71 -0
- agentinputlab/algorithms/acquire/qrag/reader_adapter.py +139 -0
- agentinputlab/algorithms/acquire/r2ag/__init__.py +57 -0
- agentinputlab/algorithms/acquire/r2ag/bridge.py +1312 -0
- agentinputlab/algorithms/acquire/r2ag/common_support.py +1029 -0
- agentinputlab/algorithms/acquire/r2ag/socket_runtime.py +255 -0
- agentinputlab/algorithms/clarify/__init__.py +61 -0
- agentinputlab/algorithms/clarify/_structured.py +64 -0
- agentinputlab/algorithms/clarify/active_task_disambiguation.py +371 -0
- agentinputlab/algorithms/clarify/ask_or_assume/__init__.py +17 -0
- agentinputlab/algorithms/clarify/ask_or_assume/monitor.py +410 -0
- agentinputlab/algorithms/clarify/future_turns.py +379 -0
- agentinputlab/algorithms/clarify/udcs.py +255 -0
- agentinputlab/algorithms/clarify/udcs_deterministic.py +196 -0
- agentinputlab/algorithms/curate/__init__.py +65 -0
- agentinputlab/algorithms/curate/acon/__init__.py +27 -0
- agentinputlab/algorithms/curate/acon/common.py +284 -0
- agentinputlab/algorithms/curate/acon/common_support.py +136 -0
- agentinputlab/algorithms/curate/acon/history_bridge.py +309 -0
- agentinputlab/algorithms/curate/acon/observation_bridge.py +290 -0
- agentinputlab/algorithms/curate/llmlingua/__init__.py +30 -0
- agentinputlab/algorithms/curate/llmlingua/adapters.py +537 -0
- agentinputlab/algorithms/curate/llmlingua/budget.py +48 -0
- agentinputlab/algorithms/curate/llmlingua/controlled.py +175 -0
- agentinputlab/algorithms/curate/llmlingua/multilingual.py +105 -0
- agentinputlab/algorithms/curate/llmlingua/span_map.py +167 -0
- agentinputlab/algorithms/curate/recomp/__init__.py +47 -0
- agentinputlab/algorithms/curate/recomp/external.py +1129 -0
- agentinputlab/algorithms/curate/recomp/selective.py +304 -0
- agentinputlab/algorithms/decompose/__init__.py +107 -0
- agentinputlab/algorithms/decompose/decomp/__init__.py +53 -0
- agentinputlab/algorithms/decompose/decomp/common_support.py +478 -0
- agentinputlab/algorithms/decompose/decomp/planner_component.py +199 -0
- agentinputlab/algorithms/decompose/decomp/worker_bridge.py +416 -0
- agentinputlab/algorithms/decompose/llm_compiler/__init__.py +35 -0
- agentinputlab/algorithms/decompose/llm_compiler/bridge.py +520 -0
- agentinputlab/algorithms/decompose/llm_compiler/common_support.py +504 -0
- agentinputlab/algorithms/decompose/plan_and_budget/__init__.py +43 -0
- agentinputlab/algorithms/decompose/plan_and_budget/cached_soft_budget.py +362 -0
- agentinputlab/algorithms/decompose/plan_and_budget/decomposer_component.py +107 -0
- agentinputlab/algorithms/decompose/plan_and_budget/full.py +205 -0
- agentinputlab/algorithms/decompose/plan_and_budget/hard_cap.py +246 -0
- agentinputlab/algorithms/decompose/plan_and_budget/regenerated_soft_budget.py +448 -0
- agentinputlab/algorithms/decompose/plan_and_solve/__init__.py +33 -0
- agentinputlab/algorithms/decompose/plan_and_solve/plan_component.py +192 -0
- agentinputlab/algorithms/decompose/plan_and_solve/ps_prompt.py +127 -0
- agentinputlab/algorithms/decompose/self_ask/__init__.py +27 -0
- agentinputlab/algorithms/decompose/self_ask/loop.py +362 -0
- agentinputlab/algorithms/decompose/self_ask/planner_component.py +117 -0
- agentinputlab/algorithms/reformulate/__init__.py +85 -0
- agentinputlab/algorithms/reformulate/infocqr/__init__.py +40 -0
- agentinputlab/algorithms/reformulate/infocqr/cache_replay.py +362 -0
- agentinputlab/algorithms/reformulate/infocqr/external.py +675 -0
- agentinputlab/algorithms/reformulate/infocqr/student.py +189 -0
- agentinputlab/algorithms/reformulate/query2doc/__init__.py +42 -0
- agentinputlab/algorithms/reformulate/query2doc/controlled_live.py +190 -0
- agentinputlab/algorithms/reformulate/query2doc/models.py +95 -0
- agentinputlab/algorithms/reformulate/query2doc/official_cached.py +335 -0
- agentinputlab/algorithms/reformulate/query2doc/querygym_bridge.py +223 -0
- agentinputlab/algorithms/reformulate/query2doc/querygym_runtime.py +326 -0
- agentinputlab/algorithms/reformulate/reformer/__init__.py +23 -0
- agentinputlab/algorithms/reformulate/reformer/external.py +417 -0
- agentinputlab/algorithms/reformulate/rrr.py +696 -0
- agentinputlab/annotation/__init__.py +5 -0
- agentinputlab/annotation/independent.py +428 -0
- agentinputlab/annotation/server.py +233 -0
- agentinputlab/annotation/static/app.js +121 -0
- agentinputlab/annotation/static/index.html +60 -0
- agentinputlab/annotation/static/review.css +2 -0
- agentinputlab/annotation/static/review.html +10 -0
- agentinputlab/annotation/static/review.js +11 -0
- agentinputlab/annotation/static/styles.css +55 -0
- agentinputlab/annotation/store.py +444 -0
- agentinputlab/api.py +368 -0
- agentinputlab/artifacts/__init__.py +15 -0
- agentinputlab/artifacts/json_file.py +104 -0
- agentinputlab/artifacts/manifest.py +98 -0
- agentinputlab/artifacts/store.py +157 -0
- agentinputlab/backends/__init__.py +98 -0
- agentinputlab/backends/embedding/__init__.py +25 -0
- agentinputlab/backends/embedding/base.py +145 -0
- agentinputlab/backends/embedding/callable.py +75 -0
- agentinputlab/backends/embedding/replay.py +129 -0
- agentinputlab/backends/llm/__init__.py +31 -0
- agentinputlab/backends/llm/base.py +149 -0
- agentinputlab/backends/llm/callable.py +108 -0
- agentinputlab/backends/llm/decomp_completion.py +88 -0
- agentinputlab/backends/llm/final_content_stop.py +72 -0
- agentinputlab/backends/llm/gpt_oss_final_channel.py +84 -0
- agentinputlab/backends/llm/openai_compatible.py +332 -0
- agentinputlab/backends/llm/transformers.py +171 -0
- agentinputlab/backends/retrieval/__init__.py +35 -0
- agentinputlab/backends/retrieval/base.py +50 -0
- agentinputlab/backends/retrieval/dense.py +777 -0
- agentinputlab/backends/retrieval/memory.py +119 -0
- agentinputlab/backends/retrieval/sqlite_fts.py +155 -0
- agentinputlab/backends/user/__init__.py +15 -0
- agentinputlab/backends/user/base.py +66 -0
- agentinputlab/backends/user/callable.py +54 -0
- agentinputlab/backends/user/replay.py +89 -0
- agentinputlab/backends/vector.py +38 -0
- agentinputlab/benchmarks/__init__.py +246 -0
- agentinputlab/benchmarks/acquisition.py +106 -0
- agentinputlab/benchmarks/ale.py +540 -0
- agentinputlab/benchmarks/appworld_ul.py +669 -0
- agentinputlab/benchmarks/assets.py +226 -0
- agentinputlab/benchmarks/base.py +68 -0
- agentinputlab/benchmarks/beir_archive.py +235 -0
- agentinputlab/benchmarks/beir_sample.py +269 -0
- agentinputlab/benchmarks/bfcl_native.py +245 -0
- agentinputlab/benchmarks/bfcl_v3.py +607 -0
- agentinputlab/benchmarks/cache.py +218 -0
- agentinputlab/benchmarks/clariq.py +203 -0
- agentinputlab/benchmarks/data/registry.json +321 -0
- agentinputlab/benchmarks/data/smoke_assets.json +511 -0
- agentinputlab/benchmarks/data/statebench_v0_8_1_task_ids.json +475 -0
- agentinputlab/benchmarks/data/toolsandbox_165848b_scenario_names.json +1 -0
- agentinputlab/benchmarks/execution.py +1111 -0
- agentinputlab/benchmarks/interactive.py +257 -0
- agentinputlab/benchmarks/ir_query.py +204 -0
- agentinputlab/benchmarks/longbench.py +203 -0
- agentinputlab/benchmarks/paired_replay.py +479 -0
- agentinputlab/benchmarks/preprocess.py +2313 -0
- agentinputlab/benchmarks/production.py +236 -0
- agentinputlab/benchmarks/qa.py +963 -0
- agentinputlab/benchmarks/registry.py +110 -0
- agentinputlab/benchmarks/retrieval.py +459 -0
- agentinputlab/benchmarks/runtime.py +509 -0
- agentinputlab/benchmarks/statebench.py +938 -0
- agentinputlab/benchmarks/tau3.py +725 -0
- agentinputlab/benchmarks/toolsandbox.py +626 -0
- agentinputlab/benchmarks/trec_dl_sample.py +263 -0
- agentinputlab/campaign.py +654 -0
- agentinputlab/campaign_runner.py +953 -0
- agentinputlab/cli/__init__.py +6 -0
- agentinputlab/cli/main.py +933 -0
- agentinputlab/connectors/__init__.py +46 -0
- agentinputlab/connectors/knowledge/__init__.py +4 -0
- agentinputlab/connectors/tools/__init__.py +30 -0
- agentinputlab/connectors/tools/base.py +57 -0
- agentinputlab/connectors/tools/mcp.py +222 -0
- agentinputlab/connectors/tools/memory.py +41 -0
- agentinputlab/connectors/tools/openapi.py +695 -0
- agentinputlab/connectors/tools/schema.py +102 -0
- agentinputlab/contracts/__init__.py +91 -0
- agentinputlab/contracts/availability.py +24 -0
- agentinputlab/contracts/dependency.py +58 -0
- agentinputlab/contracts/enums.py +82 -0
- agentinputlab/contracts/errors.py +63 -0
- agentinputlab/contracts/evidence.py +43 -0
- agentinputlab/contracts/intent.py +110 -0
- agentinputlab/contracts/lifecycle.py +164 -0
- agentinputlab/contracts/patch.py +167 -0
- agentinputlab/contracts/serialization.py +93 -0
- agentinputlab/contracts/state.py +127 -0
- agentinputlab/contracts/trace.py +59 -0
- agentinputlab/contracts/worker.py +42 -0
- agentinputlab/core/__init__.py +7 -0
- agentinputlab/data/__init__.py +32 -0
- agentinputlab/data/checksums.py +9 -0
- agentinputlab/data/downloader.py +235 -0
- agentinputlab/data/http.py +171 -0
- agentinputlab/data/index.py +21 -0
- agentinputlab/data/overlap.py +28 -0
- agentinputlab/data/preprocess.py +32 -0
- agentinputlab/data/storage.py +57 -0
- agentinputlab/data/trec_sample.py +427 -0
- agentinputlab/evaluation/__init__.py +5 -0
- agentinputlab/evaluation/outcomes.py +32 -0
- agentinputlab/experiments/__init__.py +278 -0
- agentinputlab/experiments/analysis.py +981 -0
- agentinputlab/experiments/artifacts.py +687 -0
- agentinputlab/experiments/automatic_measurement.py +678 -0
- agentinputlab/experiments/auxiliary_matrix.py +203 -0
- agentinputlab/experiments/bindings.py +430 -0
- agentinputlab/experiments/blueprints.py +147 -0
- agentinputlab/experiments/builders/__init__.py +45 -0
- agentinputlab/experiments/builders/executable_external_payload.py +423 -0
- agentinputlab/experiments/builders/external_process_arena.py +1018 -0
- agentinputlab/experiments/builders/registry_intervention.py +463 -0
- agentinputlab/experiments/cli.py +158 -0
- agentinputlab/experiments/controls.py +340 -0
- agentinputlab/experiments/driver_guard.py +205 -0
- agentinputlab/experiments/drivers/__init__.py +89 -0
- agentinputlab/experiments/drivers/external_arena.py +2908 -0
- agentinputlab/experiments/drivers/method_process.py +662 -0
- agentinputlab/experiments/drivers/parity_process.py +1484 -0
- agentinputlab/experiments/drivers/static_production.py +1574 -0
- agentinputlab/experiments/executor.py +448 -0
- agentinputlab/experiments/loader.py +605 -0
- agentinputlab/experiments/measurement.py +675 -0
- agentinputlab/experiments/paper_analysis.py +1818 -0
- agentinputlab/experiments/parity_identity.py +605 -0
- agentinputlab/experiments/plan.py +277 -0
- agentinputlab/experiments/production_builders.py +3739 -0
- agentinputlab/experiments/readiness.py +432 -0
- agentinputlab/experiments/schema.py +514 -0
- agentinputlab/experiments/secrets.py +177 -0
- agentinputlab/experiments/stateful_analysis.py +1738 -0
- agentinputlab/experiments/transfer_analysis.py +1316 -0
- agentinputlab/experiments/unseen_analysis.py +1025 -0
- agentinputlab/integrations/__init__.py +1 -0
- agentinputlab/integrations/action_space/__init__.py +1 -0
- agentinputlab/integrations/external_workers/__init__.py +76 -0
- agentinputlab/integrations/external_workers/ace.py +431 -0
- agentinputlab/integrations/external_workers/base.py +181 -0
- agentinputlab/integrations/external_workers/decomp_live.py +249 -0
- agentinputlab/integrations/external_workers/decomp_planner.py +30 -0
- agentinputlab/integrations/external_workers/infocqr_student.py +280 -0
- agentinputlab/integrations/external_workers/ircot.py +576 -0
- agentinputlab/integrations/external_workers/llmcompiler_live.py +360 -0
- agentinputlab/integrations/external_workers/longllmlingua.py +162 -0
- agentinputlab/integrations/external_workers/method.py +314 -0
- agentinputlab/integrations/external_workers/process.py +678 -0
- agentinputlab/integrations/external_workers/supervisor.py +1071 -0
- agentinputlab/integrations/full_architecture/__init__.py +1 -0
- agentinputlab/integrations/full_architecture/hyde.py +212 -0
- agentinputlab/integrations/full_architecture/plan_and_budget.py +188 -0
- agentinputlab/integrations/live_history.py +222 -0
- agentinputlab/integrations/prompt_optimization/__init__.py +1 -0
- agentinputlab/integrations/training/__init__.py +1 -0
- agentinputlab/metrics/__init__.py +50 -0
- agentinputlab/metrics/catalog.py +313 -0
- agentinputlab/metrics/harm.py +95 -0
- agentinputlab/metrics/paired.py +40 -0
- agentinputlab/metrics/qa.py +76 -0
- agentinputlab/metrics/text.py +33 -0
- agentinputlab/metrics/third_party.py +161 -0
- agentinputlab/models/__init__.py +4 -0
- agentinputlab/observability/__init__.py +56 -0
- agentinputlab/observability/budget.py +316 -0
- agentinputlab/observability/instrumentation.py +754 -0
- agentinputlab/observability/models.py +403 -0
- agentinputlab/observability/monitor.py +380 -0
- agentinputlab/optimizers/__init__.py +1 -0
- agentinputlab/optimizers/clarify/__init__.py +12 -0
- agentinputlab/optimizers/context/__init__.py +22 -0
- agentinputlab/optimizers/decompose/__init__.py +64 -0
- agentinputlab/optimizers/evidence/__init__.py +22 -0
- agentinputlab/optimizers/rewrite/__init__.py +30 -0
- agentinputlab/parity/__init__.py +5 -0
- agentinputlab/parity/base.py +206 -0
- agentinputlab/production_campaign.py +763 -0
- agentinputlab/py.typed +1 -0
- agentinputlab/recipes/__init__.py +333 -0
- agentinputlab/recipes/ace.py +418 -0
- agentinputlab/recipes/ace_bfcl.py +273 -0
- agentinputlab/recipes/ace_common_support.py +169 -0
- agentinputlab/recipes/ace_native_plan.py +322 -0
- agentinputlab/recipes/ace_online.py +711 -0
- agentinputlab/recipes/ace_retrieval.py +330 -0
- agentinputlab/recipes/ace_statebench.py +382 -0
- agentinputlab/recipes/ace_tau.py +463 -0
- agentinputlab/recipes/ace_toolsandbox.py +432 -0
- agentinputlab/recipes/acon_guideline.py +22 -0
- agentinputlab/recipes/adaptive_rag.py +822 -0
- agentinputlab/recipes/llmlingua2_inference.py +146 -0
- agentinputlab/recipes/llmlingua2_training.py +796 -0
- agentinputlab/recipes/mce.py +1486 -0
- agentinputlab/recipes/mce_final.py +335 -0
- agentinputlab/recipes/mce_text_fit.py +499 -0
- agentinputlab/recipes/mce_text_only.py +298 -0
- agentinputlab/recipes/qrag.py +729 -0
- agentinputlab/recipes/qrag_inference.py +364 -0
- agentinputlab/recipes/r2ag_features.py +236 -0
- agentinputlab/recipes/r2ag_inference.py +503 -0
- agentinputlab/recipes/r2ag_training.py +948 -0
- agentinputlab/recipes/recomp_abstractive.py +592 -0
- agentinputlab/recipes/recomp_extractive.py +525 -0
- agentinputlab/recipes/reformer.py +17 -0
- agentinputlab/recipes/rqrag.py +1516 -0
- agentinputlab/recipes/rqrag_common_support.py +193 -0
- agentinputlab/recipes/rqrag_completion.py +147 -0
- agentinputlab/recipes/skillopt.py +462 -0
- agentinputlab/recipes/skillopt_common_support.py +154 -0
- agentinputlab/recipes/skillopt_environment.py +165 -0
- agentinputlab/recipes/skillopt_environment_cli.py +5 -0
- agentinputlab/recipes/skillopt_retrieval.py +188 -0
- agentinputlab/recipes/smartsearch.py +897 -0
- agentinputlab/recipes/smartsearch_curriculum.py +283 -0
- agentinputlab/recipes/smartsearch_grpo_contracts.py +58 -0
- agentinputlab/recipes/smartsearch_grpo_data.py +314 -0
- agentinputlab/recipes/smartsearch_grpo_meter.py +324 -0
- agentinputlab/recipes/smartsearch_grpo_prefix.py +144 -0
- agentinputlab/recipes/smartsearch_grpo_rollout.py +119 -0
- agentinputlab/recipes/smartsearch_grpo_tools.py +364 -0
- agentinputlab/recipes/smartsearch_grpo_training.py +305 -0
- agentinputlab/recipes/smartsearch_grpo_transport.py +248 -0
- agentinputlab/recipes/smartsearch_grpo_worker.py +364 -0
- agentinputlab/recipes/smartsearch_inference.py +524 -0
- agentinputlab/recipes/smartsearch_native_data.py +172 -0
- agentinputlab/recipes/smartsearch_policy.py +616 -0
- agentinputlab/recipes/smartsearch_policy_worker.py +9 -0
- agentinputlab/recipes/smartsearch_sampling.py +232 -0
- agentinputlab/recipes/smartsearch_teacher.py +203 -0
- agentinputlab/recipes/smartsearch_training.py +265 -0
- agentinputlab/registry/__init__.py +46 -0
- agentinputlab/registry/beta.py +727 -0
- agentinputlab/registry/data/algorithms.json +1959 -0
- agentinputlab/registry/data/auxiliary_method_profiles.json +34 -0
- agentinputlab/registry/loader.py +100 -0
- agentinputlab/registry/result_manifest.py +184 -0
- agentinputlab/registry/runtime.py +195 -0
- agentinputlab/registry/schema.py +449 -0
- agentinputlab/runner/__init__.py +30 -0
- agentinputlab/runner/balance.py +248 -0
- agentinputlab/runner/cache.py +135 -0
- agentinputlab/runner/composite.py +130 -0
- agentinputlab/runner/engine.py +285 -0
- agentinputlab/runner/runtime_guard.py +268 -0
- agentinputlab/runner/worker.py +80 -0
- agentinputlab/runtime_bootstrap.py +288 -0
- agentinputlab/selectors/__init__.py +196 -0
- agentinputlab/selectors/advanced.py +1514 -0
- agentinputlab/selectors/artifacts.py +1087 -0
- agentinputlab/selectors/base.py +112 -0
- agentinputlab/selectors/campaigns.py +1893 -0
- agentinputlab/selectors/catalog.py +86 -0
- agentinputlab/selectors/discovery.py +1860 -0
- agentinputlab/selectors/evaluation.py +1348 -0
- agentinputlab/selectors/features.py +383 -0
- agentinputlab/selectors/learned.py +502 -0
- agentinputlab/selectors/no_op.py +28 -0
- agentinputlab/selectors/outcome_table.py +475 -0
- agentinputlab/selectors/producer.py +853 -0
- agentinputlab/selectors/rules.py +109 -0
- agentinputlab/selectors/runtime.py +420 -0
- agentinputlab/studies/__init__.py +16 -0
- agentinputlab/studies/design.py +165 -0
- agentinputlab/studies/paired.py +322 -0
- agentinputlab/studio/__init__.py +6 -0
- agentinputlab/studio/campaigns.py +819 -0
- agentinputlab/studio/engine.py +1408 -0
- agentinputlab/studio/model_backends.py +425 -0
- agentinputlab/studio/prepared.py +410 -0
- agentinputlab/studio/server.py +613 -0
- agentinputlab/studio/static/app.js +397 -0
- agentinputlab/studio/static/campaigns.html +175 -0
- agentinputlab/studio/static/campaigns.js +426 -0
- agentinputlab/studio/static/index.html +192 -0
- agentinputlab/studio/static/result.html +103 -0
- agentinputlab/studio/static/result.js +273 -0
- agentinputlab/studio/static/styles.css +467 -0
- agentinputlab-0.1.0a1.data/data/share/agentinputlab/CITATION.cff +22 -0
- agentinputlab-0.1.0a1.dist-info/METADATA +392 -0
- agentinputlab-0.1.0a1.dist-info/RECORD +469 -0
- agentinputlab-0.1.0a1.dist-info/WHEEL +5 -0
- agentinputlab-0.1.0a1.dist-info/entry_points.txt +6 -0
- agentinputlab-0.1.0a1.dist-info/licenses/LICENSE +21 -0
- agentinputlab-0.1.0a1.dist-info/licenses/NOTICE +31 -0
- agentinputlab-0.1.0a1.dist-info/licenses/THIRD_PARTY_NOTICES.md +94 -0
- agentinputlab-0.1.0a1.dist-info/top_level.txt +2 -0
- workers/README.md +94 -0
- workers/ace/README.md +261 -0
- workers/ace/__init__.py +1 -0
- workers/ace/compatibility_manifest.json +39 -0
- workers/ace/driver.py +1682 -0
- workers/acon/README.md +215 -0
- workers/acon/__init__.py +5 -0
- workers/acon/compatibility_manifest.json +93 -0
- workers/acon/driver.py +1859 -0
- workers/acon/fitted.py +395 -0
- workers/acon/guideline.py +470 -0
- workers/acon/guideline_source_manifest.json +20 -0
- workers/adaptive_rag/README.md +96 -0
- workers/adaptive_rag/__init__.py +15 -0
- workers/adaptive_rag/compatibility_manifest.json +61 -0
- workers/adaptive_rag/driver.py +1470 -0
- workers/adaptive_rag/fixture_runtime.py +163 -0
- workers/adaptive_rag/runtime.py +252 -0
- workers/ask_or_assume/README.md +124 -0
- workers/ask_or_assume/__init__.py +1 -0
- workers/ask_or_assume/compatibility_manifest.json +36 -0
- workers/ask_or_assume/driver.py +1227 -0
- workers/decomp/README.md +133 -0
- workers/decomp/UPSTREAM_LICENSE.Apache-2.0.txt +201 -0
- workers/decomp/__init__.py +5 -0
- workers/decomp/compatibility_manifest.json +205 -0
- workers/decomp/driver.py +1015 -0
- workers/decomp/live.py +137 -0
- workers/decomp/runtime.py +231 -0
- workers/infocqr/README.md +106 -0
- workers/infocqr/__init__.py +1 -0
- workers/infocqr/compatibility_manifest.json +84 -0
- workers/infocqr/driver.py +1766 -0
- workers/ircot/README.md +167 -0
- workers/ircot/__init__.py +7 -0
- workers/ircot/compatibility_manifest.json +121 -0
- workers/ircot/controlled.py +457 -0
- workers/ircot/driver.py +1373 -0
- workers/ircot/fixture_bindings.json +39 -0
- workers/ircot/fixture_config.json +48 -0
- workers/ircot/fixture_prompt.txt +2 -0
- workers/ircot/native_qa.py +398 -0
- workers/ircot/qa_compatibility_manifest.json +57 -0
- workers/ircot/runtime.py +97 -0
- workers/llmcompiler/README.md +160 -0
- workers/llmcompiler/__init__.py +1 -0
- workers/llmcompiler/compatibility_manifest.json +45 -0
- workers/llmcompiler/driver.py +1514 -0
- workers/llmcompiler/export_prompts.py +74 -0
- workers/llmcompiler/live.py +174 -0
- workers/llmcompiler/patch_manifest.json +147 -0
- workers/mce/README.md +198 -0
- workers/mce/__init__.py +29 -0
- workers/mce/compatibility_manifest.json +115 -0
- workers/mce/driver.py +2585 -0
- workers/mce/final_eval.py +173 -0
- workers/official_arenas/README.md +25 -0
- workers/official_arenas/__init__.py +1 -0
- workers/official_arenas/capture_records.py +110 -0
- workers/official_arenas/driver.py +619 -0
- workers/official_arenas/protocol.py +334 -0
- workers/official_arenas/providers/__init__.py +1 -0
- workers/official_arenas/providers/_live_history.py +228 -0
- workers/official_arenas/providers/_message_compat.py +77 -0
- workers/official_arenas/providers/appworld_ul.py +196 -0
- workers/official_arenas/providers/fixture.py +112 -0
- workers/official_arenas/providers/statebench.py +960 -0
- workers/official_arenas/providers/tau3.py +1672 -0
- workers/official_arenas/providers/tau_retail.py +586 -0
- workers/official_arenas/providers/toolsandbox.py +1321 -0
- workers/official_arenas/providers/tooltalk.py +699 -0
- workers/official_arenas/retail_capture.py +188 -0
- workers/official_arenas/retail_driver.py +275 -0
- workers/qrag/README.md +172 -0
- workers/qrag/__init__.py +1 -0
- workers/qrag/compatibility_manifest.json +64 -0
- workers/qrag/driver.py +673 -0
- workers/r2ag/README.md +154 -0
- workers/r2ag/__init__.py +5 -0
- workers/r2ag/compatibility_manifest.json +55 -0
- workers/r2ag/driver.py +1287 -0
- workers/r2ag/feature_schema.json +8 -0
- workers/r2ag/runtime.py +589 -0
- workers/recomp/README.md +120 -0
- workers/recomp/__init__.py +1 -0
- workers/recomp/compatibility_manifest.json +43 -0
- workers/recomp/driver.py +1653 -0
- workers/reformer/README.md +125 -0
- workers/reformer/__init__.py +1 -0
- workers/reformer/compatibility_manifest.json +68 -0
- workers/reformer/driver.py +1459 -0
- workers/reformer/induction.py +457 -0
- workers/rrr/README.md +126 -0
- workers/rrr/__init__.py +1 -0
- workers/rrr/compatibility_manifest.json +57 -0
- workers/rrr/driver.py +1133 -0
- workers/smartsearch/README.md +331 -0
- workers/smartsearch/__init__.py +1 -0
- workers/smartsearch/compatibility_manifest.json +41 -0
- workers/smartsearch/driver.py +1643 -0
|
@@ -0,0 +1,1227 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Pinned standalone component worker for Ask or Assume?.
|
|
3
|
+
|
|
4
|
+
The official ``IntentAgent`` is tightly coupled to OpenHands State,
|
|
5
|
+
ConversationMemory, Condenser, LLMRegistry, and controller transitions. This
|
|
6
|
+
worker therefore does not claim to run UA-Multi. It loads the exact pinned
|
|
7
|
+
IntentAgent prompt and ``ClarifyDecisionTool``/``ClarifyTool`` schemas without
|
|
8
|
+
executing checkout code, then runs the two separable model paths through one
|
|
9
|
+
operator-configured OpenAI-compatible backend.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import ast
|
|
16
|
+
import hashlib
|
|
17
|
+
import json
|
|
18
|
+
import math
|
|
19
|
+
import os
|
|
20
|
+
import re
|
|
21
|
+
import subprocess
|
|
22
|
+
import sys
|
|
23
|
+
import urllib.error
|
|
24
|
+
import urllib.parse
|
|
25
|
+
import urllib.request
|
|
26
|
+
from collections.abc import Mapping, Sequence
|
|
27
|
+
from dataclasses import dataclass
|
|
28
|
+
from pathlib import Path, PurePosixPath
|
|
29
|
+
from typing import Any, Protocol, TextIO
|
|
30
|
+
|
|
31
|
+
UPSTREAM_COMMIT = "728464a755f15fd448aff61a5cb32dce97fd9c1d"
|
|
32
|
+
METHOD_ID = "ask_or_assume"
|
|
33
|
+
RELEASE_ID = "clarify.ask_or_assume.intent_monitor_component"
|
|
34
|
+
PROTOCOL_VERSION = "agentinputlab.ask-or-assume.monitor.v1"
|
|
35
|
+
MANIFEST_SCHEMA = "agentinputlab.ask-or-assume.compatibility.v1"
|
|
36
|
+
DEFAULT_MANIFEST = Path(__file__).with_name("compatibility_manifest.json")
|
|
37
|
+
INTENT_PROMPT_PATH = "openhands/agenthub/intent_agent/prompts/system_prompt.j2"
|
|
38
|
+
INTENT_TOOL_PATH = "openhands/agenthub/intent_agent/tools/clarify_decision.py"
|
|
39
|
+
CLARIFY_TOOL_PATH = "openhands/agenthub/clarify_agent/tools/clarify.py"
|
|
40
|
+
TOOL_NAMES_PATH = "openhands/llm/tool_names.py"
|
|
41
|
+
MAX_REQUEST_BYTES = 2 * 1024 * 1024
|
|
42
|
+
MAX_HISTORY_ITEMS = 2048
|
|
43
|
+
MAX_TEXT_CHARS = 500_000
|
|
44
|
+
_HASH_RE = re.compile(r"[0-9a-f]{64}")
|
|
45
|
+
_COMMIT_RE = re.compile(r"[0-9a-f]{40}")
|
|
46
|
+
_REF_RE = re.compile(r"refs/[A-Za-z0-9._/-]+")
|
|
47
|
+
_ENV_NAME_RE = re.compile(r"[A-Z_][A-Z0-9_]*")
|
|
48
|
+
_ROOT_FIELDS = frozenset(
|
|
49
|
+
{
|
|
50
|
+
"method_id",
|
|
51
|
+
"release_id",
|
|
52
|
+
"upstream_commit",
|
|
53
|
+
"state_hash",
|
|
54
|
+
"payload",
|
|
55
|
+
"dependency_fingerprints",
|
|
56
|
+
"minimum_reservation",
|
|
57
|
+
"protocol_version",
|
|
58
|
+
"request_id",
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
_PAYLOAD_FIELDS = frozenset(
|
|
62
|
+
{
|
|
63
|
+
"task_id",
|
|
64
|
+
"task_view",
|
|
65
|
+
"history",
|
|
66
|
+
"turn_index",
|
|
67
|
+
"clarification_turns",
|
|
68
|
+
"controls",
|
|
69
|
+
"intent_prompt_hash",
|
|
70
|
+
"seed",
|
|
71
|
+
}
|
|
72
|
+
)
|
|
73
|
+
_CONTROL_FIELDS = frozenset(
|
|
74
|
+
{
|
|
75
|
+
"skip_first_turn",
|
|
76
|
+
"max_coding_turns",
|
|
77
|
+
"max_user_turns",
|
|
78
|
+
"max_model_calls",
|
|
79
|
+
"max_tokens",
|
|
80
|
+
"max_cost_usd",
|
|
81
|
+
}
|
|
82
|
+
)
|
|
83
|
+
_ROLE_NAMES = ("intent", "coding")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _requires_qwen38_initial_system_message(model: Any) -> bool:
|
|
87
|
+
if not isinstance(model, str):
|
|
88
|
+
return False
|
|
89
|
+
alias = model.strip().casefold().replace("_", "-")
|
|
90
|
+
return alias == "qwen3.8" or alias.startswith("qwen3.8-")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _coalesce_chat_system_messages(messages: Sequence[Any]) -> list[Any]:
|
|
94
|
+
copied = [dict(item) if isinstance(item, Mapping) else item for item in messages]
|
|
95
|
+
system_items: list[dict[str, Any]] = []
|
|
96
|
+
non_system_items: list[Any] = []
|
|
97
|
+
for item in copied:
|
|
98
|
+
if isinstance(item, Mapping) and item.get("role") == "system":
|
|
99
|
+
content = item.get("content")
|
|
100
|
+
if not isinstance(content, str):
|
|
101
|
+
raise DriverError("INVALID_SCHEMA", "Qwen3.8 system message content must be text")
|
|
102
|
+
system_items.append(dict(item))
|
|
103
|
+
else:
|
|
104
|
+
non_system_items.append(item)
|
|
105
|
+
if not system_items:
|
|
106
|
+
return copied
|
|
107
|
+
merged = dict(system_items[0])
|
|
108
|
+
merged["content"] = "\n\n".join(item["content"] for item in system_items)
|
|
109
|
+
return [merged, *non_system_items]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class DriverError(RuntimeError):
|
|
113
|
+
"""Expected fail-closed worker error with a stable public code."""
|
|
114
|
+
|
|
115
|
+
def __init__(self, code: str, message: str) -> None:
|
|
116
|
+
super().__init__(message)
|
|
117
|
+
self.code = code
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@dataclass(frozen=True, slots=True)
|
|
121
|
+
class CompatibilityPolicy:
|
|
122
|
+
commit: str
|
|
123
|
+
license_path: str
|
|
124
|
+
license_spdx: str
|
|
125
|
+
license_sha256: str
|
|
126
|
+
require_clean_checkout: bool
|
|
127
|
+
excluded_prefixes: tuple[str, ...]
|
|
128
|
+
source_hashes: Mapping[str, str]
|
|
129
|
+
|
|
130
|
+
@classmethod
|
|
131
|
+
def from_mapping(cls, value: Mapping[str, Any]) -> CompatibilityPolicy:
|
|
132
|
+
if value.get("schema_version") != MANIFEST_SCHEMA:
|
|
133
|
+
raise DriverError("INVALID_MANIFEST", "Ask-or-Assume manifest schema mismatch")
|
|
134
|
+
upstream = _mapping(value.get("upstream"), "manifest upstream")
|
|
135
|
+
sources = _mapping(value.get("source_files"), "manifest source_files")
|
|
136
|
+
commit = _text(upstream.get("commit"), "manifest commit")
|
|
137
|
+
license_spdx = _text(upstream.get("license_spdx"), "manifest license_spdx")
|
|
138
|
+
if commit != UPSTREAM_COMMIT or license_spdx != "MIT":
|
|
139
|
+
raise DriverError(
|
|
140
|
+
"INVALID_MANIFEST",
|
|
141
|
+
"Ask-or-Assume manifest must pin the approved commit and MIT root license",
|
|
142
|
+
)
|
|
143
|
+
clean = upstream.get("require_clean_checkout")
|
|
144
|
+
if not isinstance(clean, bool):
|
|
145
|
+
raise DriverError("INVALID_MANIFEST", "manifest clean-checkout flag must be boolean")
|
|
146
|
+
raw_excluded = upstream.get("excluded_prefixes")
|
|
147
|
+
if not isinstance(raw_excluded, Sequence) or isinstance(raw_excluded, (str, bytes)):
|
|
148
|
+
raise DriverError("INVALID_MANIFEST", "manifest excluded_prefixes must be a sequence")
|
|
149
|
+
excluded = tuple(_safe_prefix(item) for item in raw_excluded)
|
|
150
|
+
if "enterprise/" not in excluded:
|
|
151
|
+
raise DriverError("INVALID_MANIFEST", "manifest must exclude enterprise/")
|
|
152
|
+
source_hashes: dict[str, str] = {}
|
|
153
|
+
for raw_path, raw_hash in sources.items():
|
|
154
|
+
path = _safe_relative_path(raw_path, "manifest source path")
|
|
155
|
+
_reject_excluded_path(path, excluded)
|
|
156
|
+
source_hashes[path] = _hash(raw_hash, f"manifest hash for {path}")
|
|
157
|
+
required = {INTENT_PROMPT_PATH, INTENT_TOOL_PATH, CLARIFY_TOOL_PATH, TOOL_NAMES_PATH}
|
|
158
|
+
if not required <= set(source_hashes):
|
|
159
|
+
raise DriverError("INVALID_MANIFEST", "manifest omits an official prompt/tool source")
|
|
160
|
+
return cls(
|
|
161
|
+
commit=commit,
|
|
162
|
+
license_path=_safe_relative_path(
|
|
163
|
+
_text(upstream.get("license_path"), "manifest license_path"),
|
|
164
|
+
"manifest license_path",
|
|
165
|
+
),
|
|
166
|
+
license_spdx=license_spdx,
|
|
167
|
+
license_sha256=_hash(upstream.get("license_sha256"), "manifest license hash"),
|
|
168
|
+
require_clean_checkout=clean,
|
|
169
|
+
excluded_prefixes=excluded,
|
|
170
|
+
source_hashes=source_hashes,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@dataclass(frozen=True, slots=True)
|
|
175
|
+
class RoleConfig:
|
|
176
|
+
model: str
|
|
177
|
+
fingerprint: Mapping[str, Any]
|
|
178
|
+
max_output_tokens: int
|
|
179
|
+
input_cost_per_million: float
|
|
180
|
+
output_cost_per_million: float
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _fingerprint_model(value: Mapping[str, Any]) -> str | None:
|
|
184
|
+
"""Resolve model identity from native or AgentInputLab backend fingerprints."""
|
|
185
|
+
|
|
186
|
+
for key in ("model", "model_id"):
|
|
187
|
+
candidate = value.get(key)
|
|
188
|
+
if isinstance(candidate, str) and candidate.strip():
|
|
189
|
+
return candidate
|
|
190
|
+
nested = value.get("backend")
|
|
191
|
+
if isinstance(nested, Mapping):
|
|
192
|
+
return _fingerprint_model(nested)
|
|
193
|
+
return None
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@dataclass(frozen=True, slots=True)
|
|
197
|
+
class BackendConfig:
|
|
198
|
+
base_url: str
|
|
199
|
+
api_key_env: str
|
|
200
|
+
timeout_seconds: float
|
|
201
|
+
roles: Mapping[str, RoleConfig]
|
|
202
|
+
|
|
203
|
+
@classmethod
|
|
204
|
+
def from_json(cls, raw: str) -> BackendConfig:
|
|
205
|
+
root = _mapping(_loads_strict_json(raw), "backend configuration")
|
|
206
|
+
_exact_keys(root, {"base_url", "api_key_env", "timeout_seconds", "roles"}, "backend")
|
|
207
|
+
base_url = _validate_base_url(_text(root["base_url"], "backend base_url"))
|
|
208
|
+
api_key_env = _text(root["api_key_env"], "backend api_key_env")
|
|
209
|
+
if _ENV_NAME_RE.fullmatch(api_key_env) is None:
|
|
210
|
+
raise DriverError("INVALID_BACKEND_CONFIG", "backend api_key_env is invalid")
|
|
211
|
+
timeout = _positive_float(root["timeout_seconds"], "backend timeout_seconds")
|
|
212
|
+
raw_roles = _mapping(root["roles"], "backend roles")
|
|
213
|
+
if set(raw_roles) != set(_ROLE_NAMES):
|
|
214
|
+
raise DriverError("INVALID_BACKEND_CONFIG", "backend must bind intent and coding roles")
|
|
215
|
+
roles: dict[str, RoleConfig] = {}
|
|
216
|
+
for role in _ROLE_NAMES:
|
|
217
|
+
item = _mapping(raw_roles[role], f"backend {role} role")
|
|
218
|
+
_exact_keys(
|
|
219
|
+
item,
|
|
220
|
+
{
|
|
221
|
+
"model",
|
|
222
|
+
"fingerprint",
|
|
223
|
+
"max_output_tokens",
|
|
224
|
+
"input_cost_per_million",
|
|
225
|
+
"output_cost_per_million",
|
|
226
|
+
},
|
|
227
|
+
f"backend {role} role",
|
|
228
|
+
)
|
|
229
|
+
model = _text(item["model"], f"backend {role} model")
|
|
230
|
+
fingerprint = dict(_mapping(item["fingerprint"], f"backend {role} fingerprint"))
|
|
231
|
+
if not fingerprint or _fingerprint_model(fingerprint) != model:
|
|
232
|
+
raise DriverError(
|
|
233
|
+
"INVALID_BACKEND_CONFIG",
|
|
234
|
+
f"backend {role} fingerprint must bind its exact model",
|
|
235
|
+
)
|
|
236
|
+
_canonical_json(fingerprint)
|
|
237
|
+
roles[role] = RoleConfig(
|
|
238
|
+
model=model,
|
|
239
|
+
fingerprint=fingerprint,
|
|
240
|
+
max_output_tokens=_positive_int(
|
|
241
|
+
item["max_output_tokens"], f"backend {role} max_output_tokens"
|
|
242
|
+
),
|
|
243
|
+
input_cost_per_million=_nonnegative_float(
|
|
244
|
+
item["input_cost_per_million"], f"backend {role} input price"
|
|
245
|
+
),
|
|
246
|
+
output_cost_per_million=_nonnegative_float(
|
|
247
|
+
item["output_cost_per_million"], f"backend {role} output price"
|
|
248
|
+
),
|
|
249
|
+
)
|
|
250
|
+
return cls(base_url=base_url, api_key_env=api_key_env, timeout_seconds=timeout, roles=roles)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
@dataclass(frozen=True, slots=True)
|
|
254
|
+
class CompletionResult:
|
|
255
|
+
arguments: Mapping[str, Any]
|
|
256
|
+
input_tokens: int
|
|
257
|
+
output_tokens: int
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class CompletionBackend(Protocol):
|
|
261
|
+
def fingerprint(self, role: str) -> Mapping[str, Any]: ...
|
|
262
|
+
|
|
263
|
+
def complete(
|
|
264
|
+
self,
|
|
265
|
+
*,
|
|
266
|
+
role: str,
|
|
267
|
+
messages: Sequence[Mapping[str, Any]],
|
|
268
|
+
tool: Mapping[str, Any],
|
|
269
|
+
seed: int,
|
|
270
|
+
) -> CompletionResult: ...
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
class OpenAICompatibleBackend:
|
|
274
|
+
"""Small startup-configured OpenAI-compatible chat-completions client."""
|
|
275
|
+
|
|
276
|
+
def __init__(self, config: BackendConfig) -> None:
|
|
277
|
+
self.config = config
|
|
278
|
+
|
|
279
|
+
def fingerprint(self, role: str) -> Mapping[str, Any]:
|
|
280
|
+
return self.config.roles[role].fingerprint
|
|
281
|
+
|
|
282
|
+
def complete(
|
|
283
|
+
self,
|
|
284
|
+
*,
|
|
285
|
+
role: str,
|
|
286
|
+
messages: Sequence[Mapping[str, Any]],
|
|
287
|
+
tool: Mapping[str, Any],
|
|
288
|
+
seed: int,
|
|
289
|
+
) -> CompletionResult:
|
|
290
|
+
role_config = self.config.roles[role]
|
|
291
|
+
api_key = os.environ.get(self.config.api_key_env)
|
|
292
|
+
if not api_key:
|
|
293
|
+
raise DriverError("BACKEND_CREDENTIAL_UNAVAILABLE", "backend credential is unavailable")
|
|
294
|
+
tool_name = _tool_name(tool)
|
|
295
|
+
request_messages = list(messages)
|
|
296
|
+
if _requires_qwen38_initial_system_message(role_config.model):
|
|
297
|
+
request_messages = _coalesce_chat_system_messages(request_messages)
|
|
298
|
+
body = {
|
|
299
|
+
"model": role_config.model,
|
|
300
|
+
"messages": request_messages,
|
|
301
|
+
"tools": [tool],
|
|
302
|
+
"tool_choice": {"type": "function", "function": {"name": tool_name}},
|
|
303
|
+
"temperature": 0,
|
|
304
|
+
"seed": seed,
|
|
305
|
+
"max_tokens": role_config.max_output_tokens,
|
|
306
|
+
}
|
|
307
|
+
encoded = _canonical_json(body).encode("utf-8")
|
|
308
|
+
request = urllib.request.Request(
|
|
309
|
+
f"{self.config.base_url}/chat/completions",
|
|
310
|
+
data=encoded,
|
|
311
|
+
headers={
|
|
312
|
+
"Authorization": f"Bearer {api_key}",
|
|
313
|
+
"Content-Type": "application/json",
|
|
314
|
+
},
|
|
315
|
+
method="POST",
|
|
316
|
+
)
|
|
317
|
+
try:
|
|
318
|
+
with urllib.request.urlopen(request, timeout=self.config.timeout_seconds) as response:
|
|
319
|
+
raw = response.read(MAX_REQUEST_BYTES + 1)
|
|
320
|
+
except (OSError, urllib.error.HTTPError, urllib.error.URLError) as exc:
|
|
321
|
+
raise DriverError("BACKEND_CALL_FAILED", "backend completion request failed") from exc
|
|
322
|
+
if len(raw) > MAX_REQUEST_BYTES:
|
|
323
|
+
raise DriverError("BACKEND_RESPONSE_TOO_LARGE", "backend response exceeded its limit")
|
|
324
|
+
root = _mapping(_loads_strict_json(raw.decode("utf-8")), "backend response")
|
|
325
|
+
choices = root.get("choices")
|
|
326
|
+
if (
|
|
327
|
+
not isinstance(choices, Sequence)
|
|
328
|
+
or isinstance(choices, (str, bytes))
|
|
329
|
+
or len(choices) != 1
|
|
330
|
+
):
|
|
331
|
+
raise DriverError("BACKEND_OUTPUT_INVALID", "backend must return exactly one choice")
|
|
332
|
+
message = _mapping(_mapping(choices[0], "backend choice").get("message"), "backend message")
|
|
333
|
+
calls = message.get("tool_calls")
|
|
334
|
+
if not isinstance(calls, Sequence) or isinstance(calls, (str, bytes)) or len(calls) != 1:
|
|
335
|
+
raise DriverError("BACKEND_OUTPUT_INVALID", "backend must return exactly one tool call")
|
|
336
|
+
function = _mapping(
|
|
337
|
+
_mapping(calls[0], "backend tool call").get("function"), "tool function"
|
|
338
|
+
)
|
|
339
|
+
if function.get("name") != tool_name:
|
|
340
|
+
raise DriverError("BACKEND_OUTPUT_INVALID", "backend called a non-forced tool")
|
|
341
|
+
arguments_raw = function.get("arguments")
|
|
342
|
+
if not isinstance(arguments_raw, str):
|
|
343
|
+
raise DriverError("BACKEND_OUTPUT_INVALID", "backend tool arguments must be JSON text")
|
|
344
|
+
arguments = _mapping(_loads_strict_json(arguments_raw), "backend tool arguments")
|
|
345
|
+
usage = _mapping(root.get("usage"), "backend usage")
|
|
346
|
+
return CompletionResult(
|
|
347
|
+
arguments=dict(arguments),
|
|
348
|
+
input_tokens=_nonnegative_int(usage.get("prompt_tokens"), "backend prompt_tokens"),
|
|
349
|
+
output_tokens=_nonnegative_int(
|
|
350
|
+
usage.get("completion_tokens"), "backend completion_tokens"
|
|
351
|
+
),
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
@dataclass(frozen=True, slots=True)
|
|
356
|
+
class Controls:
|
|
357
|
+
skip_first_turn: bool
|
|
358
|
+
max_coding_turns: int
|
|
359
|
+
max_user_turns: int
|
|
360
|
+
max_model_calls: int
|
|
361
|
+
max_tokens: int
|
|
362
|
+
max_cost_usd: float
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
@dataclass(frozen=True, slots=True)
|
|
366
|
+
class ValidatedRequest:
|
|
367
|
+
request_id: str
|
|
368
|
+
state_hash: str
|
|
369
|
+
task_id: str
|
|
370
|
+
task_view: Mapping[str, Any]
|
|
371
|
+
history: tuple[Mapping[str, Any], ...]
|
|
372
|
+
turn_index: int
|
|
373
|
+
clarification_turns: int
|
|
374
|
+
controls: Controls
|
|
375
|
+
seed: int
|
|
376
|
+
dependencies: Mapping[str, Mapping[str, Any]]
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
@dataclass(frozen=True, slots=True)
|
|
380
|
+
class OfficialAssets:
|
|
381
|
+
intent_prompt: str
|
|
382
|
+
intent_prompt_sha256: str
|
|
383
|
+
intent_tool: Mapping[str, Any]
|
|
384
|
+
clarify_tool: Mapping[str, Any]
|
|
385
|
+
intent_tool_schema_hash: str
|
|
386
|
+
clarify_tool_schema_hash: str
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
class UsageTracker:
|
|
390
|
+
def __init__(self, controls: Controls, config: BackendConfig) -> None:
|
|
391
|
+
self.controls = controls
|
|
392
|
+
self.config = config
|
|
393
|
+
self.model_calls = 0
|
|
394
|
+
self.input_tokens = 0
|
|
395
|
+
self.output_tokens = 0
|
|
396
|
+
self.cost_usd = 0.0
|
|
397
|
+
|
|
398
|
+
def complete(
|
|
399
|
+
self,
|
|
400
|
+
backend: CompletionBackend,
|
|
401
|
+
*,
|
|
402
|
+
role: str,
|
|
403
|
+
messages: Sequence[Mapping[str, Any]],
|
|
404
|
+
tool: Mapping[str, Any],
|
|
405
|
+
seed: int,
|
|
406
|
+
) -> Mapping[str, Any]:
|
|
407
|
+
if self.model_calls >= self.controls.max_model_calls:
|
|
408
|
+
raise DriverError(
|
|
409
|
+
"MODEL_CALL_BUDGET_EXCEEDED",
|
|
410
|
+
"Ask-or-Assume reached max_model_calls before the next provider attempt",
|
|
411
|
+
)
|
|
412
|
+
self.model_calls += 1
|
|
413
|
+
result = backend.complete(role=role, messages=messages, tool=tool, seed=seed)
|
|
414
|
+
if not isinstance(result, CompletionResult):
|
|
415
|
+
raise DriverError("BACKEND_OUTPUT_INVALID", "backend returned an invalid completion")
|
|
416
|
+
self.input_tokens += _nonnegative_int(result.input_tokens, "completion input_tokens")
|
|
417
|
+
self.output_tokens += _nonnegative_int(result.output_tokens, "completion output_tokens")
|
|
418
|
+
pricing = self.config.roles[role]
|
|
419
|
+
self.cost_usd += (
|
|
420
|
+
result.input_tokens * pricing.input_cost_per_million
|
|
421
|
+
+ result.output_tokens * pricing.output_cost_per_million
|
|
422
|
+
) / 1_000_000
|
|
423
|
+
if self.input_tokens + self.output_tokens > self.controls.max_tokens:
|
|
424
|
+
raise DriverError("TOKEN_BUDGET_EXCEEDED", "Ask-or-Assume exceeded max_tokens")
|
|
425
|
+
if self.cost_usd > self.controls.max_cost_usd:
|
|
426
|
+
raise DriverError("COST_BUDGET_EXCEEDED", "Ask-or-Assume exceeded max_cost_usd")
|
|
427
|
+
return dict(result.arguments)
|
|
428
|
+
|
|
429
|
+
def as_dict(self) -> dict[str, int | float]:
|
|
430
|
+
return {
|
|
431
|
+
"model_calls": self.model_calls,
|
|
432
|
+
"retrieval_calls": 0,
|
|
433
|
+
"input_tokens": self.input_tokens,
|
|
434
|
+
"output_tokens": self.output_tokens,
|
|
435
|
+
"cost_usd": self.cost_usd,
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
class AskOrAssumeRuntime:
|
|
440
|
+
"""Execute the controlled component harness against one verified checkout."""
|
|
441
|
+
|
|
442
|
+
def __init__(
|
|
443
|
+
self,
|
|
444
|
+
checkout: Path,
|
|
445
|
+
backend_config: BackendConfig,
|
|
446
|
+
backend: CompletionBackend,
|
|
447
|
+
*,
|
|
448
|
+
policy: CompatibilityPolicy | None = None,
|
|
449
|
+
) -> None:
|
|
450
|
+
self.policy = policy or load_compatibility_policy()
|
|
451
|
+
self.checkout = verify_checkout(checkout, self.policy)
|
|
452
|
+
self.assets = load_official_assets(self.checkout)
|
|
453
|
+
self.backend_config = backend_config
|
|
454
|
+
self.backend = backend
|
|
455
|
+
for role in _ROLE_NAMES:
|
|
456
|
+
if _canonical_json(backend.fingerprint(role)) != _canonical_json(
|
|
457
|
+
backend_config.roles[role].fingerprint
|
|
458
|
+
):
|
|
459
|
+
raise DriverError(
|
|
460
|
+
"BACKEND_IDENTITY_MISMATCH",
|
|
461
|
+
f"runtime {role} backend differs from startup configuration",
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
def run(self, payload: Mapping[str, Any]) -> dict[str, Any]:
|
|
465
|
+
request = validate_request(payload, self.backend_config, self.assets)
|
|
466
|
+
controls = request.controls
|
|
467
|
+
if request.turn_index >= controls.max_coding_turns:
|
|
468
|
+
raise DriverError(
|
|
469
|
+
"CODING_TURN_BUDGET_EXCEEDED",
|
|
470
|
+
"Ask-or-Assume effective coding-turn budget is exhausted",
|
|
471
|
+
)
|
|
472
|
+
if controls.skip_first_turn and request.turn_index == 0:
|
|
473
|
+
raise DriverError(
|
|
474
|
+
"FIRST_TURN_NOT_DELEGATED",
|
|
475
|
+
"Ask-or-Assume IntentAgent must not run before the first coding turn",
|
|
476
|
+
)
|
|
477
|
+
tracker = UsageTracker(controls, self.backend_config)
|
|
478
|
+
history_document = _canonical_json(
|
|
479
|
+
{
|
|
480
|
+
"task_id": request.task_id,
|
|
481
|
+
"task_view": request.task_view,
|
|
482
|
+
"conversation_history": request.history,
|
|
483
|
+
"effective_coding_turns": request.turn_index,
|
|
484
|
+
"effective_user_turns": request.clarification_turns,
|
|
485
|
+
}
|
|
486
|
+
)
|
|
487
|
+
intent_arguments = tracker.complete(
|
|
488
|
+
self.backend,
|
|
489
|
+
role="intent",
|
|
490
|
+
messages=(
|
|
491
|
+
{"role": "system", "content": self.assets.intent_prompt},
|
|
492
|
+
{"role": "user", "content": history_document},
|
|
493
|
+
),
|
|
494
|
+
tool=self.assets.intent_tool,
|
|
495
|
+
seed=request.seed,
|
|
496
|
+
)
|
|
497
|
+
decision = _validate_intent_decision(intent_arguments)
|
|
498
|
+
needs_clarification = decision["needs_clarification"]
|
|
499
|
+
reasons = decision["reasons"]
|
|
500
|
+
message = decision["message"]
|
|
501
|
+
questions: tuple[Mapping[str, Any], ...] = ()
|
|
502
|
+
clarification_message = ""
|
|
503
|
+
trace: list[dict[str, Any]] = [
|
|
504
|
+
{
|
|
505
|
+
"index": 0,
|
|
506
|
+
"path": "intent_decision",
|
|
507
|
+
"role": "intent",
|
|
508
|
+
"tool_name": _tool_name(self.assets.intent_tool),
|
|
509
|
+
"decision": needs_clarification,
|
|
510
|
+
"reasons": reasons,
|
|
511
|
+
"message": message,
|
|
512
|
+
}
|
|
513
|
+
]
|
|
514
|
+
if needs_clarification:
|
|
515
|
+
if request.clarification_turns >= controls.max_user_turns:
|
|
516
|
+
raise DriverError(
|
|
517
|
+
"USER_TURN_BUDGET_EXCEEDED",
|
|
518
|
+
"Ask-or-Assume effective user-turn budget is exhausted",
|
|
519
|
+
)
|
|
520
|
+
reminder = "Before proceeding further, ask a clarifying question."
|
|
521
|
+
if reasons:
|
|
522
|
+
reminder += f"\nReason: {reasons.strip()}"
|
|
523
|
+
clarify_arguments = tracker.complete(
|
|
524
|
+
self.backend,
|
|
525
|
+
role="coding",
|
|
526
|
+
messages=(
|
|
527
|
+
{"role": "user", "content": history_document},
|
|
528
|
+
{"role": "system", "content": reminder},
|
|
529
|
+
),
|
|
530
|
+
tool=self.assets.clarify_tool,
|
|
531
|
+
seed=request.seed + 1,
|
|
532
|
+
)
|
|
533
|
+
clarify = _validate_clarify_call(clarify_arguments)
|
|
534
|
+
questions = clarify["questions"]
|
|
535
|
+
clarification_message = _render_official_clarify_message(clarify["message"], questions)
|
|
536
|
+
trace.append(
|
|
537
|
+
{
|
|
538
|
+
"index": 1,
|
|
539
|
+
"path": "forced_clarify",
|
|
540
|
+
"role": "coding",
|
|
541
|
+
"tool_name": _tool_name(self.assets.clarify_tool),
|
|
542
|
+
"message": clarify["message"],
|
|
543
|
+
"questions": questions,
|
|
544
|
+
"wait_for_response": clarify["wait"],
|
|
545
|
+
"rendered_message": clarification_message,
|
|
546
|
+
}
|
|
547
|
+
)
|
|
548
|
+
return {
|
|
549
|
+
"status": "ok",
|
|
550
|
+
"request_id": request.request_id,
|
|
551
|
+
"upstream_commit": UPSTREAM_COMMIT,
|
|
552
|
+
"protocol_version": PROTOCOL_VERSION,
|
|
553
|
+
"payload": {
|
|
554
|
+
"needs_clarification": needs_clarification,
|
|
555
|
+
"reasons": [reasons] if reasons else [],
|
|
556
|
+
"message": message,
|
|
557
|
+
"clarification_question": clarification_message,
|
|
558
|
+
"clarification_questions": list(questions),
|
|
559
|
+
"intent_summary": message,
|
|
560
|
+
"transition": (
|
|
561
|
+
"await"
|
|
562
|
+
if needs_clarification and clarify["wait"]
|
|
563
|
+
else "force"
|
|
564
|
+
if needs_clarification
|
|
565
|
+
else "resume"
|
|
566
|
+
),
|
|
567
|
+
"delegated": True,
|
|
568
|
+
"forced": needs_clarification,
|
|
569
|
+
"first_turn_skipped": False,
|
|
570
|
+
"decision_trace": trace,
|
|
571
|
+
"execution_mode": "controlled_component_harness",
|
|
572
|
+
"component_only": True,
|
|
573
|
+
"openhands_full_system": False,
|
|
574
|
+
"ua_multi_parity_claimed": False,
|
|
575
|
+
"intent_prompt_sha256": self.assets.intent_prompt_sha256,
|
|
576
|
+
"intent_tool_schema_hash": self.assets.intent_tool_schema_hash,
|
|
577
|
+
"clarify_tool_schema_hash": self.assets.clarify_tool_schema_hash,
|
|
578
|
+
"effective_coding_turns": request.turn_index,
|
|
579
|
+
"effective_user_turns": request.clarification_turns,
|
|
580
|
+
"model_fingerprints": {
|
|
581
|
+
role: dict(self.backend_config.roles[role].fingerprint) for role in _ROLE_NAMES
|
|
582
|
+
},
|
|
583
|
+
},
|
|
584
|
+
"usage": tracker.as_dict(),
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def load_compatibility_policy(path: Path = DEFAULT_MANIFEST) -> CompatibilityPolicy:
|
|
589
|
+
try:
|
|
590
|
+
value = _loads_strict_json(path.read_text(encoding="utf-8"))
|
|
591
|
+
except OSError as exc:
|
|
592
|
+
raise DriverError("MANIFEST_UNAVAILABLE", "compatibility manifest is unavailable") from exc
|
|
593
|
+
return CompatibilityPolicy.from_mapping(_mapping(value, "compatibility manifest"))
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def verify_checkout(checkout: Path, policy: CompatibilityPolicy) -> Path:
|
|
597
|
+
try:
|
|
598
|
+
root = checkout.expanduser().resolve(strict=True)
|
|
599
|
+
except OSError as exc:
|
|
600
|
+
raise DriverError("CHECKOUT_UNAVAILABLE", "Ask-or-Assume checkout is unavailable") from exc
|
|
601
|
+
if not root.is_dir():
|
|
602
|
+
raise DriverError("CHECKOUT_UNAVAILABLE", "Ask-or-Assume checkout must be a directory")
|
|
603
|
+
if read_checkout_head(root) != policy.commit:
|
|
604
|
+
raise DriverError("UPSTREAM_REVISION_MISMATCH", "checkout is not at the pinned commit")
|
|
605
|
+
license_path = _checkout_file(root, policy.license_path)
|
|
606
|
+
if _file_sha256(license_path) != policy.license_sha256:
|
|
607
|
+
raise DriverError("UPSTREAM_LICENSE_MISMATCH", "root license hash does not match")
|
|
608
|
+
license_text = license_path.read_text(encoding="utf-8", errors="strict")
|
|
609
|
+
if "MIT License" not in license_text or "enterprise/" not in license_text:
|
|
610
|
+
raise DriverError(
|
|
611
|
+
"UPSTREAM_LICENSE_MISMATCH",
|
|
612
|
+
"root license must identify MIT content and the separate enterprise subtree",
|
|
613
|
+
)
|
|
614
|
+
verified_sources: list[Path] = []
|
|
615
|
+
for relative, expected_hash in policy.source_hashes.items():
|
|
616
|
+
_reject_excluded_path(relative, policy.excluded_prefixes)
|
|
617
|
+
source = _checkout_file(root, relative)
|
|
618
|
+
if _file_sha256(source) != expected_hash:
|
|
619
|
+
raise DriverError("UPSTREAM_SOURCE_MISMATCH", f"source hash mismatch: {relative}")
|
|
620
|
+
verified_sources.append(source)
|
|
621
|
+
_reject_enterprise_imports(verified_sources)
|
|
622
|
+
if policy.require_clean_checkout:
|
|
623
|
+
_require_clean_checkout(root)
|
|
624
|
+
return root
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def load_official_assets(checkout: Path) -> OfficialAssets:
|
|
628
|
+
root = checkout.resolve(strict=True)
|
|
629
|
+
prompt_path = _checkout_file(root, INTENT_PROMPT_PATH)
|
|
630
|
+
prompt = prompt_path.read_text(encoding="utf-8", errors="strict")
|
|
631
|
+
names = _literal_assignments(_checkout_file(root, TOOL_NAMES_PATH), {})
|
|
632
|
+
intent_values = _literal_assignments(_checkout_file(root, INTENT_TOOL_PATH), names)
|
|
633
|
+
clarify_values = _literal_assignments(_checkout_file(root, CLARIFY_TOOL_PATH), names)
|
|
634
|
+
intent_tool = _mapping(intent_values.get("ClarifyDecisionTool"), "ClarifyDecisionTool")
|
|
635
|
+
clarify_tool = _mapping(clarify_values.get("ClarifyTool"), "ClarifyTool")
|
|
636
|
+
if _tool_name(intent_tool) != "clarify_decision" or _tool_name(clarify_tool) != "clarify":
|
|
637
|
+
raise DriverError("UPSTREAM_SCHEMA_INVALID", "official tool names are invalid")
|
|
638
|
+
_validate_tool_schema(intent_tool)
|
|
639
|
+
_validate_tool_schema(clarify_tool)
|
|
640
|
+
return OfficialAssets(
|
|
641
|
+
intent_prompt=prompt,
|
|
642
|
+
intent_prompt_sha256=hashlib.sha256(prompt.encode("utf-8")).hexdigest(),
|
|
643
|
+
intent_tool=dict(intent_tool),
|
|
644
|
+
clarify_tool=dict(clarify_tool),
|
|
645
|
+
intent_tool_schema_hash=_canonical_hash(intent_tool),
|
|
646
|
+
clarify_tool_schema_hash=_canonical_hash(clarify_tool),
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
def validate_request(
|
|
651
|
+
value: Mapping[str, Any], config: BackendConfig, assets: OfficialAssets
|
|
652
|
+
) -> ValidatedRequest:
|
|
653
|
+
_exact_keys(value, set(_ROOT_FIELDS), "worker request")
|
|
654
|
+
if value["method_id"] != METHOD_ID or value["release_id"] != RELEASE_ID:
|
|
655
|
+
raise DriverError("PROFILE_IDENTITY_MISMATCH", "worker request targets another profile")
|
|
656
|
+
if value["upstream_commit"] != UPSTREAM_COMMIT:
|
|
657
|
+
raise DriverError("UPSTREAM_REVISION_MISMATCH", "worker request targets another revision")
|
|
658
|
+
if value["protocol_version"] != PROTOCOL_VERSION:
|
|
659
|
+
raise DriverError("PROTOCOL_MISMATCH", "worker protocol version mismatch")
|
|
660
|
+
request_id = _hash(value["request_id"], "worker request_id")
|
|
661
|
+
state_hash = _hash(value["state_hash"], "worker state_hash")
|
|
662
|
+
identity = {key: value[key] for key in _ROOT_FIELDS if key != "request_id"}
|
|
663
|
+
if request_id != _canonical_hash(identity):
|
|
664
|
+
raise DriverError("REQUEST_ID_MISMATCH", "worker request_id is invalid")
|
|
665
|
+
reservation = _mapping(value["minimum_reservation"], "minimum_reservation")
|
|
666
|
+
if reservation != {"model_calls": 1}:
|
|
667
|
+
raise DriverError("INVALID_RESERVATION", "worker must reserve one IntentAgent model call")
|
|
668
|
+
dependencies = _mapping(value["dependency_fingerprints"], "dependency_fingerprints")
|
|
669
|
+
if set(dependencies) != {"intent_model", "coding_model"}:
|
|
670
|
+
raise DriverError("BACKEND_IDENTITY_MISMATCH", "request must bind both model roles")
|
|
671
|
+
normalized_dependencies: dict[str, Mapping[str, Any]] = {}
|
|
672
|
+
for role, key in (("intent", "intent_model"), ("coding", "coding_model")):
|
|
673
|
+
fingerprint = _mapping(dependencies[key], f"{key} fingerprint")
|
|
674
|
+
if _canonical_json(fingerprint) != _canonical_json(config.roles[role].fingerprint):
|
|
675
|
+
raise DriverError(
|
|
676
|
+
"BACKEND_IDENTITY_MISMATCH", f"request {key} differs from worker startup"
|
|
677
|
+
)
|
|
678
|
+
normalized_dependencies[key] = dict(fingerprint)
|
|
679
|
+
payload = _mapping(value["payload"], "worker payload")
|
|
680
|
+
_exact_keys(payload, set(_PAYLOAD_FIELDS), "worker payload")
|
|
681
|
+
if payload["intent_prompt_hash"] != assets.intent_prompt_sha256:
|
|
682
|
+
raise DriverError("PROMPT_IDENTITY_MISMATCH", "request intent prompt hash is not pinned")
|
|
683
|
+
task_id = payload["task_id"]
|
|
684
|
+
if not isinstance(task_id, str):
|
|
685
|
+
raise DriverError("INVALID_SCHEMA", "task_id must be text")
|
|
686
|
+
task_view = _validate_task_view(payload["task_view"])
|
|
687
|
+
history = _validate_history(payload["history"])
|
|
688
|
+
turn_index = _nonnegative_int(payload["turn_index"], "effective coding turns")
|
|
689
|
+
clarification_turns = _nonnegative_int(payload["clarification_turns"], "effective user turns")
|
|
690
|
+
controls = _validate_controls(payload["controls"])
|
|
691
|
+
if clarification_turns > controls.max_user_turns:
|
|
692
|
+
raise DriverError("USER_TURN_BUDGET_EXCEEDED", "effective user turns exceed the budget")
|
|
693
|
+
return ValidatedRequest(
|
|
694
|
+
request_id=request_id,
|
|
695
|
+
state_hash=state_hash,
|
|
696
|
+
task_id=task_id,
|
|
697
|
+
task_view=task_view,
|
|
698
|
+
history=history,
|
|
699
|
+
turn_index=turn_index,
|
|
700
|
+
clarification_turns=clarification_turns,
|
|
701
|
+
controls=controls,
|
|
702
|
+
seed=_nonnegative_int(payload["seed"], "worker seed"),
|
|
703
|
+
dependencies=normalized_dependencies,
|
|
704
|
+
)
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
def _validate_task_view(value: Any) -> Mapping[str, Any]:
|
|
708
|
+
task = _mapping(value, "task_view")
|
|
709
|
+
_exact_keys(task, {"query", "instruction", "context", "tool_names"}, "task_view")
|
|
710
|
+
query = _plain_text(task["query"], "task query")
|
|
711
|
+
instruction = _plain_text(task["instruction"], "task instruction")
|
|
712
|
+
if not query.strip() and not instruction.strip():
|
|
713
|
+
raise DriverError("INVALID_SCHEMA", "task view requires query or instruction")
|
|
714
|
+
context = task["context"]
|
|
715
|
+
tool_names = task["tool_names"]
|
|
716
|
+
_canonical_json(context)
|
|
717
|
+
if not isinstance(tool_names, Sequence) or isinstance(tool_names, (str, bytes)):
|
|
718
|
+
raise DriverError("INVALID_SCHEMA", "task tool_names must be a sequence")
|
|
719
|
+
if not all(isinstance(item, str) and item for item in tool_names):
|
|
720
|
+
raise DriverError("INVALID_SCHEMA", "task tool_names must contain non-empty text")
|
|
721
|
+
normalized = {
|
|
722
|
+
"query": query,
|
|
723
|
+
"instruction": instruction,
|
|
724
|
+
"context": context,
|
|
725
|
+
"tool_names": list(tool_names),
|
|
726
|
+
}
|
|
727
|
+
if len(_canonical_json(normalized)) > MAX_TEXT_CHARS:
|
|
728
|
+
raise DriverError("REQUEST_TOO_LARGE", "task view exceeds its text limit")
|
|
729
|
+
return normalized
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
def _validate_history(value: Any) -> tuple[Mapping[str, Any], ...]:
|
|
733
|
+
if not isinstance(value, Sequence) or isinstance(value, (str, bytes)):
|
|
734
|
+
raise DriverError("INVALID_SCHEMA", "history must be a sequence")
|
|
735
|
+
if len(value) > MAX_HISTORY_ITEMS:
|
|
736
|
+
raise DriverError("REQUEST_TOO_LARGE", "history contains too many messages")
|
|
737
|
+
result: list[Mapping[str, Any]] = []
|
|
738
|
+
for index, raw in enumerate(value):
|
|
739
|
+
item = _mapping(raw, f"history[{index}]")
|
|
740
|
+
_exact_keys(item, {"role", "content", "name"}, f"history[{index}]")
|
|
741
|
+
role = _text(item["role"], f"history[{index}] role")
|
|
742
|
+
content = _plain_text(item["content"], f"history[{index}] content")
|
|
743
|
+
name = item["name"]
|
|
744
|
+
if name is not None and not isinstance(name, str):
|
|
745
|
+
raise DriverError("INVALID_SCHEMA", f"history[{index}] name must be text or null")
|
|
746
|
+
result.append({"role": role, "content": content, "name": name})
|
|
747
|
+
return tuple(result)
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
def _validate_controls(value: Any) -> Controls:
|
|
751
|
+
item = _mapping(value, "controls")
|
|
752
|
+
_exact_keys(item, set(_CONTROL_FIELDS), "controls")
|
|
753
|
+
skip = item["skip_first_turn"]
|
|
754
|
+
if not isinstance(skip, bool):
|
|
755
|
+
raise DriverError("INVALID_SCHEMA", "skip_first_turn must be boolean")
|
|
756
|
+
return Controls(
|
|
757
|
+
skip_first_turn=skip,
|
|
758
|
+
max_coding_turns=_positive_int(item["max_coding_turns"], "max_coding_turns"),
|
|
759
|
+
max_user_turns=_positive_int(item["max_user_turns"], "max_user_turns"),
|
|
760
|
+
max_model_calls=_positive_int(item["max_model_calls"], "max_model_calls"),
|
|
761
|
+
max_tokens=_positive_int(item["max_tokens"], "max_tokens"),
|
|
762
|
+
max_cost_usd=_nonnegative_float(item["max_cost_usd"], "max_cost_usd"),
|
|
763
|
+
)
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
def _validate_intent_decision(value: Mapping[str, Any]) -> dict[str, Any]:
|
|
767
|
+
if set(value) - {"needs_clarification", "message", "reasons"}:
|
|
768
|
+
raise DriverError("BACKEND_OUTPUT_INVALID", "clarify_decision returned unknown fields")
|
|
769
|
+
decision = value.get("needs_clarification")
|
|
770
|
+
if not isinstance(decision, bool):
|
|
771
|
+
raise DriverError("BACKEND_OUTPUT_INVALID", "needs_clarification must be boolean")
|
|
772
|
+
message = value.get("message", "")
|
|
773
|
+
reasons = value.get("reasons", "")
|
|
774
|
+
if not isinstance(message, str) or not isinstance(reasons, str):
|
|
775
|
+
raise DriverError("BACKEND_OUTPUT_INVALID", "decision message and reasons must be text")
|
|
776
|
+
if decision and not reasons.strip():
|
|
777
|
+
raise DriverError("BACKEND_OUTPUT_INVALID", "clarification decision requires reasons")
|
|
778
|
+
return {
|
|
779
|
+
"needs_clarification": decision,
|
|
780
|
+
"message": message.strip(),
|
|
781
|
+
"reasons": reasons.strip(),
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
def _validate_clarify_call(value: Mapping[str, Any]) -> dict[str, Any]:
|
|
786
|
+
if set(value) - {"message", "questions", "wait_for_response"}:
|
|
787
|
+
raise DriverError("BACKEND_OUTPUT_INVALID", "clarify call returned unknown fields")
|
|
788
|
+
message = value.get("message", "")
|
|
789
|
+
if not isinstance(message, str):
|
|
790
|
+
raise DriverError("BACKEND_OUTPUT_INVALID", "clarify message must be text")
|
|
791
|
+
wait = value.get("wait_for_response", True)
|
|
792
|
+
if not isinstance(wait, bool):
|
|
793
|
+
raise DriverError("BACKEND_OUTPUT_INVALID", "wait_for_response must be boolean")
|
|
794
|
+
raw_questions = value.get("questions")
|
|
795
|
+
if not isinstance(raw_questions, Sequence) or isinstance(raw_questions, (str, bytes)):
|
|
796
|
+
raise DriverError("BACKEND_OUTPUT_INVALID", "clarify questions must be a sequence")
|
|
797
|
+
if not raw_questions:
|
|
798
|
+
raise DriverError("BACKEND_OUTPUT_INVALID", "clarify requires at least one question")
|
|
799
|
+
questions: list[Mapping[str, Any]] = []
|
|
800
|
+
for index, raw in enumerate(raw_questions):
|
|
801
|
+
item = _mapping(raw, f"clarify question {index}")
|
|
802
|
+
if set(item) - {"id", "text", "options", "default"}:
|
|
803
|
+
raise DriverError("BACKEND_OUTPUT_INVALID", "clarify question has unknown fields")
|
|
804
|
+
question_id = _text(item.get("id"), f"clarify question {index} id")
|
|
805
|
+
text = _text(item.get("text"), f"clarify question {index} text")
|
|
806
|
+
options = item.get("options", [])
|
|
807
|
+
if not isinstance(options, Sequence) or isinstance(options, (str, bytes)):
|
|
808
|
+
raise DriverError("BACKEND_OUTPUT_INVALID", "clarify options must be a sequence")
|
|
809
|
+
if not all(isinstance(option, str) and option.strip() for option in options):
|
|
810
|
+
raise DriverError("BACKEND_OUTPUT_INVALID", "clarify options must be non-empty text")
|
|
811
|
+
default = item.get("default", "")
|
|
812
|
+
if not isinstance(default, str):
|
|
813
|
+
raise DriverError("BACKEND_OUTPUT_INVALID", "clarify default must be text")
|
|
814
|
+
questions.append(
|
|
815
|
+
{"id": question_id, "text": text, "options": list(options), "default": default}
|
|
816
|
+
)
|
|
817
|
+
return {"message": message.strip(), "questions": tuple(questions), "wait": wait}
|
|
818
|
+
|
|
819
|
+
|
|
820
|
+
def _render_official_clarify_message(preamble: str, questions: Sequence[Mapping[str, Any]]) -> str:
|
|
821
|
+
"""Mirror the pinned MIT ``clarify_agent.function_calling`` renderer."""
|
|
822
|
+
|
|
823
|
+
lines = [f"{preamble}\n" if preamble else "I need some clarification before proceeding:\n"]
|
|
824
|
+
if questions:
|
|
825
|
+
lines.append("**Please answer the following:**\n")
|
|
826
|
+
for index, question in enumerate(questions, 1):
|
|
827
|
+
lines.append(f"Q{index}. {question['text']}")
|
|
828
|
+
lines.append("")
|
|
829
|
+
return "\n".join(lines)
|
|
830
|
+
|
|
831
|
+
|
|
832
|
+
def _literal_assignments(path: Path, initial: Mapping[str, Any]) -> dict[str, Any]:
|
|
833
|
+
try:
|
|
834
|
+
tree = ast.parse(path.read_text(encoding="utf-8"), filename=path.name)
|
|
835
|
+
except (OSError, SyntaxError, UnicodeError) as exc:
|
|
836
|
+
raise DriverError("UPSTREAM_SCHEMA_INVALID", "official schema source is invalid") from exc
|
|
837
|
+
environment = dict(initial)
|
|
838
|
+
for node in tree.body:
|
|
839
|
+
if isinstance(node, ast.Assign) and len(node.targets) == 1:
|
|
840
|
+
target = node.targets[0]
|
|
841
|
+
if not isinstance(target, ast.Name):
|
|
842
|
+
continue
|
|
843
|
+
try:
|
|
844
|
+
environment[target.id] = _safe_ast_value(node.value, environment)
|
|
845
|
+
except DriverError:
|
|
846
|
+
if target.id in {"ClarifyDecisionTool", "ClarifyTool"}:
|
|
847
|
+
raise
|
|
848
|
+
return environment
|
|
849
|
+
|
|
850
|
+
|
|
851
|
+
def _safe_ast_value(node: ast.AST, environment: Mapping[str, Any]) -> Any:
|
|
852
|
+
if isinstance(node, ast.Constant):
|
|
853
|
+
return node.value
|
|
854
|
+
if isinstance(node, ast.Name) and node.id in environment:
|
|
855
|
+
return environment[node.id]
|
|
856
|
+
if isinstance(node, ast.Dict):
|
|
857
|
+
return {
|
|
858
|
+
_safe_ast_value(key, environment): _safe_ast_value(value, environment)
|
|
859
|
+
for key, value in zip(node.keys, node.values, strict=True)
|
|
860
|
+
}
|
|
861
|
+
if isinstance(node, (ast.List, ast.Tuple)):
|
|
862
|
+
return [_safe_ast_value(item, environment) for item in node.elts]
|
|
863
|
+
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
|
|
864
|
+
if node.func.id not in {"ChatCompletionToolParam", "ChatCompletionToolParamFunctionChunk"}:
|
|
865
|
+
raise DriverError("UPSTREAM_SCHEMA_INVALID", "official schema uses an unsafe call")
|
|
866
|
+
if node.args or any(keyword.arg is None for keyword in node.keywords):
|
|
867
|
+
raise DriverError(
|
|
868
|
+
"UPSTREAM_SCHEMA_INVALID", "official schema call shape is unsupported"
|
|
869
|
+
)
|
|
870
|
+
return {
|
|
871
|
+
str(keyword.arg): _safe_ast_value(keyword.value, environment)
|
|
872
|
+
for keyword in node.keywords
|
|
873
|
+
}
|
|
874
|
+
raise DriverError("UPSTREAM_SCHEMA_INVALID", "official schema contains unsupported syntax")
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
def _validate_tool_schema(value: Mapping[str, Any]) -> None:
|
|
878
|
+
_exact_keys(value, {"type", "function"}, "official tool")
|
|
879
|
+
if value["type"] != "function":
|
|
880
|
+
raise DriverError("UPSTREAM_SCHEMA_INVALID", "official tool type is invalid")
|
|
881
|
+
function = _mapping(value["function"], "official tool function")
|
|
882
|
+
_exact_keys(function, {"name", "description", "parameters"}, "official tool function")
|
|
883
|
+
parameters = _mapping(function["parameters"], "official tool parameters")
|
|
884
|
+
if parameters.get("type") != "object" or parameters.get("additionalProperties") is not False:
|
|
885
|
+
raise DriverError("UPSTREAM_SCHEMA_INVALID", "official tool schema is not closed")
|
|
886
|
+
|
|
887
|
+
|
|
888
|
+
def _tool_name(value: Mapping[str, Any]) -> str:
|
|
889
|
+
function = _mapping(value.get("function"), "tool function")
|
|
890
|
+
return _text(function.get("name"), "tool name")
|
|
891
|
+
|
|
892
|
+
|
|
893
|
+
def read_checkout_head(root: Path) -> str:
|
|
894
|
+
git_entry = root / ".git"
|
|
895
|
+
if git_entry.is_dir():
|
|
896
|
+
git_dir = git_entry.resolve(strict=True)
|
|
897
|
+
elif git_entry.is_file():
|
|
898
|
+
raw = git_entry.read_text(encoding="utf-8").strip()
|
|
899
|
+
if not raw.startswith("gitdir: "):
|
|
900
|
+
raise DriverError("GIT_METADATA_INVALID", "checkout .git file is malformed")
|
|
901
|
+
candidate = Path(raw.removeprefix("gitdir: "))
|
|
902
|
+
git_dir = (
|
|
903
|
+
(root / candidate).resolve(strict=True)
|
|
904
|
+
if not candidate.is_absolute()
|
|
905
|
+
else candidate.resolve(strict=True)
|
|
906
|
+
)
|
|
907
|
+
else:
|
|
908
|
+
raise DriverError("GIT_METADATA_INVALID", "checkout lacks Git metadata")
|
|
909
|
+
head = (git_dir / "HEAD").read_text(encoding="utf-8").strip()
|
|
910
|
+
if _COMMIT_RE.fullmatch(head):
|
|
911
|
+
return head
|
|
912
|
+
if not head.startswith("ref: "):
|
|
913
|
+
raise DriverError("GIT_METADATA_INVALID", "checkout HEAD is malformed")
|
|
914
|
+
ref = head.removeprefix("ref: ")
|
|
915
|
+
if _REF_RE.fullmatch(ref) is None or ".." in PurePosixPath(ref).parts:
|
|
916
|
+
raise DriverError("GIT_METADATA_INVALID", "checkout HEAD reference is unsafe")
|
|
917
|
+
common_dir = git_dir
|
|
918
|
+
common_file = git_dir / "commondir"
|
|
919
|
+
if common_file.is_file():
|
|
920
|
+
candidate = Path(common_file.read_text(encoding="utf-8").strip())
|
|
921
|
+
common_dir = (
|
|
922
|
+
(git_dir / candidate).resolve(strict=True)
|
|
923
|
+
if not candidate.is_absolute()
|
|
924
|
+
else candidate.resolve(strict=True)
|
|
925
|
+
)
|
|
926
|
+
for base in (git_dir, common_dir):
|
|
927
|
+
loose = _safe_child(base, ref)
|
|
928
|
+
if loose.is_file():
|
|
929
|
+
value = loose.read_text(encoding="utf-8").strip()
|
|
930
|
+
if _COMMIT_RE.fullmatch(value):
|
|
931
|
+
return value
|
|
932
|
+
packed = base / "packed-refs"
|
|
933
|
+
if packed.is_file():
|
|
934
|
+
for line in packed.read_text(encoding="utf-8").splitlines():
|
|
935
|
+
if not line or line.startswith(("#", "^")):
|
|
936
|
+
continue
|
|
937
|
+
commit, separator, name = line.partition(" ")
|
|
938
|
+
if separator and name == ref and _COMMIT_RE.fullmatch(commit):
|
|
939
|
+
return commit
|
|
940
|
+
raise DriverError("GIT_METADATA_INVALID", "checkout HEAD commit cannot be resolved")
|
|
941
|
+
|
|
942
|
+
|
|
943
|
+
def _require_clean_checkout(root: Path) -> None:
|
|
944
|
+
environment = {
|
|
945
|
+
"PATH": os.environ.get("PATH", ""),
|
|
946
|
+
"LC_ALL": "C",
|
|
947
|
+
"GIT_CONFIG_NOSYSTEM": "1",
|
|
948
|
+
"GIT_CONFIG_GLOBAL": os.devnull,
|
|
949
|
+
"GIT_OPTIONAL_LOCKS": "0",
|
|
950
|
+
}
|
|
951
|
+
try:
|
|
952
|
+
result = subprocess.run(
|
|
953
|
+
(
|
|
954
|
+
"git",
|
|
955
|
+
"-c",
|
|
956
|
+
"core.fsmonitor=false",
|
|
957
|
+
"-C",
|
|
958
|
+
str(root),
|
|
959
|
+
"status",
|
|
960
|
+
"--porcelain=v1",
|
|
961
|
+
"--untracked-files=all",
|
|
962
|
+
),
|
|
963
|
+
check=False,
|
|
964
|
+
capture_output=True,
|
|
965
|
+
text=True,
|
|
966
|
+
encoding="utf-8",
|
|
967
|
+
errors="replace",
|
|
968
|
+
env=environment,
|
|
969
|
+
timeout=15,
|
|
970
|
+
)
|
|
971
|
+
except (OSError, subprocess.SubprocessError) as exc:
|
|
972
|
+
raise DriverError("GIT_STATUS_FAILED", "clean-checkout verification could not run") from exc
|
|
973
|
+
if result.returncode != 0:
|
|
974
|
+
raise DriverError("GIT_STATUS_FAILED", "clean-checkout verification failed")
|
|
975
|
+
if result.stdout:
|
|
976
|
+
raise DriverError("DIRTY_CHECKOUT", "checkout contains tracked or untracked changes")
|
|
977
|
+
|
|
978
|
+
|
|
979
|
+
def _reject_enterprise_imports(paths: Sequence[Path]) -> None:
|
|
980
|
+
for path in paths:
|
|
981
|
+
if path.suffix != ".py":
|
|
982
|
+
continue
|
|
983
|
+
try:
|
|
984
|
+
tree = ast.parse(path.read_text(encoding="utf-8"), filename=path.name)
|
|
985
|
+
except (OSError, SyntaxError, UnicodeError) as exc:
|
|
986
|
+
raise DriverError("UPSTREAM_SOURCE_INVALID", "pinned source cannot be parsed") from exc
|
|
987
|
+
for node in ast.walk(tree):
|
|
988
|
+
names: tuple[str, ...] = ()
|
|
989
|
+
if isinstance(node, ast.Import):
|
|
990
|
+
names = tuple(alias.name for alias in node.names)
|
|
991
|
+
elif isinstance(node, ast.ImportFrom) and node.module:
|
|
992
|
+
names = (node.module,)
|
|
993
|
+
if any(name == "enterprise" or name.startswith("enterprise.") for name in names):
|
|
994
|
+
raise DriverError("ENTERPRISE_EXCLUDED", "worker refuses enterprise imports")
|
|
995
|
+
|
|
996
|
+
|
|
997
|
+
def serve(runtime: AskOrAssumeRuntime, input_stream: TextIO, output_stream: TextIO) -> None:
|
|
998
|
+
for raw_line in input_stream:
|
|
999
|
+
if not raw_line.strip():
|
|
1000
|
+
continue
|
|
1001
|
+
request_id = ""
|
|
1002
|
+
try:
|
|
1003
|
+
if len(raw_line.encode("utf-8")) > MAX_REQUEST_BYTES:
|
|
1004
|
+
raise DriverError("REQUEST_TOO_LARGE", "worker request exceeds its byte limit")
|
|
1005
|
+
value = _mapping(_loads_strict_json(raw_line), "JSONL request")
|
|
1006
|
+
if isinstance(value.get("request_id"), str):
|
|
1007
|
+
request_id = value["request_id"][:128]
|
|
1008
|
+
response = runtime.run(value)
|
|
1009
|
+
except DriverError as exc:
|
|
1010
|
+
response = _failure_response(request_id, exc.code, str(exc))
|
|
1011
|
+
except Exception as exc: # pragma: no cover - final process boundary
|
|
1012
|
+
response = _failure_response(
|
|
1013
|
+
request_id,
|
|
1014
|
+
"INTERNAL_WORKER_ERROR",
|
|
1015
|
+
f"Ask-or-Assume worker failed with {type(exc).__name__}",
|
|
1016
|
+
)
|
|
1017
|
+
output_stream.write(_canonical_json(response) + "\n")
|
|
1018
|
+
output_stream.flush()
|
|
1019
|
+
|
|
1020
|
+
|
|
1021
|
+
def _failure_response(request_id: str, code: str, message: str) -> dict[str, Any]:
|
|
1022
|
+
return {
|
|
1023
|
+
"status": "error",
|
|
1024
|
+
"request_id": request_id,
|
|
1025
|
+
"upstream_commit": UPSTREAM_COMMIT,
|
|
1026
|
+
"protocol_version": PROTOCOL_VERSION,
|
|
1027
|
+
"failure_code": code,
|
|
1028
|
+
"message": message,
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
|
|
1032
|
+
def _checkout_file(root: Path, relative: str) -> Path:
|
|
1033
|
+
path = _safe_child(root, relative)
|
|
1034
|
+
if not path.is_file() or path.is_symlink():
|
|
1035
|
+
raise DriverError("UPSTREAM_FILE_UNAVAILABLE", f"pinned file is unavailable: {relative}")
|
|
1036
|
+
return path
|
|
1037
|
+
|
|
1038
|
+
|
|
1039
|
+
def _safe_child(root: Path, relative: str) -> Path:
|
|
1040
|
+
candidate = (root / relative).resolve(strict=False)
|
|
1041
|
+
if candidate != root and root not in candidate.parents:
|
|
1042
|
+
raise DriverError("UNSAFE_PATH", "path escapes its fixed root")
|
|
1043
|
+
return candidate
|
|
1044
|
+
|
|
1045
|
+
|
|
1046
|
+
def _safe_relative_path(value: Any, label: str) -> str:
|
|
1047
|
+
if not isinstance(value, str) or not value or "\\" in value:
|
|
1048
|
+
raise DriverError("INVALID_MANIFEST", f"{label} is invalid")
|
|
1049
|
+
path = PurePosixPath(value)
|
|
1050
|
+
if path.is_absolute() or any(part in {"", ".", ".."} for part in path.parts):
|
|
1051
|
+
raise DriverError("INVALID_MANIFEST", f"{label} is unsafe")
|
|
1052
|
+
return path.as_posix()
|
|
1053
|
+
|
|
1054
|
+
|
|
1055
|
+
def _safe_prefix(value: Any) -> str:
|
|
1056
|
+
result = _safe_relative_path(value, "excluded prefix")
|
|
1057
|
+
return f"{result.rstrip('/')}/"
|
|
1058
|
+
|
|
1059
|
+
|
|
1060
|
+
def _reject_excluded_path(relative: str, excluded: Sequence[str]) -> None:
|
|
1061
|
+
normalized = f"{relative.rstrip('/')}/" if relative.endswith("/") else relative
|
|
1062
|
+
if any(normalized.startswith(prefix) for prefix in excluded):
|
|
1063
|
+
raise DriverError(
|
|
1064
|
+
"ENTERPRISE_EXCLUDED", "enterprise/ is outside the worker license boundary"
|
|
1065
|
+
)
|
|
1066
|
+
|
|
1067
|
+
|
|
1068
|
+
def _file_sha256(path: Path) -> str:
|
|
1069
|
+
digest = hashlib.sha256()
|
|
1070
|
+
with path.open("rb") as handle:
|
|
1071
|
+
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
|
1072
|
+
digest.update(chunk)
|
|
1073
|
+
return digest.hexdigest()
|
|
1074
|
+
|
|
1075
|
+
|
|
1076
|
+
def _validate_base_url(value: str) -> str:
|
|
1077
|
+
parsed = urllib.parse.urlparse(value)
|
|
1078
|
+
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
|
|
1079
|
+
raise DriverError("INVALID_BACKEND_CONFIG", "backend base_url must be HTTP(S)")
|
|
1080
|
+
if parsed.username or parsed.password or parsed.query or parsed.fragment:
|
|
1081
|
+
raise DriverError(
|
|
1082
|
+
"INVALID_BACKEND_CONFIG", "backend base_url must not contain secrets/query"
|
|
1083
|
+
)
|
|
1084
|
+
return value.rstrip("/")
|
|
1085
|
+
|
|
1086
|
+
|
|
1087
|
+
def _canonical_hash(value: Any) -> str:
|
|
1088
|
+
return hashlib.sha256(_canonical_json(value).encode("utf-8")).hexdigest()
|
|
1089
|
+
|
|
1090
|
+
|
|
1091
|
+
def _canonical_json(value: Any) -> str:
|
|
1092
|
+
try:
|
|
1093
|
+
return json.dumps(
|
|
1094
|
+
value,
|
|
1095
|
+
ensure_ascii=False,
|
|
1096
|
+
sort_keys=True,
|
|
1097
|
+
separators=(",", ":"),
|
|
1098
|
+
allow_nan=False,
|
|
1099
|
+
)
|
|
1100
|
+
except (TypeError, ValueError) as exc:
|
|
1101
|
+
raise DriverError("NON_CANONICAL_JSON", "value is not canonical JSON") from exc
|
|
1102
|
+
|
|
1103
|
+
|
|
1104
|
+
def _loads_strict_json(raw: str) -> Any:
|
|
1105
|
+
def reject_constant(value: str) -> None:
|
|
1106
|
+
raise ValueError(f"non-finite constant: {value}")
|
|
1107
|
+
|
|
1108
|
+
def reject_duplicates(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
|
|
1109
|
+
result: dict[str, Any] = {}
|
|
1110
|
+
for key, value in pairs:
|
|
1111
|
+
if key in result:
|
|
1112
|
+
raise ValueError(f"duplicate key: {key}")
|
|
1113
|
+
result[key] = value
|
|
1114
|
+
return result
|
|
1115
|
+
|
|
1116
|
+
try:
|
|
1117
|
+
return json.loads(raw, parse_constant=reject_constant, object_pairs_hook=reject_duplicates)
|
|
1118
|
+
except (TypeError, ValueError, json.JSONDecodeError) as exc:
|
|
1119
|
+
raise DriverError("INVALID_JSON", "input is not strict JSON") from exc
|
|
1120
|
+
|
|
1121
|
+
|
|
1122
|
+
def _mapping(value: Any, label: str) -> Mapping[str, Any]:
|
|
1123
|
+
if not isinstance(value, Mapping) or any(not isinstance(key, str) for key in value):
|
|
1124
|
+
raise DriverError("INVALID_SCHEMA", f"{label} must be a string-keyed mapping")
|
|
1125
|
+
return value
|
|
1126
|
+
|
|
1127
|
+
|
|
1128
|
+
def _exact_keys(value: Mapping[str, Any], expected: set[str], label: str) -> None:
|
|
1129
|
+
if set(value) != expected:
|
|
1130
|
+
raise DriverError("INVALID_SCHEMA", f"{label} fields do not match the protocol")
|
|
1131
|
+
|
|
1132
|
+
|
|
1133
|
+
def _text(value: Any, label: str) -> str:
|
|
1134
|
+
if not isinstance(value, str) or not value.strip():
|
|
1135
|
+
raise DriverError("INVALID_SCHEMA", f"{label} must be non-empty text")
|
|
1136
|
+
return value
|
|
1137
|
+
|
|
1138
|
+
|
|
1139
|
+
def _plain_text(value: Any, label: str) -> str:
|
|
1140
|
+
if not isinstance(value, str):
|
|
1141
|
+
raise DriverError("INVALID_SCHEMA", f"{label} must be text")
|
|
1142
|
+
return value
|
|
1143
|
+
|
|
1144
|
+
|
|
1145
|
+
def _hash(value: Any, label: str) -> str:
|
|
1146
|
+
if not isinstance(value, str) or _HASH_RE.fullmatch(value) is None:
|
|
1147
|
+
raise DriverError("INVALID_SCHEMA", f"{label} must be a SHA-256 hash")
|
|
1148
|
+
return value
|
|
1149
|
+
|
|
1150
|
+
|
|
1151
|
+
def _positive_int(value: Any, label: str) -> int:
|
|
1152
|
+
if isinstance(value, bool) or not isinstance(value, int) or value < 1:
|
|
1153
|
+
raise DriverError("INVALID_SCHEMA", f"{label} must be a positive integer")
|
|
1154
|
+
return value
|
|
1155
|
+
|
|
1156
|
+
|
|
1157
|
+
def _nonnegative_int(value: Any, label: str) -> int:
|
|
1158
|
+
if isinstance(value, bool) or not isinstance(value, int) or value < 0:
|
|
1159
|
+
raise DriverError("INVALID_SCHEMA", f"{label} must be a non-negative integer")
|
|
1160
|
+
return value
|
|
1161
|
+
|
|
1162
|
+
|
|
1163
|
+
def _positive_float(value: Any, label: str) -> float:
|
|
1164
|
+
result = _nonnegative_float(value, label)
|
|
1165
|
+
if result <= 0:
|
|
1166
|
+
raise DriverError("INVALID_SCHEMA", f"{label} must be positive")
|
|
1167
|
+
return result
|
|
1168
|
+
|
|
1169
|
+
|
|
1170
|
+
def _nonnegative_float(value: Any, label: str) -> float:
|
|
1171
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
1172
|
+
raise DriverError("INVALID_SCHEMA", f"{label} must be numeric")
|
|
1173
|
+
result = float(value)
|
|
1174
|
+
if not math.isfinite(result) or result < 0:
|
|
1175
|
+
raise DriverError("INVALID_SCHEMA", f"{label} must be finite and non-negative")
|
|
1176
|
+
return result
|
|
1177
|
+
|
|
1178
|
+
|
|
1179
|
+
def build_argument_parser() -> argparse.ArgumentParser:
|
|
1180
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
1181
|
+
parser.add_argument("--checkout", required=True, type=Path)
|
|
1182
|
+
parser.add_argument("--backend-config-json")
|
|
1183
|
+
parser.add_argument("--verify-only", action="store_true")
|
|
1184
|
+
return parser
|
|
1185
|
+
|
|
1186
|
+
|
|
1187
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
1188
|
+
args = build_argument_parser().parse_args(argv)
|
|
1189
|
+
policy = load_compatibility_policy()
|
|
1190
|
+
checkout = verify_checkout(args.checkout, policy)
|
|
1191
|
+
assets = load_official_assets(checkout)
|
|
1192
|
+
if args.verify_only:
|
|
1193
|
+
print(
|
|
1194
|
+
_canonical_json(
|
|
1195
|
+
{
|
|
1196
|
+
"status": "ok",
|
|
1197
|
+
"upstream_commit": UPSTREAM_COMMIT,
|
|
1198
|
+
"release_id": RELEASE_ID,
|
|
1199
|
+
"execution_mode": "controlled_component_harness",
|
|
1200
|
+
"component_only": True,
|
|
1201
|
+
"openhands_full_system": False,
|
|
1202
|
+
"intent_prompt_sha256": assets.intent_prompt_sha256,
|
|
1203
|
+
"intent_tool_schema_hash": assets.intent_tool_schema_hash,
|
|
1204
|
+
"clarify_tool_schema_hash": assets.clarify_tool_schema_hash,
|
|
1205
|
+
}
|
|
1206
|
+
)
|
|
1207
|
+
)
|
|
1208
|
+
return 0
|
|
1209
|
+
if args.backend_config_json is None:
|
|
1210
|
+
raise DriverError("INVALID_BACKEND_CONFIG", "serve mode requires backend configuration")
|
|
1211
|
+
config = BackendConfig.from_json(args.backend_config_json)
|
|
1212
|
+
runtime = AskOrAssumeRuntime(
|
|
1213
|
+
checkout,
|
|
1214
|
+
config,
|
|
1215
|
+
OpenAICompatibleBackend(config),
|
|
1216
|
+
policy=policy,
|
|
1217
|
+
)
|
|
1218
|
+
serve(runtime, sys.stdin, sys.stdout)
|
|
1219
|
+
return 0
|
|
1220
|
+
|
|
1221
|
+
|
|
1222
|
+
if __name__ == "__main__": # pragma: no cover - exercised through subprocess deployment
|
|
1223
|
+
try:
|
|
1224
|
+
raise SystemExit(main())
|
|
1225
|
+
except DriverError as exc:
|
|
1226
|
+
print(_canonical_json(_failure_response("", exc.code, str(exc))))
|
|
1227
|
+
raise SystemExit(2) from None
|