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
workers/ace/driver.py
ADDED
|
@@ -0,0 +1,1682 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Checksum-pinned JSONL driver for upstream ACE generic text tasks.
|
|
3
|
+
|
|
4
|
+
The worker deliberately keeps deployment configuration outside request payloads:
|
|
5
|
+
the operator supplies one verified checkout and one explicit LLM configuration at
|
|
6
|
+
process startup. Requests can contain examples and bounded scientific controls,
|
|
7
|
+
but never a module, callable, command, endpoint, or filesystem path.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import ast
|
|
14
|
+
import contextlib
|
|
15
|
+
import functools
|
|
16
|
+
import hashlib
|
|
17
|
+
import importlib
|
|
18
|
+
import io
|
|
19
|
+
import json
|
|
20
|
+
import math
|
|
21
|
+
import os
|
|
22
|
+
import random
|
|
23
|
+
import re
|
|
24
|
+
import subprocess
|
|
25
|
+
import sys
|
|
26
|
+
import tempfile
|
|
27
|
+
import threading
|
|
28
|
+
import urllib.parse
|
|
29
|
+
from collections.abc import Mapping, Sequence
|
|
30
|
+
from dataclasses import dataclass
|
|
31
|
+
from pathlib import Path, PurePosixPath
|
|
32
|
+
from typing import Any, TextIO
|
|
33
|
+
|
|
34
|
+
UPSTREAM_COMMIT = "bcb7cea0504afad6f55fec4845dd4864c9f9eee7"
|
|
35
|
+
PROTOCOL_VERSION = "agentinputlab.ace.offline.v1"
|
|
36
|
+
_HASH_RE = re.compile(r"[0-9a-f]{64}")
|
|
37
|
+
_COMMIT_RE = re.compile(r"[0-9a-f]{40}")
|
|
38
|
+
_REF_RE = re.compile(r"refs/[A-Za-z0-9._/-]+")
|
|
39
|
+
_ENV_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
|
|
40
|
+
_PLAYBOOK_LINE_RE = re.compile(r"\[([^\]]+)\]\s*helpful=(\d+)\s*harmful=(\d+)\s*::\s*(.*)")
|
|
41
|
+
_ROLE_NAMES = ("target", "reflector", "curator")
|
|
42
|
+
_PROVIDERS = frozenset(("openai", "sambanova", "together", "commonstack"))
|
|
43
|
+
_STANDARD_SECTIONS = (
|
|
44
|
+
"STRATEGIES & INSIGHTS",
|
|
45
|
+
"FORMULAS & CALCULATIONS",
|
|
46
|
+
"CODE SNIPPETS & TEMPLATES",
|
|
47
|
+
"COMMON MISTAKES TO AVOID",
|
|
48
|
+
"PROBLEM-SOLVING HEURISTICS",
|
|
49
|
+
"CONTEXT CLUES & INDICATORS",
|
|
50
|
+
"OTHERS",
|
|
51
|
+
)
|
|
52
|
+
_MAX_JSONL_BYTES = 8 * 1024 * 1024
|
|
53
|
+
_MAX_UPSTREAM_STDOUT_BYTES = 1024 * 1024
|
|
54
|
+
_UPSTREAM_MODULES = ("ace", "llm", "logger", "playbook_utils", "utils")
|
|
55
|
+
_OPENAI_CONSTRUCTOR_ENV_LOCK = threading.RLock()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class WorkerFailure(RuntimeError):
|
|
59
|
+
"""Expected fail-closed worker error with a stable public code."""
|
|
60
|
+
|
|
61
|
+
def __init__(self, code: str, message: str) -> None:
|
|
62
|
+
super().__init__(message)
|
|
63
|
+
self.code = code
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class BoundedHashSink(io.TextIOBase):
|
|
67
|
+
"""Discard upstream text while retaining only bounded audit metadata."""
|
|
68
|
+
|
|
69
|
+
def __init__(self, max_bytes: int = _MAX_UPSTREAM_STDOUT_BYTES) -> None:
|
|
70
|
+
super().__init__()
|
|
71
|
+
self.max_bytes = max_bytes
|
|
72
|
+
self.byte_count = 0
|
|
73
|
+
self._digest = hashlib.sha256()
|
|
74
|
+
|
|
75
|
+
def writable(self) -> bool:
|
|
76
|
+
return True
|
|
77
|
+
|
|
78
|
+
def write(self, value: str) -> int:
|
|
79
|
+
if not isinstance(value, str):
|
|
80
|
+
raise TypeError("upstream stdout must be text")
|
|
81
|
+
encoded = value.encode("utf-8", errors="replace")
|
|
82
|
+
if self.byte_count + len(encoded) > self.max_bytes:
|
|
83
|
+
raise WorkerFailure(
|
|
84
|
+
"UPSTREAM_OUTPUT_LIMIT_EXCEEDED",
|
|
85
|
+
"ACE upstream stdout exceeded the bounded diagnostic limit",
|
|
86
|
+
)
|
|
87
|
+
self._digest.update(encoded)
|
|
88
|
+
self.byte_count += len(encoded)
|
|
89
|
+
return len(value)
|
|
90
|
+
|
|
91
|
+
def flush(self) -> None:
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def sha256(self) -> str:
|
|
96
|
+
return self._digest.hexdigest()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass(frozen=True, slots=True)
|
|
100
|
+
class CompatibilityPolicy:
|
|
101
|
+
commit: str
|
|
102
|
+
license_path: str
|
|
103
|
+
license_spdx: str
|
|
104
|
+
license_sha256: str
|
|
105
|
+
source_hashes: Mapping[str, str]
|
|
106
|
+
require_clean_checkout: bool
|
|
107
|
+
|
|
108
|
+
@classmethod
|
|
109
|
+
def from_mapping(cls, value: Mapping[str, Any]) -> CompatibilityPolicy:
|
|
110
|
+
upstream = _require_mapping(value.get("upstream"), "compatibility upstream")
|
|
111
|
+
sources = _require_mapping(value.get("source_files"), "compatibility source_files")
|
|
112
|
+
commit = _require_text(upstream.get("commit"), "compatibility commit")
|
|
113
|
+
license_path = _require_text(upstream.get("license_path"), "compatibility license_path")
|
|
114
|
+
license_spdx = _require_text(upstream.get("license_spdx"), "compatibility license_spdx")
|
|
115
|
+
license_sha256 = _require_hash(
|
|
116
|
+
upstream.get("license_sha256"), "compatibility license_sha256"
|
|
117
|
+
)
|
|
118
|
+
require_clean_checkout = upstream.get("require_clean_checkout")
|
|
119
|
+
if not isinstance(require_clean_checkout, bool):
|
|
120
|
+
raise WorkerFailure(
|
|
121
|
+
"INVALID_COMPATIBILITY_MANIFEST",
|
|
122
|
+
"ACE compatibility policy must declare require_clean_checkout",
|
|
123
|
+
)
|
|
124
|
+
if commit != UPSTREAM_COMMIT or license_spdx != "Apache-2.0":
|
|
125
|
+
raise WorkerFailure(
|
|
126
|
+
"INVALID_COMPATIBILITY_MANIFEST",
|
|
127
|
+
"ACE compatibility policy must pin the approved commit and Apache-2.0",
|
|
128
|
+
)
|
|
129
|
+
source_hashes: dict[str, str] = {}
|
|
130
|
+
for raw_path, raw_hash in sources.items():
|
|
131
|
+
path = _safe_relative_path(raw_path, "compatibility source path")
|
|
132
|
+
source_hashes[path] = _require_hash(raw_hash, f"compatibility hash for {path}")
|
|
133
|
+
if not source_hashes:
|
|
134
|
+
raise WorkerFailure(
|
|
135
|
+
"INVALID_COMPATIBILITY_MANIFEST",
|
|
136
|
+
"ACE compatibility policy requires source hashes",
|
|
137
|
+
)
|
|
138
|
+
return cls(
|
|
139
|
+
commit=commit,
|
|
140
|
+
license_path=_safe_relative_path(license_path, "compatibility license path"),
|
|
141
|
+
license_spdx=license_spdx,
|
|
142
|
+
license_sha256=license_sha256,
|
|
143
|
+
source_hashes=source_hashes,
|
|
144
|
+
require_clean_checkout=require_clean_checkout,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@dataclass(frozen=True, slots=True)
|
|
149
|
+
class RoleLLMConfig:
|
|
150
|
+
model: str
|
|
151
|
+
fingerprint: Mapping[str, Any]
|
|
152
|
+
input_cost_per_million: float
|
|
153
|
+
output_cost_per_million: float
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@dataclass(frozen=True, slots=True)
|
|
157
|
+
class LLMConfig:
|
|
158
|
+
api_provider: str
|
|
159
|
+
max_tokens_per_call: int
|
|
160
|
+
json_mode: bool
|
|
161
|
+
roles: Mapping[str, RoleLLMConfig]
|
|
162
|
+
base_url: str | None = None
|
|
163
|
+
api_key_env: str | None = None
|
|
164
|
+
extra_body: Mapping[str, Any] | None = None
|
|
165
|
+
|
|
166
|
+
@classmethod
|
|
167
|
+
def from_json(cls, raw: str) -> LLMConfig:
|
|
168
|
+
value = _loads_strict_json(raw)
|
|
169
|
+
root = _require_mapping(value, "LLM configuration")
|
|
170
|
+
required = {"api_provider", "max_tokens_per_call", "json_mode", "roles"}
|
|
171
|
+
allowed = required | {"base_url", "api_key_env", "extra_body"}
|
|
172
|
+
if not required.issubset(root) or not set(root).issubset(allowed):
|
|
173
|
+
raise WorkerFailure(
|
|
174
|
+
"INVALID_SCHEMA", "LLM configuration fields do not match the protocol"
|
|
175
|
+
)
|
|
176
|
+
provider = _require_text(root["api_provider"], "LLM api_provider")
|
|
177
|
+
if provider not in _PROVIDERS:
|
|
178
|
+
raise WorkerFailure(
|
|
179
|
+
"INVALID_LLM_CONFIG",
|
|
180
|
+
f"LLM api_provider must be one of {sorted(_PROVIDERS)}",
|
|
181
|
+
)
|
|
182
|
+
max_tokens = _require_positive_int(root["max_tokens_per_call"], "LLM max_tokens_per_call")
|
|
183
|
+
json_mode = root["json_mode"]
|
|
184
|
+
if not isinstance(json_mode, bool):
|
|
185
|
+
raise WorkerFailure("INVALID_LLM_CONFIG", "LLM json_mode must be boolean")
|
|
186
|
+
raw_roles = _require_mapping(root["roles"], "LLM roles")
|
|
187
|
+
if set(raw_roles) != set(_ROLE_NAMES):
|
|
188
|
+
raise WorkerFailure(
|
|
189
|
+
"INVALID_LLM_CONFIG",
|
|
190
|
+
"LLM roles must bind target, reflector, and curator exactly",
|
|
191
|
+
)
|
|
192
|
+
roles: dict[str, RoleLLMConfig] = {}
|
|
193
|
+
for role in _ROLE_NAMES:
|
|
194
|
+
role_value = _require_mapping(raw_roles[role], f"LLM role {role}")
|
|
195
|
+
_require_exact_keys(
|
|
196
|
+
role_value,
|
|
197
|
+
{
|
|
198
|
+
"model",
|
|
199
|
+
"fingerprint",
|
|
200
|
+
"input_cost_per_million",
|
|
201
|
+
"output_cost_per_million",
|
|
202
|
+
},
|
|
203
|
+
f"LLM role {role}",
|
|
204
|
+
)
|
|
205
|
+
fingerprint = _require_mapping(role_value["fingerprint"], f"LLM {role} fingerprint")
|
|
206
|
+
if not fingerprint:
|
|
207
|
+
raise WorkerFailure(
|
|
208
|
+
"INVALID_LLM_CONFIG", f"LLM {role} fingerprint must be non-empty"
|
|
209
|
+
)
|
|
210
|
+
_canonical_json(fingerprint)
|
|
211
|
+
roles[role] = RoleLLMConfig(
|
|
212
|
+
model=_require_text(role_value["model"], f"LLM {role} model"),
|
|
213
|
+
fingerprint=dict(fingerprint),
|
|
214
|
+
input_cost_per_million=_require_non_negative_float(
|
|
215
|
+
role_value["input_cost_per_million"],
|
|
216
|
+
f"LLM {role} input_cost_per_million",
|
|
217
|
+
),
|
|
218
|
+
output_cost_per_million=_require_non_negative_float(
|
|
219
|
+
role_value["output_cost_per_million"],
|
|
220
|
+
f"LLM {role} output_cost_per_million",
|
|
221
|
+
),
|
|
222
|
+
)
|
|
223
|
+
base_url_value = root.get("base_url")
|
|
224
|
+
api_key_env_value = root.get("api_key_env")
|
|
225
|
+
if (base_url_value is None) != (api_key_env_value is None):
|
|
226
|
+
raise WorkerFailure(
|
|
227
|
+
"INVALID_LLM_CONFIG",
|
|
228
|
+
"LLM base_url and api_key_env must be supplied together",
|
|
229
|
+
)
|
|
230
|
+
base_url: str | None = None
|
|
231
|
+
api_key_env: str | None = None
|
|
232
|
+
if base_url_value is not None:
|
|
233
|
+
if provider != "openai":
|
|
234
|
+
raise WorkerFailure(
|
|
235
|
+
"INVALID_LLM_CONFIG",
|
|
236
|
+
"custom OpenAI-compatible endpoints require api_provider=openai",
|
|
237
|
+
)
|
|
238
|
+
base_url = _require_http_url(base_url_value, "LLM base_url")
|
|
239
|
+
api_key_env = _require_env_name(api_key_env_value, "LLM api_key_env")
|
|
240
|
+
extra_body_value = root.get("extra_body")
|
|
241
|
+
extra_body: Mapping[str, Any] | None = None
|
|
242
|
+
if extra_body_value is not None:
|
|
243
|
+
if base_url is None:
|
|
244
|
+
raise WorkerFailure(
|
|
245
|
+
"INVALID_LLM_CONFIG",
|
|
246
|
+
"LLM extra_body is permitted only for an operator-bound endpoint",
|
|
247
|
+
)
|
|
248
|
+
extra_body = dict(_require_mapping(extra_body_value, "LLM extra_body"))
|
|
249
|
+
_canonical_json(extra_body)
|
|
250
|
+
return cls(
|
|
251
|
+
api_provider=provider,
|
|
252
|
+
max_tokens_per_call=max_tokens,
|
|
253
|
+
json_mode=json_mode,
|
|
254
|
+
roles=roles,
|
|
255
|
+
base_url=base_url,
|
|
256
|
+
api_key_env=api_key_env,
|
|
257
|
+
extra_body=extra_body,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@dataclass(frozen=True, slots=True)
|
|
262
|
+
class Controls:
|
|
263
|
+
max_rounds: int
|
|
264
|
+
max_model_calls: int
|
|
265
|
+
max_tokens: int
|
|
266
|
+
max_cost_usd: float
|
|
267
|
+
seed: int
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
@dataclass(frozen=True, slots=True)
|
|
271
|
+
class ValidatedRequest:
|
|
272
|
+
request_id: str
|
|
273
|
+
train_examples: tuple[Mapping[str, Any], ...]
|
|
274
|
+
validation_examples: tuple[Mapping[str, Any], ...]
|
|
275
|
+
initial_bullets: tuple[dict[str, Any], ...]
|
|
276
|
+
controls: Controls
|
|
277
|
+
split_audit_hash: str
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
class GenericTextProcessor:
|
|
281
|
+
"""Minimal task-agnostic exact-match processor required by upstream ACE."""
|
|
282
|
+
|
|
283
|
+
@staticmethod
|
|
284
|
+
def answer_is_correct(predicted: str, ground_truth: str) -> bool:
|
|
285
|
+
return predicted.strip().casefold() == ground_truth.strip().casefold()
|
|
286
|
+
|
|
287
|
+
def evaluate_accuracy(self, predictions: Sequence[str], ground_truths: Sequence[str]) -> float:
|
|
288
|
+
if len(predictions) != len(ground_truths):
|
|
289
|
+
raise ValueError("predictions and ground truths must have equal length")
|
|
290
|
+
if not predictions:
|
|
291
|
+
return 0.0
|
|
292
|
+
correct = sum(
|
|
293
|
+
self.answer_is_correct(prediction, truth)
|
|
294
|
+
for prediction, truth in zip(predictions, ground_truths, strict=True)
|
|
295
|
+
)
|
|
296
|
+
return correct / len(predictions)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
class ProviderAttemptCompletions:
|
|
300
|
+
"""Transparent completion proxy that enforces the shared attempt budget."""
|
|
301
|
+
|
|
302
|
+
def __init__(self, delegate: Any, tracker: UsageTracker, role: str) -> None:
|
|
303
|
+
self._delegate = delegate
|
|
304
|
+
self._tracker = tracker
|
|
305
|
+
self._role = role
|
|
306
|
+
|
|
307
|
+
def create(self, *args: Any, **kwargs: Any) -> Any:
|
|
308
|
+
self._tracker.before_provider_attempt(self._role)
|
|
309
|
+
try:
|
|
310
|
+
result = self._delegate.create(*args, **kwargs)
|
|
311
|
+
except Exception:
|
|
312
|
+
self._tracker.failed_provider_attempt()
|
|
313
|
+
raise
|
|
314
|
+
self._tracker.after_provider_response(self._role, result)
|
|
315
|
+
return result
|
|
316
|
+
|
|
317
|
+
def __getattr__(self, name: str) -> Any:
|
|
318
|
+
return getattr(self._delegate, name)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
class ProviderAttemptChat:
|
|
322
|
+
"""Transparent chat proxy retaining all non-completion client behavior."""
|
|
323
|
+
|
|
324
|
+
def __init__(self, delegate: Any, tracker: UsageTracker, role: str) -> None:
|
|
325
|
+
self._delegate = delegate
|
|
326
|
+
self.completions = ProviderAttemptCompletions(delegate.completions, tracker, role)
|
|
327
|
+
|
|
328
|
+
def __getattr__(self, name: str) -> Any:
|
|
329
|
+
return getattr(self._delegate, name)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
class ProviderAttemptClient:
|
|
333
|
+
"""Transparent upstream client proxy with a counted completion seam."""
|
|
334
|
+
|
|
335
|
+
def __init__(self, delegate: Any, tracker: UsageTracker, role: str) -> None:
|
|
336
|
+
self._delegate = delegate
|
|
337
|
+
self.chat = ProviderAttemptChat(delegate.chat, tracker, role)
|
|
338
|
+
|
|
339
|
+
def __getattr__(self, name: str) -> Any:
|
|
340
|
+
return getattr(self._delegate, name)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
class CompletionDefaults:
|
|
344
|
+
"""Inject immutable deployment-only completion fields before observation."""
|
|
345
|
+
|
|
346
|
+
def __init__(self, delegate: Any, extra_body: Mapping[str, Any]) -> None:
|
|
347
|
+
self._delegate = delegate
|
|
348
|
+
self._extra_body = dict(extra_body)
|
|
349
|
+
|
|
350
|
+
def create(self, *args: Any, **kwargs: Any) -> Any:
|
|
351
|
+
if "extra_body" in kwargs:
|
|
352
|
+
raise WorkerFailure(
|
|
353
|
+
"UPSTREAM_INTERFACE_MISMATCH",
|
|
354
|
+
"upstream ACE unexpectedly supplied request-level extra_body",
|
|
355
|
+
)
|
|
356
|
+
if (
|
|
357
|
+
"seed" in self._extra_body
|
|
358
|
+
and "seed" in kwargs
|
|
359
|
+
and (type(kwargs["seed"]) is not int or kwargs["seed"] != self._extra_body["seed"])
|
|
360
|
+
):
|
|
361
|
+
raise WorkerFailure(
|
|
362
|
+
"UPSTREAM_INTERFACE_MISMATCH", "upstream ACE seed differs from the fit controls"
|
|
363
|
+
)
|
|
364
|
+
return self._delegate.create(*args, extra_body=dict(self._extra_body), **kwargs)
|
|
365
|
+
|
|
366
|
+
def __getattr__(self, name: str) -> Any:
|
|
367
|
+
return getattr(self._delegate, name)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
class CompletionDefaultChat:
|
|
371
|
+
def __init__(self, delegate: Any, extra_body: Mapping[str, Any]) -> None:
|
|
372
|
+
self._delegate = delegate
|
|
373
|
+
self.completions = CompletionDefaults(delegate.completions, extra_body)
|
|
374
|
+
|
|
375
|
+
def __getattr__(self, name: str) -> Any:
|
|
376
|
+
return getattr(self._delegate, name)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
class CompletionDefaultClient:
|
|
380
|
+
def __init__(self, delegate: Any, extra_body: Mapping[str, Any]) -> None:
|
|
381
|
+
self._delegate = delegate
|
|
382
|
+
self.chat = CompletionDefaultChat(delegate.chat, extra_body)
|
|
383
|
+
|
|
384
|
+
def __getattr__(self, name: str) -> Any:
|
|
385
|
+
return getattr(self._delegate, name)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
class UsageTracker:
|
|
389
|
+
"""Observe upstream logical calls, provider attempts, and returned usage."""
|
|
390
|
+
|
|
391
|
+
def __init__(self, controls: Controls, llm_config: LLMConfig) -> None:
|
|
392
|
+
self.controls = controls
|
|
393
|
+
self.llm_config = llm_config
|
|
394
|
+
self.model_calls = 0
|
|
395
|
+
self.input_tokens = 0
|
|
396
|
+
self.output_tokens = 0
|
|
397
|
+
self.cost_usd = 0.0
|
|
398
|
+
self.call_sequence: list[str] = []
|
|
399
|
+
self.provider_attempt_sequence: list[str] = []
|
|
400
|
+
self.provider_input_tokens = 0
|
|
401
|
+
self.provider_output_tokens = 0
|
|
402
|
+
self.provider_cost_usd = 0.0
|
|
403
|
+
self.provider_usage_responses = 0
|
|
404
|
+
self.failed_provider_attempts = 0
|
|
405
|
+
self.first_component_failure: WorkerFailure | None = None
|
|
406
|
+
self._lock = threading.Lock()
|
|
407
|
+
|
|
408
|
+
def install(self, ace_system: Any) -> None:
|
|
409
|
+
self._install_component(ace_system.generator, "generate", "target")
|
|
410
|
+
self._install_component(ace_system.reflector, "reflect", "reflector")
|
|
411
|
+
self._install_component(ace_system.curator, "curate", "curator")
|
|
412
|
+
|
|
413
|
+
def _install_component(self, component: Any, method_name: str, role: str) -> None:
|
|
414
|
+
client = getattr(component, "api_client", None)
|
|
415
|
+
chat = getattr(client, "chat", None)
|
|
416
|
+
completions = getattr(chat, "completions", None)
|
|
417
|
+
if not callable(getattr(completions, "create", None)):
|
|
418
|
+
raise WorkerFailure(
|
|
419
|
+
"UPSTREAM_INTERFACE_MISMATCH",
|
|
420
|
+
f"upstream ACE {role} lacks api_client.chat.completions.create()",
|
|
421
|
+
)
|
|
422
|
+
component.api_client = ProviderAttemptClient(client, self, role)
|
|
423
|
+
self._wrap(component, method_name, role)
|
|
424
|
+
|
|
425
|
+
def _wrap(self, component: Any, method_name: str, role: str) -> None:
|
|
426
|
+
original = getattr(component, method_name, None)
|
|
427
|
+
if not callable(original):
|
|
428
|
+
raise WorkerFailure(
|
|
429
|
+
"UPSTREAM_INTERFACE_MISMATCH",
|
|
430
|
+
f"upstream ACE {role} does not expose {method_name}()",
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
@functools.wraps(original)
|
|
434
|
+
def observed(*args: Any, **kwargs: Any) -> Any:
|
|
435
|
+
self._before_component_call(role)
|
|
436
|
+
try:
|
|
437
|
+
result = original(*args, **kwargs)
|
|
438
|
+
self._after_call(role, result)
|
|
439
|
+
except WorkerFailure as exc:
|
|
440
|
+
# Native validation may catch this and then fail in its own
|
|
441
|
+
# empty-results branch. Keep the original guard failure/costs.
|
|
442
|
+
with self._lock:
|
|
443
|
+
if self.first_component_failure is None:
|
|
444
|
+
self.first_component_failure = exc
|
|
445
|
+
raise
|
|
446
|
+
return result
|
|
447
|
+
|
|
448
|
+
setattr(component, method_name, observed)
|
|
449
|
+
|
|
450
|
+
def _before_component_call(self, role: str) -> None:
|
|
451
|
+
with self._lock:
|
|
452
|
+
self.call_sequence.append(role)
|
|
453
|
+
|
|
454
|
+
def before_provider_attempt(self, role: str) -> None:
|
|
455
|
+
with self._lock:
|
|
456
|
+
if self.model_calls >= self.controls.max_model_calls:
|
|
457
|
+
raise WorkerFailure(
|
|
458
|
+
"MODEL_CALL_BUDGET_EXCEEDED",
|
|
459
|
+
"ACE reached max_model_calls before the next provider attempt",
|
|
460
|
+
)
|
|
461
|
+
self.model_calls += 1
|
|
462
|
+
self.provider_attempt_sequence.append(role)
|
|
463
|
+
|
|
464
|
+
def _after_call(self, role: str, result: Any) -> None:
|
|
465
|
+
if not isinstance(result, tuple) or not result:
|
|
466
|
+
raise WorkerFailure(
|
|
467
|
+
"UPSTREAM_USAGE_UNAVAILABLE",
|
|
468
|
+
f"upstream ACE {role} returned an unexpected result",
|
|
469
|
+
)
|
|
470
|
+
info = result[-1]
|
|
471
|
+
if not isinstance(info, Mapping):
|
|
472
|
+
raise WorkerFailure(
|
|
473
|
+
"UPSTREAM_USAGE_UNAVAILABLE",
|
|
474
|
+
f"upstream ACE {role} omitted call metadata",
|
|
475
|
+
)
|
|
476
|
+
input_tokens = _require_non_negative_int(
|
|
477
|
+
info.get("prompt_num_tokens"), f"upstream {role} prompt_num_tokens"
|
|
478
|
+
)
|
|
479
|
+
output_tokens = _require_non_negative_int(
|
|
480
|
+
info.get("response_num_tokens"), f"upstream {role} response_num_tokens"
|
|
481
|
+
)
|
|
482
|
+
expected_model = self.llm_config.roles[role].model
|
|
483
|
+
if info.get("model") != expected_model:
|
|
484
|
+
raise WorkerFailure(
|
|
485
|
+
"UPSTREAM_MODEL_MISMATCH",
|
|
486
|
+
f"upstream {role} reported a model other than the configured model",
|
|
487
|
+
)
|
|
488
|
+
pricing = self.llm_config.roles[role]
|
|
489
|
+
cost = (
|
|
490
|
+
input_tokens * pricing.input_cost_per_million
|
|
491
|
+
+ output_tokens * pricing.output_cost_per_million
|
|
492
|
+
) / 1_000_000
|
|
493
|
+
with self._lock:
|
|
494
|
+
self.input_tokens += input_tokens
|
|
495
|
+
self.output_tokens += output_tokens
|
|
496
|
+
self.cost_usd += cost
|
|
497
|
+
if self.input_tokens + self.output_tokens > self.controls.max_tokens:
|
|
498
|
+
raise WorkerFailure(
|
|
499
|
+
"TOKEN_BUDGET_EXCEEDED", "ACE exceeded the request token budget"
|
|
500
|
+
)
|
|
501
|
+
if self.cost_usd > self.controls.max_cost_usd:
|
|
502
|
+
raise WorkerFailure("COST_BUDGET_EXCEEDED", "ACE exceeded the request cost budget")
|
|
503
|
+
|
|
504
|
+
def failed_provider_attempt(self) -> None:
|
|
505
|
+
with self._lock:
|
|
506
|
+
self.failed_provider_attempts += 1
|
|
507
|
+
|
|
508
|
+
def after_provider_response(self, role: str, response: Any) -> None:
|
|
509
|
+
# Observe SDK usage before upstream JSON parsing can fail or retry.
|
|
510
|
+
# Older native test doubles may expose usage only in component metadata;
|
|
511
|
+
# that existing path remains available, without double-counting tokens.
|
|
512
|
+
usage = (
|
|
513
|
+
response.get("usage")
|
|
514
|
+
if isinstance(response, Mapping)
|
|
515
|
+
else getattr(response, "usage", None)
|
|
516
|
+
)
|
|
517
|
+
if usage is None:
|
|
518
|
+
return
|
|
519
|
+
read = usage.get if isinstance(usage, Mapping) else lambda key: getattr(usage, key, None)
|
|
520
|
+
incoming = _require_non_negative_int(read("prompt_tokens"), "provider prompt_tokens")
|
|
521
|
+
outgoing = _require_non_negative_int(
|
|
522
|
+
read("completion_tokens"), "provider completion_tokens"
|
|
523
|
+
)
|
|
524
|
+
pricing = self.llm_config.roles[role]
|
|
525
|
+
with self._lock:
|
|
526
|
+
self.provider_input_tokens += incoming
|
|
527
|
+
self.provider_output_tokens += outgoing
|
|
528
|
+
self.provider_usage_responses += 1
|
|
529
|
+
self.provider_cost_usd += (
|
|
530
|
+
incoming * pricing.input_cost_per_million
|
|
531
|
+
+ outgoing * pricing.output_cost_per_million
|
|
532
|
+
) / 1_000_000
|
|
533
|
+
if self.provider_input_tokens + self.provider_output_tokens > self.controls.max_tokens:
|
|
534
|
+
raise WorkerFailure(
|
|
535
|
+
"TOKEN_BUDGET_EXCEEDED", "ACE exceeded the request token budget"
|
|
536
|
+
)
|
|
537
|
+
if self.provider_cost_usd > self.controls.max_cost_usd:
|
|
538
|
+
raise WorkerFailure("COST_BUDGET_EXCEEDED", "ACE exceeded the request cost budget")
|
|
539
|
+
|
|
540
|
+
def require_upstream_chain(self) -> None:
|
|
541
|
+
self._require_sequence(self.call_sequence, "agent")
|
|
542
|
+
self._require_sequence(self.provider_attempt_sequence, "provider-attempt")
|
|
543
|
+
|
|
544
|
+
@staticmethod
|
|
545
|
+
def _require_sequence(sequence: Sequence[str], label: str) -> None:
|
|
546
|
+
try:
|
|
547
|
+
first_target = sequence.index("target")
|
|
548
|
+
first_reflector = sequence.index("reflector")
|
|
549
|
+
first_curator = sequence.index("curator")
|
|
550
|
+
except ValueError as exc:
|
|
551
|
+
raise WorkerFailure(
|
|
552
|
+
"UPSTREAM_CHAIN_INCOMPLETE",
|
|
553
|
+
f"ACE did not execute the complete {label} chain",
|
|
554
|
+
) from exc
|
|
555
|
+
if not first_target < first_reflector < first_curator:
|
|
556
|
+
raise WorkerFailure(
|
|
557
|
+
"UPSTREAM_CHAIN_REORDERED",
|
|
558
|
+
f"ACE did not preserve Generator -> Reflector -> Curator {label} order",
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
def as_dict(self) -> dict[str, int | float]:
|
|
562
|
+
# Provider and component totals describe the same successful requests.
|
|
563
|
+
# Taking their maximum retains pre-parse costs without summing twice.
|
|
564
|
+
incoming = max(self.input_tokens, self.provider_input_tokens)
|
|
565
|
+
outgoing = max(self.output_tokens, self.provider_output_tokens)
|
|
566
|
+
return {
|
|
567
|
+
"model_calls": self.model_calls,
|
|
568
|
+
"input_tokens": incoming,
|
|
569
|
+
"output_tokens": outgoing,
|
|
570
|
+
"tokens": incoming + outgoing,
|
|
571
|
+
"cost_usd": max(self.cost_usd, self.provider_cost_usd),
|
|
572
|
+
"provider_usage_responses": self.provider_usage_responses,
|
|
573
|
+
"failed_provider_attempts_with_unknown_tokens": self.failed_provider_attempts,
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
class ACEWorkerRuntime:
|
|
578
|
+
"""One fixed checkout and LLM configuration serving many JSONL requests."""
|
|
579
|
+
|
|
580
|
+
def __init__(
|
|
581
|
+
self,
|
|
582
|
+
checkout: Path,
|
|
583
|
+
llm_config: LLMConfig,
|
|
584
|
+
*,
|
|
585
|
+
policy: CompatibilityPolicy | None = None,
|
|
586
|
+
data_processor: Any | None = None,
|
|
587
|
+
) -> None:
|
|
588
|
+
if data_processor is not None:
|
|
589
|
+
for name in ("answer_is_correct", "evaluate_accuracy", "fingerprint"):
|
|
590
|
+
if not callable(getattr(data_processor, name, None)):
|
|
591
|
+
raise WorkerFailure("INVALID_TASK_PROCESSOR", f"task processor lacks {name}()")
|
|
592
|
+
fingerprint = data_processor.fingerprint()
|
|
593
|
+
if not isinstance(fingerprint, Mapping):
|
|
594
|
+
raise WorkerFailure(
|
|
595
|
+
"INVALID_TASK_PROCESSOR", "task processor fingerprint must be an object"
|
|
596
|
+
)
|
|
597
|
+
self.task_processor_fingerprint = json.loads(_canonical_json(fingerprint))
|
|
598
|
+
else:
|
|
599
|
+
self.task_processor_fingerprint = None
|
|
600
|
+
self.data_processor = (
|
|
601
|
+
data_processor if data_processor is not None else GenericTextProcessor()
|
|
602
|
+
)
|
|
603
|
+
self.policy = policy or load_compatibility_policy()
|
|
604
|
+
self.checkout = verify_checkout(checkout, self.policy)
|
|
605
|
+
self.llm_config = llm_config
|
|
606
|
+
self.ace_class, self.playbook_utils = load_upstream(self.checkout)
|
|
607
|
+
self.last_tracker: UsageTracker | None = None
|
|
608
|
+
|
|
609
|
+
@contextlib.contextmanager
|
|
610
|
+
def _bound_constructor_environment(self):
|
|
611
|
+
"""Let native initialization use the explicitly configured provider.
|
|
612
|
+
|
|
613
|
+
Upstream initializes clients before the adapter can replace them. Its
|
|
614
|
+
constructor reads the default OpenAI environment names even when the
|
|
615
|
+
operator has chosen another key name. Bind those names for construction
|
|
616
|
+
only; restore the worker's environment on success and on failure.
|
|
617
|
+
"""
|
|
618
|
+
with _OPENAI_CONSTRUCTOR_ENV_LOCK:
|
|
619
|
+
if self.llm_config.base_url is None:
|
|
620
|
+
yield
|
|
621
|
+
return
|
|
622
|
+
key_name = self.llm_config.api_key_env
|
|
623
|
+
api_key = os.environ.get(key_name, "") if key_name else ""
|
|
624
|
+
if not api_key:
|
|
625
|
+
raise WorkerFailure(
|
|
626
|
+
"MISSING_API_KEY",
|
|
627
|
+
f"ACE OpenAI-compatible runtime requires environment {key_name}",
|
|
628
|
+
)
|
|
629
|
+
values = {"OPENAI_API_KEY": api_key, "OPENAI_BASE_URL": self.llm_config.base_url}
|
|
630
|
+
previous = {name: os.environ.get(name) for name in values}
|
|
631
|
+
try:
|
|
632
|
+
os.environ.update(values)
|
|
633
|
+
yield
|
|
634
|
+
finally:
|
|
635
|
+
for name, old in previous.items():
|
|
636
|
+
if old is None:
|
|
637
|
+
os.environ.pop(name, None)
|
|
638
|
+
else:
|
|
639
|
+
os.environ[name] = old
|
|
640
|
+
|
|
641
|
+
def run(self, payload: Mapping[str, Any]) -> dict[str, Any]:
|
|
642
|
+
self.last_tracker = None
|
|
643
|
+
request = validate_request(payload, self.llm_config)
|
|
644
|
+
validator = getattr(self.data_processor, "validate_examples", None)
|
|
645
|
+
if callable(validator):
|
|
646
|
+
try:
|
|
647
|
+
validator(request.train_examples, request.validation_examples)
|
|
648
|
+
except (TypeError, ValueError) as exc:
|
|
649
|
+
raise WorkerFailure(
|
|
650
|
+
"INVALID_EXAMPLES", "task processor rejected the fit groups"
|
|
651
|
+
) from exc
|
|
652
|
+
if (
|
|
653
|
+
self.task_processor_fingerprint is not None
|
|
654
|
+
and json.loads(_canonical_json(self.data_processor.fingerprint()))
|
|
655
|
+
!= self.task_processor_fingerprint
|
|
656
|
+
):
|
|
657
|
+
raise WorkerFailure("INVALID_TASK_PROCESSOR", "task processor fingerprint changed")
|
|
658
|
+
initial_text = render_playbook(request.initial_bullets)
|
|
659
|
+
roles = self.llm_config.roles
|
|
660
|
+
diagnostics = BoundedHashSink()
|
|
661
|
+
with contextlib.redirect_stdout(diagnostics):
|
|
662
|
+
with self._bound_constructor_environment():
|
|
663
|
+
ace_system = self.ace_class(
|
|
664
|
+
api_provider=self.llm_config.api_provider,
|
|
665
|
+
generator_model=roles["target"].model,
|
|
666
|
+
reflector_model=roles["reflector"].model,
|
|
667
|
+
curator_model=roles["curator"].model,
|
|
668
|
+
max_tokens=self.llm_config.max_tokens_per_call,
|
|
669
|
+
initial_playbook=initial_text,
|
|
670
|
+
use_bulletpoint_analyzer=False,
|
|
671
|
+
)
|
|
672
|
+
if self.llm_config.base_url is not None:
|
|
673
|
+
self._bind_openai_compatible_clients(ace_system, seed=request.controls.seed)
|
|
674
|
+
get_next_id = getattr(self.playbook_utils, "get_next_global_id", None)
|
|
675
|
+
if not callable(get_next_id):
|
|
676
|
+
raise WorkerFailure(
|
|
677
|
+
"UPSTREAM_INTERFACE_MISMATCH",
|
|
678
|
+
"pinned playbook_utils lacks get_next_global_id()",
|
|
679
|
+
)
|
|
680
|
+
ace_system.next_global_id = get_next_id(initial_text)
|
|
681
|
+
tracker = UsageTracker(request.controls, self.llm_config)
|
|
682
|
+
self.last_tracker = tracker
|
|
683
|
+
tracker.install(ace_system)
|
|
684
|
+
train_samples = [adapt_example(item) for item in request.train_examples]
|
|
685
|
+
validation_samples = [adapt_example(item) for item in request.validation_examples]
|
|
686
|
+
controls = request.controls
|
|
687
|
+
if self.llm_config.max_tokens_per_call > controls.max_tokens:
|
|
688
|
+
raise WorkerFailure(
|
|
689
|
+
"INVALID_BUDGET",
|
|
690
|
+
"configured max_tokens_per_call exceeds the request token budget",
|
|
691
|
+
)
|
|
692
|
+
random_state = random.getstate()
|
|
693
|
+
try:
|
|
694
|
+
random.seed(controls.seed)
|
|
695
|
+
with tempfile.TemporaryDirectory(prefix="agentinputlab-ace-") as save_dir:
|
|
696
|
+
upstream_config = {
|
|
697
|
+
"num_epochs": 1,
|
|
698
|
+
"max_num_rounds": controls.max_rounds,
|
|
699
|
+
"curator_frequency": 1,
|
|
700
|
+
"eval_steps": len(train_samples),
|
|
701
|
+
"save_steps": len(train_samples) + 1,
|
|
702
|
+
"playbook_token_budget": controls.max_tokens,
|
|
703
|
+
"task_name": "agentinputlab_generic_text",
|
|
704
|
+
"json_mode": self.llm_config.json_mode,
|
|
705
|
+
"no_ground_truth": False,
|
|
706
|
+
"save_dir": save_dir,
|
|
707
|
+
"test_workers": 1,
|
|
708
|
+
"use_bulletpoint_analyzer": False,
|
|
709
|
+
}
|
|
710
|
+
with contextlib.redirect_stdout(diagnostics):
|
|
711
|
+
raw_result = ace_system.run(
|
|
712
|
+
mode="offline",
|
|
713
|
+
train_samples=train_samples,
|
|
714
|
+
val_samples=validation_samples,
|
|
715
|
+
test_samples=None,
|
|
716
|
+
data_processor=self.data_processor,
|
|
717
|
+
config=upstream_config,
|
|
718
|
+
)
|
|
719
|
+
except UnboundLocalError as exc:
|
|
720
|
+
final = exc.__traceback__
|
|
721
|
+
while final is not None and final.tb_next is not None:
|
|
722
|
+
final = final.tb_next
|
|
723
|
+
if (
|
|
724
|
+
final is not None
|
|
725
|
+
and final.tb_frame.f_code.co_name == "evaluate_test_set"
|
|
726
|
+
and Path(final.tb_frame.f_code.co_filename).resolve() == self.checkout / "utils.py"
|
|
727
|
+
and "final_results" in str(exc)
|
|
728
|
+
):
|
|
729
|
+
if tracker.first_component_failure is not None:
|
|
730
|
+
raise tracker.first_component_failure from exc
|
|
731
|
+
raise WorkerFailure(
|
|
732
|
+
"UPSTREAM_VALIDATION_NO_RESULTS",
|
|
733
|
+
"pinned ACE validation returned no valid results; fit failed, usage retained",
|
|
734
|
+
) from exc
|
|
735
|
+
raise
|
|
736
|
+
finally:
|
|
737
|
+
random.setstate(random_state)
|
|
738
|
+
tracker.require_upstream_chain()
|
|
739
|
+
score = extract_validation_score(raw_result)
|
|
740
|
+
best_playbook = getattr(ace_system, "best_playbook", None)
|
|
741
|
+
if not isinstance(best_playbook, str):
|
|
742
|
+
raise WorkerFailure("UPSTREAM_RESULT_INVALID", "upstream ACE omitted its best playbook")
|
|
743
|
+
candidate = parse_playbook(best_playbook, initial=request.initial_bullets)
|
|
744
|
+
operations = diff_playbooks(request.initial_bullets, candidate)
|
|
745
|
+
return {
|
|
746
|
+
"status": "ok",
|
|
747
|
+
"request_id": request.request_id,
|
|
748
|
+
"protocol_version": PROTOCOL_VERSION,
|
|
749
|
+
"upstream_commit": UPSTREAM_COMMIT,
|
|
750
|
+
"split_audit_hash": request.split_audit_hash,
|
|
751
|
+
"train_ids": [str(item["example_id"]) for item in request.train_examples],
|
|
752
|
+
"validation_ids": [str(item["example_id"]) for item in request.validation_examples],
|
|
753
|
+
"iterations": [
|
|
754
|
+
{
|
|
755
|
+
"index": 0,
|
|
756
|
+
"operations": operations,
|
|
757
|
+
"candidate_playbook_hash": playbook_hash(candidate),
|
|
758
|
+
"validation_score": score,
|
|
759
|
+
}
|
|
760
|
+
],
|
|
761
|
+
"selected_iteration": 0,
|
|
762
|
+
"final_playbook": list(candidate),
|
|
763
|
+
"usage": tracker.as_dict(),
|
|
764
|
+
"diagnostics": {
|
|
765
|
+
**(
|
|
766
|
+
{"operator_task_processor": self.task_processor_fingerprint}
|
|
767
|
+
if self.task_processor_fingerprint is not None
|
|
768
|
+
else {}
|
|
769
|
+
),
|
|
770
|
+
"upstream_stdout_bytes": diagnostics.byte_count,
|
|
771
|
+
"upstream_stdout_sha256": diagnostics.sha256,
|
|
772
|
+
"content_retained": False,
|
|
773
|
+
"provider_binding": (
|
|
774
|
+
"operator_openai_compatible"
|
|
775
|
+
if self.llm_config.base_url is not None
|
|
776
|
+
else "upstream_native"
|
|
777
|
+
),
|
|
778
|
+
},
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
def _bind_openai_compatible_clients(self, ace_system: Any, *, seed: int) -> None:
|
|
782
|
+
"""Replace only upstream client objects using operator-startup configuration."""
|
|
783
|
+
|
|
784
|
+
base_url = self.llm_config.base_url
|
|
785
|
+
api_key_env = self.llm_config.api_key_env
|
|
786
|
+
if base_url is None or api_key_env is None: # pragma: no cover - caller invariant
|
|
787
|
+
raise WorkerFailure("INVALID_LLM_CONFIG", "ACE endpoint binding is incomplete")
|
|
788
|
+
api_key = os.environ.get(api_key_env, "")
|
|
789
|
+
if not api_key:
|
|
790
|
+
raise WorkerFailure(
|
|
791
|
+
"MISSING_API_KEY",
|
|
792
|
+
f"ACE OpenAI-compatible runtime requires environment {api_key_env}",
|
|
793
|
+
)
|
|
794
|
+
try:
|
|
795
|
+
openai_module = importlib.import_module("openai")
|
|
796
|
+
client_class = openai_module.OpenAI
|
|
797
|
+
clients = tuple(
|
|
798
|
+
client_class(api_key=api_key, base_url=base_url, max_retries=0) for _ in _ROLE_NAMES
|
|
799
|
+
)
|
|
800
|
+
except (AttributeError, ImportError, TypeError, ValueError) as exc:
|
|
801
|
+
raise WorkerFailure(
|
|
802
|
+
"OPENAI_COMPATIBLE_CLIENT_UNAVAILABLE",
|
|
803
|
+
"ACE could not initialize its operator-bound OpenAI-compatible clients",
|
|
804
|
+
) from exc
|
|
805
|
+
extra_body = dict(self.llm_config.extra_body or {})
|
|
806
|
+
if "seed" in extra_body and (
|
|
807
|
+
type(extra_body["seed"]) is not int or extra_body["seed"] != seed
|
|
808
|
+
):
|
|
809
|
+
raise WorkerFailure(
|
|
810
|
+
"INVALID_LLM_CONFIG", "deployment seed differs from the fit controls"
|
|
811
|
+
)
|
|
812
|
+
extra_body["seed"] = seed
|
|
813
|
+
clients = tuple(CompletionDefaultClient(client, extra_body) for client in clients)
|
|
814
|
+
components = (
|
|
815
|
+
ace_system.generator,
|
|
816
|
+
ace_system.reflector,
|
|
817
|
+
ace_system.curator,
|
|
818
|
+
)
|
|
819
|
+
for component, client in zip(components, clients, strict=True):
|
|
820
|
+
component.api_client = client
|
|
821
|
+
ace_system.generator_client = clients[0]
|
|
822
|
+
ace_system.reflector_client = clients[1]
|
|
823
|
+
ace_system.curator_client = clients[2]
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
def load_compatibility_policy() -> CompatibilityPolicy:
|
|
827
|
+
path = Path(__file__).with_name("compatibility_manifest.json")
|
|
828
|
+
try:
|
|
829
|
+
raw = _loads_strict_json(path.read_text(encoding="utf-8"))
|
|
830
|
+
except OSError as exc:
|
|
831
|
+
raise WorkerFailure(
|
|
832
|
+
"COMPATIBILITY_MANIFEST_UNAVAILABLE", "ACE compatibility manifest is unavailable"
|
|
833
|
+
) from exc
|
|
834
|
+
return CompatibilityPolicy.from_mapping(_require_mapping(raw, "compatibility manifest"))
|
|
835
|
+
|
|
836
|
+
|
|
837
|
+
def verify_checkout(checkout: Path, policy: CompatibilityPolicy) -> Path:
|
|
838
|
+
try:
|
|
839
|
+
root = checkout.expanduser().resolve(strict=True)
|
|
840
|
+
except OSError as exc:
|
|
841
|
+
raise WorkerFailure("CHECKOUT_UNAVAILABLE", "ACE checkout does not exist") from exc
|
|
842
|
+
if not root.is_dir():
|
|
843
|
+
raise WorkerFailure("CHECKOUT_UNAVAILABLE", "ACE checkout must be a directory")
|
|
844
|
+
if read_checkout_head(root) != policy.commit:
|
|
845
|
+
raise WorkerFailure(
|
|
846
|
+
"UPSTREAM_REVISION_MISMATCH", "ACE checkout is not at the pinned commit"
|
|
847
|
+
)
|
|
848
|
+
license_file = safe_checkout_file(root, policy.license_path)
|
|
849
|
+
if file_sha256(license_file) != policy.license_sha256:
|
|
850
|
+
raise WorkerFailure("UPSTREAM_LICENSE_MISMATCH", "ACE license hash does not match")
|
|
851
|
+
license_text = license_file.read_text(encoding="utf-8", errors="strict")[:500]
|
|
852
|
+
if "Apache License" not in license_text or "Version 2.0" not in license_text:
|
|
853
|
+
raise WorkerFailure(
|
|
854
|
+
"UPSTREAM_LICENSE_MISMATCH", "ACE checkout does not contain Apache-2.0 text"
|
|
855
|
+
)
|
|
856
|
+
source_paths: list[Path] = []
|
|
857
|
+
for relative, expected in policy.source_hashes.items():
|
|
858
|
+
source = safe_checkout_file(root, relative)
|
|
859
|
+
if file_sha256(source) != expected:
|
|
860
|
+
raise WorkerFailure("UPSTREAM_SOURCE_MISMATCH", f"ACE source hash mismatch: {relative}")
|
|
861
|
+
source_paths.append(source)
|
|
862
|
+
reject_appworld_imports(source_paths)
|
|
863
|
+
if policy.require_clean_checkout:
|
|
864
|
+
require_clean_git_checkout(root)
|
|
865
|
+
return root
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
def require_clean_git_checkout(root: Path) -> None:
|
|
869
|
+
"""Reject tracked or untracked checkout changes with a fixed Git query."""
|
|
870
|
+
|
|
871
|
+
environment = {
|
|
872
|
+
"PATH": os.environ.get("PATH", ""),
|
|
873
|
+
"LC_ALL": "C",
|
|
874
|
+
"GIT_CONFIG_NOSYSTEM": "1",
|
|
875
|
+
"GIT_CONFIG_GLOBAL": os.devnull,
|
|
876
|
+
"GIT_OPTIONAL_LOCKS": "0",
|
|
877
|
+
}
|
|
878
|
+
try:
|
|
879
|
+
result = subprocess.run(
|
|
880
|
+
(
|
|
881
|
+
"git",
|
|
882
|
+
"-c",
|
|
883
|
+
"core.fsmonitor=false",
|
|
884
|
+
"-C",
|
|
885
|
+
str(root),
|
|
886
|
+
"status",
|
|
887
|
+
"--porcelain=v1",
|
|
888
|
+
"--untracked-files=all",
|
|
889
|
+
),
|
|
890
|
+
check=False,
|
|
891
|
+
capture_output=True,
|
|
892
|
+
text=True,
|
|
893
|
+
encoding="utf-8",
|
|
894
|
+
errors="replace",
|
|
895
|
+
env=environment,
|
|
896
|
+
timeout=15,
|
|
897
|
+
)
|
|
898
|
+
except (OSError, subprocess.SubprocessError) as exc:
|
|
899
|
+
raise WorkerFailure(
|
|
900
|
+
"GIT_STATUS_FAILED", "ACE clean-checkout verification could not run"
|
|
901
|
+
) from exc
|
|
902
|
+
if result.returncode != 0:
|
|
903
|
+
raise WorkerFailure("GIT_STATUS_FAILED", "ACE clean-checkout verification failed")
|
|
904
|
+
if result.stdout:
|
|
905
|
+
raise WorkerFailure("DIRTY_CHECKOUT", "ACE checkout contains tracked or untracked changes")
|
|
906
|
+
|
|
907
|
+
|
|
908
|
+
def read_checkout_head(root: Path) -> str:
|
|
909
|
+
git_entry = root / ".git"
|
|
910
|
+
if git_entry.is_dir():
|
|
911
|
+
git_dir = git_entry.resolve(strict=True)
|
|
912
|
+
elif git_entry.is_file():
|
|
913
|
+
raw = git_entry.read_text(encoding="utf-8").strip()
|
|
914
|
+
if not raw.startswith("gitdir: "):
|
|
915
|
+
raise WorkerFailure("GIT_METADATA_INVALID", "ACE .git file is malformed")
|
|
916
|
+
candidate = Path(raw.removeprefix("gitdir: "))
|
|
917
|
+
git_dir = (
|
|
918
|
+
(root / candidate).resolve(strict=True)
|
|
919
|
+
if not candidate.is_absolute()
|
|
920
|
+
else candidate.resolve(strict=True)
|
|
921
|
+
)
|
|
922
|
+
else:
|
|
923
|
+
raise WorkerFailure("GIT_METADATA_INVALID", "ACE checkout lacks Git metadata")
|
|
924
|
+
head = (git_dir / "HEAD").read_text(encoding="utf-8").strip()
|
|
925
|
+
if _COMMIT_RE.fullmatch(head):
|
|
926
|
+
return head
|
|
927
|
+
if not head.startswith("ref: "):
|
|
928
|
+
raise WorkerFailure("GIT_METADATA_INVALID", "ACE HEAD is malformed")
|
|
929
|
+
ref = head.removeprefix("ref: ")
|
|
930
|
+
if not _REF_RE.fullmatch(ref) or ".." in PurePosixPath(ref).parts:
|
|
931
|
+
raise WorkerFailure("GIT_METADATA_INVALID", "ACE HEAD reference is unsafe")
|
|
932
|
+
common_dir = git_dir
|
|
933
|
+
common_file = git_dir / "commondir"
|
|
934
|
+
if common_file.is_file():
|
|
935
|
+
common_candidate = Path(common_file.read_text(encoding="utf-8").strip())
|
|
936
|
+
common_dir = (
|
|
937
|
+
(git_dir / common_candidate).resolve(strict=True)
|
|
938
|
+
if not common_candidate.is_absolute()
|
|
939
|
+
else common_candidate.resolve(strict=True)
|
|
940
|
+
)
|
|
941
|
+
for base in (git_dir, common_dir):
|
|
942
|
+
loose = safe_child(base, ref)
|
|
943
|
+
if loose.is_file():
|
|
944
|
+
value = loose.read_text(encoding="utf-8").strip()
|
|
945
|
+
if _COMMIT_RE.fullmatch(value):
|
|
946
|
+
return value
|
|
947
|
+
packed = base / "packed-refs"
|
|
948
|
+
if packed.is_file():
|
|
949
|
+
for line in packed.read_text(encoding="utf-8").splitlines():
|
|
950
|
+
if not line or line.startswith(("#", "^")):
|
|
951
|
+
continue
|
|
952
|
+
value, separator, name = line.partition(" ")
|
|
953
|
+
if separator and name == ref and _COMMIT_RE.fullmatch(value):
|
|
954
|
+
return value
|
|
955
|
+
raise WorkerFailure("GIT_METADATA_INVALID", "ACE HEAD commit cannot be resolved")
|
|
956
|
+
|
|
957
|
+
|
|
958
|
+
def safe_checkout_file(root: Path, relative: str) -> Path:
|
|
959
|
+
path = safe_child(root, relative)
|
|
960
|
+
if not path.is_file() or path.is_symlink():
|
|
961
|
+
raise WorkerFailure(
|
|
962
|
+
"UPSTREAM_FILE_UNAVAILABLE", f"ACE pinned file is missing or unsafe: {relative}"
|
|
963
|
+
)
|
|
964
|
+
return path
|
|
965
|
+
|
|
966
|
+
|
|
967
|
+
def safe_child(root: Path, relative: str) -> Path:
|
|
968
|
+
candidate = (root / relative).resolve(strict=False)
|
|
969
|
+
if candidate != root and root not in candidate.parents:
|
|
970
|
+
raise WorkerFailure("UNSAFE_PATH", "path escapes its fixed root")
|
|
971
|
+
return candidate
|
|
972
|
+
|
|
973
|
+
|
|
974
|
+
def reject_appworld_imports(paths: Sequence[Path]) -> None:
|
|
975
|
+
for path in paths:
|
|
976
|
+
if path.suffix != ".py":
|
|
977
|
+
continue
|
|
978
|
+
try:
|
|
979
|
+
tree = ast.parse(path.read_text(encoding="utf-8"), filename=path.name)
|
|
980
|
+
except (OSError, SyntaxError, UnicodeError) as exc:
|
|
981
|
+
raise WorkerFailure(
|
|
982
|
+
"UPSTREAM_SOURCE_INVALID", "ACE pinned Python source cannot be parsed"
|
|
983
|
+
) from exc
|
|
984
|
+
for node in ast.walk(tree):
|
|
985
|
+
names: tuple[str, ...] = ()
|
|
986
|
+
if isinstance(node, ast.Import):
|
|
987
|
+
names = tuple(alias.name for alias in node.names)
|
|
988
|
+
elif isinstance(node, ast.ImportFrom) and node.module:
|
|
989
|
+
names = (node.module,)
|
|
990
|
+
if any(_is_appworld_module(name) for name in names):
|
|
991
|
+
raise WorkerFailure("APPWORLD_FORBIDDEN", "ACE worker refuses to import AppWorld")
|
|
992
|
+
|
|
993
|
+
|
|
994
|
+
def load_upstream(checkout: Path) -> tuple[type[Any], Any]:
|
|
995
|
+
checkout = checkout.resolve(strict=True)
|
|
996
|
+
for name in tuple(sys.modules):
|
|
997
|
+
if any(name == root or name.startswith(f"{root}.") for root in _UPSTREAM_MODULES):
|
|
998
|
+
del sys.modules[name]
|
|
999
|
+
old_dont_write = sys.dont_write_bytecode
|
|
1000
|
+
had_dotenv_setting = "PYTHON_DOTENV_DISABLED" in os.environ
|
|
1001
|
+
old_dotenv_setting = os.environ.get("PYTHON_DOTENV_DISABLED")
|
|
1002
|
+
sys.dont_write_bytecode = True
|
|
1003
|
+
os.environ["PYTHON_DOTENV_DISABLED"] = "1"
|
|
1004
|
+
sys.path.insert(0, str(checkout))
|
|
1005
|
+
before = set(sys.modules)
|
|
1006
|
+
try:
|
|
1007
|
+
with contextlib.redirect_stdout(BoundedHashSink()):
|
|
1008
|
+
module = importlib.import_module("ace.ace")
|
|
1009
|
+
playbook_utils = importlib.import_module("playbook_utils")
|
|
1010
|
+
except ModuleNotFoundError as exc:
|
|
1011
|
+
if exc.name in {"numpy", "openai", "dotenv", "tiktoken"}:
|
|
1012
|
+
raise WorkerFailure(
|
|
1013
|
+
"UPSTREAM_DEPENDENCY_MISSING",
|
|
1014
|
+
"install agentinputlab[ace] in the isolated worker environment",
|
|
1015
|
+
) from None
|
|
1016
|
+
raise WorkerFailure(
|
|
1017
|
+
"UPSTREAM_IMPORT_FAILED", "pinned ACE import requires its verified source dependencies"
|
|
1018
|
+
) from None
|
|
1019
|
+
except Exception as exc:
|
|
1020
|
+
raise WorkerFailure(
|
|
1021
|
+
"UPSTREAM_IMPORT_FAILED",
|
|
1022
|
+
f"pinned ACE import failed: {type(exc).__name__}: {exc}",
|
|
1023
|
+
) from exc
|
|
1024
|
+
finally:
|
|
1025
|
+
sys.path.remove(str(checkout))
|
|
1026
|
+
sys.dont_write_bytecode = old_dont_write
|
|
1027
|
+
if had_dotenv_setting:
|
|
1028
|
+
assert old_dotenv_setting is not None
|
|
1029
|
+
os.environ["PYTHON_DOTENV_DISABLED"] = old_dotenv_setting
|
|
1030
|
+
else:
|
|
1031
|
+
os.environ.pop("PYTHON_DOTENV_DISABLED", None)
|
|
1032
|
+
imported = set(sys.modules) - before
|
|
1033
|
+
if any(_is_appworld_module(name) for name in imported):
|
|
1034
|
+
raise WorkerFailure("APPWORLD_FORBIDDEN", "ACE worker imported AppWorld")
|
|
1035
|
+
for imported_module in (module, playbook_utils):
|
|
1036
|
+
origin = getattr(imported_module, "__file__", None)
|
|
1037
|
+
if not isinstance(origin, str):
|
|
1038
|
+
raise WorkerFailure(
|
|
1039
|
+
"UPSTREAM_IMPORT_MISMATCH", "ACE upstream module has no source origin"
|
|
1040
|
+
)
|
|
1041
|
+
resolved = Path(origin).resolve(strict=True)
|
|
1042
|
+
if checkout not in resolved.parents:
|
|
1043
|
+
raise WorkerFailure(
|
|
1044
|
+
"UPSTREAM_IMPORT_MISMATCH", "ACE upstream module came from another checkout"
|
|
1045
|
+
)
|
|
1046
|
+
ace_class = getattr(module, "ACE", None)
|
|
1047
|
+
if not isinstance(ace_class, type) or not callable(getattr(ace_class, "run", None)):
|
|
1048
|
+
raise WorkerFailure("UPSTREAM_INTERFACE_MISMATCH", "pinned ace.ace.ACE.run is unavailable")
|
|
1049
|
+
return ace_class, playbook_utils
|
|
1050
|
+
|
|
1051
|
+
|
|
1052
|
+
def validate_request(payload: Mapping[str, Any], llm_config: LLMConfig) -> ValidatedRequest:
|
|
1053
|
+
_require_exact_keys(
|
|
1054
|
+
payload,
|
|
1055
|
+
{
|
|
1056
|
+
"request_id",
|
|
1057
|
+
"train_examples",
|
|
1058
|
+
"validation_examples",
|
|
1059
|
+
"initial_playbook",
|
|
1060
|
+
"controls",
|
|
1061
|
+
"model_fingerprints",
|
|
1062
|
+
"split_audit_hash",
|
|
1063
|
+
"upstream_commit",
|
|
1064
|
+
"protocol_version",
|
|
1065
|
+
},
|
|
1066
|
+
"ACE request",
|
|
1067
|
+
)
|
|
1068
|
+
if payload["upstream_commit"] != UPSTREAM_COMMIT:
|
|
1069
|
+
raise WorkerFailure(
|
|
1070
|
+
"UPSTREAM_REVISION_MISMATCH", "ACE request targets another upstream commit"
|
|
1071
|
+
)
|
|
1072
|
+
if payload["protocol_version"] != PROTOCOL_VERSION:
|
|
1073
|
+
raise WorkerFailure("PROTOCOL_MISMATCH", "ACE request protocol version mismatch")
|
|
1074
|
+
request_id = _require_hash(payload["request_id"], "ACE request_id")
|
|
1075
|
+
split_audit_hash = _require_hash(payload["split_audit_hash"], "ACE split_audit_hash")
|
|
1076
|
+
train = validate_examples(payload["train_examples"], "train")
|
|
1077
|
+
validation = validate_examples(payload["validation_examples"], "validation")
|
|
1078
|
+
train_ids = tuple(str(item["example_id"]) for item in train)
|
|
1079
|
+
validation_ids = tuple(str(item["example_id"]) for item in validation)
|
|
1080
|
+
if set(train_ids) & set(validation_ids):
|
|
1081
|
+
raise WorkerFailure("SPLIT_OVERLAP", "ACE train and validation IDs overlap")
|
|
1082
|
+
initial = validate_initial_playbook(payload["initial_playbook"])
|
|
1083
|
+
controls = validate_controls(payload["controls"])
|
|
1084
|
+
if llm_config.base_url is not None and "seed" in (llm_config.extra_body or {}):
|
|
1085
|
+
declared_seed = llm_config.extra_body["seed"]
|
|
1086
|
+
if type(declared_seed) is not int or declared_seed != controls.seed:
|
|
1087
|
+
raise WorkerFailure(
|
|
1088
|
+
"INVALID_LLM_CONFIG", "deployment seed differs from the fit controls"
|
|
1089
|
+
)
|
|
1090
|
+
validate_model_fingerprints(payload["model_fingerprints"], llm_config)
|
|
1091
|
+
expected_split_hash = split_hash(train, validation)
|
|
1092
|
+
if split_audit_hash != expected_split_hash:
|
|
1093
|
+
raise WorkerFailure("SPLIT_AUDIT_MISMATCH", "ACE split audit hash is invalid")
|
|
1094
|
+
request_base = {
|
|
1095
|
+
"train_ids": train_ids,
|
|
1096
|
+
"validation_ids": validation_ids,
|
|
1097
|
+
"train_examples": train,
|
|
1098
|
+
"validation_examples": validation,
|
|
1099
|
+
"initial_playbook_hash": playbook_hash(initial),
|
|
1100
|
+
# Preserve the request's numeric JSON representation (for example,
|
|
1101
|
+
# ``1`` versus ``1.0``) because the recipe hashes the pre-normalized
|
|
1102
|
+
# control mapping when it constructs request_id.
|
|
1103
|
+
"controls": dict(_require_mapping(payload["controls"], "ACE controls")),
|
|
1104
|
+
"model_fingerprints": payload["model_fingerprints"],
|
|
1105
|
+
"split_audit_hash": split_audit_hash,
|
|
1106
|
+
"upstream_commit": UPSTREAM_COMMIT,
|
|
1107
|
+
}
|
|
1108
|
+
if request_id != canonical_hash(request_base):
|
|
1109
|
+
raise WorkerFailure("REQUEST_ID_MISMATCH", "ACE request_id is invalid")
|
|
1110
|
+
return ValidatedRequest(
|
|
1111
|
+
request_id=request_id,
|
|
1112
|
+
train_examples=train,
|
|
1113
|
+
validation_examples=validation,
|
|
1114
|
+
initial_bullets=initial,
|
|
1115
|
+
controls=controls,
|
|
1116
|
+
split_audit_hash=split_audit_hash,
|
|
1117
|
+
)
|
|
1118
|
+
|
|
1119
|
+
|
|
1120
|
+
def validate_examples(value: Any, split: str) -> tuple[Mapping[str, Any], ...]:
|
|
1121
|
+
if not isinstance(value, Sequence) or isinstance(value, (str, bytes)) or not value:
|
|
1122
|
+
raise WorkerFailure("INVALID_EXAMPLES", f"ACE {split} examples must be non-empty")
|
|
1123
|
+
examples: list[Mapping[str, Any]] = []
|
|
1124
|
+
ids: list[str] = []
|
|
1125
|
+
for index, raw in enumerate(value):
|
|
1126
|
+
item = _require_mapping(raw, f"ACE {split} example {index}")
|
|
1127
|
+
_require_exact_keys(
|
|
1128
|
+
item,
|
|
1129
|
+
{"example_id", "input", "expected_output", "split", "metadata"},
|
|
1130
|
+
f"ACE {split} example {index}",
|
|
1131
|
+
)
|
|
1132
|
+
example_id = _require_text(item["example_id"], f"ACE {split} example_id")
|
|
1133
|
+
if item["split"] != split:
|
|
1134
|
+
raise WorkerFailure("INVALID_EXAMPLES", f"ACE {split} example has wrong split")
|
|
1135
|
+
if item["expected_output"] is None:
|
|
1136
|
+
raise WorkerFailure("INVALID_EXAMPLES", f"ACE {split} example requires expected_output")
|
|
1137
|
+
_canonical_json(item["input"])
|
|
1138
|
+
_canonical_json(item["expected_output"])
|
|
1139
|
+
_canonical_json(_require_mapping(item["metadata"], "ACE example metadata"))
|
|
1140
|
+
examples.append(dict(item))
|
|
1141
|
+
ids.append(example_id)
|
|
1142
|
+
if len(ids) != len(set(ids)):
|
|
1143
|
+
raise WorkerFailure("INVALID_EXAMPLES", f"ACE {split} example IDs must be unique")
|
|
1144
|
+
return tuple(examples)
|
|
1145
|
+
|
|
1146
|
+
|
|
1147
|
+
def validate_initial_playbook(value: Any) -> tuple[dict[str, Any], ...]:
|
|
1148
|
+
playbook = _require_mapping(value, "ACE initial_playbook")
|
|
1149
|
+
_require_exact_keys(playbook, {"bullets"}, "ACE initial_playbook")
|
|
1150
|
+
raw_bullets = playbook["bullets"]
|
|
1151
|
+
if not isinstance(raw_bullets, Sequence) or isinstance(raw_bullets, (str, bytes)):
|
|
1152
|
+
raise WorkerFailure("INVALID_PLAYBOOK", "ACE initial bullets must be a sequence")
|
|
1153
|
+
bullets: list[dict[str, Any]] = []
|
|
1154
|
+
ids: list[str] = []
|
|
1155
|
+
for index, raw in enumerate(raw_bullets):
|
|
1156
|
+
item = _require_mapping(raw, f"ACE initial bullet {index}")
|
|
1157
|
+
_require_exact_keys(
|
|
1158
|
+
item,
|
|
1159
|
+
{"bullet_id", "content", "helpful", "harmful", "metadata"},
|
|
1160
|
+
f"ACE initial bullet {index}",
|
|
1161
|
+
)
|
|
1162
|
+
bullet_id = _require_text(item["bullet_id"], "ACE bullet_id")
|
|
1163
|
+
content = _require_text(item["content"], "ACE bullet content")
|
|
1164
|
+
if any(character in bullet_id for character in ("\n", "\r", "]")):
|
|
1165
|
+
raise WorkerFailure("INVALID_PLAYBOOK", "ACE bullet_id is not line-safe")
|
|
1166
|
+
if "\n" in content or "\r" in content:
|
|
1167
|
+
raise WorkerFailure("INVALID_PLAYBOOK", "ACE bullet content must be one line")
|
|
1168
|
+
helpful = _require_non_negative_int(item["helpful"], "ACE helpful counter")
|
|
1169
|
+
harmful = _require_non_negative_int(item["harmful"], "ACE harmful counter")
|
|
1170
|
+
metadata = _require_mapping(item["metadata"], "ACE bullet metadata")
|
|
1171
|
+
_canonical_json(metadata)
|
|
1172
|
+
bullets.append(
|
|
1173
|
+
{
|
|
1174
|
+
"bullet_id": bullet_id,
|
|
1175
|
+
"content": content,
|
|
1176
|
+
"helpful": helpful,
|
|
1177
|
+
"harmful": harmful,
|
|
1178
|
+
"metadata": dict(metadata),
|
|
1179
|
+
}
|
|
1180
|
+
)
|
|
1181
|
+
ids.append(bullet_id)
|
|
1182
|
+
if len(ids) != len(set(ids)):
|
|
1183
|
+
raise WorkerFailure("INVALID_PLAYBOOK", "ACE bullet IDs must be unique")
|
|
1184
|
+
return tuple(bullets)
|
|
1185
|
+
|
|
1186
|
+
|
|
1187
|
+
def validate_controls(value: Any) -> Controls:
|
|
1188
|
+
controls = _require_mapping(value, "ACE controls")
|
|
1189
|
+
_require_exact_keys(
|
|
1190
|
+
controls,
|
|
1191
|
+
{"max_rounds", "max_model_calls", "max_tokens", "max_cost_usd", "seed"},
|
|
1192
|
+
"ACE controls",
|
|
1193
|
+
)
|
|
1194
|
+
return Controls(
|
|
1195
|
+
max_rounds=_require_positive_int(controls["max_rounds"], "ACE max_rounds"),
|
|
1196
|
+
max_model_calls=_require_positive_int(controls["max_model_calls"], "ACE max_model_calls"),
|
|
1197
|
+
max_tokens=_require_positive_int(controls["max_tokens"], "ACE max_tokens"),
|
|
1198
|
+
max_cost_usd=_require_non_negative_float(controls["max_cost_usd"], "ACE max_cost_usd"),
|
|
1199
|
+
seed=_require_non_negative_int(controls["seed"], "ACE seed"),
|
|
1200
|
+
)
|
|
1201
|
+
|
|
1202
|
+
|
|
1203
|
+
def validate_model_fingerprints(value: Any, config: LLMConfig) -> None:
|
|
1204
|
+
fingerprints = _require_mapping(value, "ACE model_fingerprints")
|
|
1205
|
+
if set(fingerprints) != set(_ROLE_NAMES):
|
|
1206
|
+
raise WorkerFailure(
|
|
1207
|
+
"MODEL_FINGERPRINT_MISMATCH",
|
|
1208
|
+
"ACE request must bind target, reflector, and curator fingerprints",
|
|
1209
|
+
)
|
|
1210
|
+
for role in _ROLE_NAMES:
|
|
1211
|
+
supplied = _require_mapping(fingerprints[role], f"ACE {role} fingerprint")
|
|
1212
|
+
if _canonical_json(supplied) != _canonical_json(config.roles[role].fingerprint):
|
|
1213
|
+
raise WorkerFailure(
|
|
1214
|
+
"MODEL_FINGERPRINT_MISMATCH",
|
|
1215
|
+
f"ACE {role} fingerprint differs from worker startup configuration",
|
|
1216
|
+
)
|
|
1217
|
+
|
|
1218
|
+
|
|
1219
|
+
def split_hash(train: Sequence[Mapping[str, Any]], validation: Sequence[Mapping[str, Any]]) -> str:
|
|
1220
|
+
value = {
|
|
1221
|
+
"train_ids": [str(item["example_id"]) for item in train],
|
|
1222
|
+
"validation_ids": [str(item["example_id"]) for item in validation],
|
|
1223
|
+
"test_ids": [],
|
|
1224
|
+
"train_input_hashes": [canonical_hash(item["input"]) for item in train],
|
|
1225
|
+
"validation_input_hashes": [canonical_hash(item["input"]) for item in validation],
|
|
1226
|
+
"test_input_hashes": [],
|
|
1227
|
+
}
|
|
1228
|
+
return canonical_hash(value)
|
|
1229
|
+
|
|
1230
|
+
|
|
1231
|
+
def adapt_example(item: Mapping[str, Any]) -> dict[str, str]:
|
|
1232
|
+
raw_input = item["input"]
|
|
1233
|
+
context = ""
|
|
1234
|
+
if isinstance(raw_input, str):
|
|
1235
|
+
question = raw_input
|
|
1236
|
+
elif isinstance(raw_input, Mapping):
|
|
1237
|
+
aliases = [
|
|
1238
|
+
name
|
|
1239
|
+
for name in ("question", "query", "prompt", "instruction", "q")
|
|
1240
|
+
if name in raw_input
|
|
1241
|
+
]
|
|
1242
|
+
if len(aliases) > 1:
|
|
1243
|
+
raise WorkerFailure(
|
|
1244
|
+
"AMBIGUOUS_TEXT_INPUT", "ACE text input contains multiple question fields"
|
|
1245
|
+
)
|
|
1246
|
+
question = render_text(raw_input[aliases[0]]) if aliases else render_text(raw_input)
|
|
1247
|
+
if "context" in raw_input:
|
|
1248
|
+
context = render_text(raw_input["context"])
|
|
1249
|
+
else:
|
|
1250
|
+
question = render_text(raw_input)
|
|
1251
|
+
if not question.strip():
|
|
1252
|
+
raise WorkerFailure("INVALID_TEXT_INPUT", "ACE text question must be non-empty")
|
|
1253
|
+
target = render_text(item["expected_output"])
|
|
1254
|
+
if not target.strip():
|
|
1255
|
+
raise WorkerFailure("INVALID_TEXT_INPUT", "ACE text target must be non-empty")
|
|
1256
|
+
return {
|
|
1257
|
+
"example_id": str(item["example_id"]),
|
|
1258
|
+
"context": context,
|
|
1259
|
+
"question": question,
|
|
1260
|
+
"target": target,
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
|
|
1264
|
+
def render_text(value: Any) -> str:
|
|
1265
|
+
return value if isinstance(value, str) else _canonical_json(value)
|
|
1266
|
+
|
|
1267
|
+
|
|
1268
|
+
def render_playbook(bullets: Sequence[Mapping[str, Any]]) -> str:
|
|
1269
|
+
grouped: dict[str, list[str]] = {section: [] for section in _STANDARD_SECTIONS}
|
|
1270
|
+
for bullet in bullets:
|
|
1271
|
+
metadata = _require_mapping(bullet.get("metadata", {}), "ACE bullet metadata")
|
|
1272
|
+
raw_section = metadata.get("section", "OTHERS")
|
|
1273
|
+
section = normalize_section(raw_section)
|
|
1274
|
+
grouped[section].append(
|
|
1275
|
+
f"[{bullet['bullet_id']}] helpful={bullet['helpful']} "
|
|
1276
|
+
f"harmful={bullet['harmful']} :: {bullet['content']}"
|
|
1277
|
+
)
|
|
1278
|
+
lines: list[str] = []
|
|
1279
|
+
for section in _STANDARD_SECTIONS:
|
|
1280
|
+
if lines:
|
|
1281
|
+
lines.append("")
|
|
1282
|
+
lines.append(f"## {section}")
|
|
1283
|
+
lines.extend(grouped[section])
|
|
1284
|
+
return "\n".join(lines)
|
|
1285
|
+
|
|
1286
|
+
|
|
1287
|
+
def normalize_section(value: Any) -> str:
|
|
1288
|
+
if not isinstance(value, str):
|
|
1289
|
+
raise WorkerFailure("INVALID_PLAYBOOK", "ACE bullet section must be text")
|
|
1290
|
+
normalized = value.strip().upper().replace("_", " ")
|
|
1291
|
+
normalized = re.sub(r"\s+", " ", normalized)
|
|
1292
|
+
aliases = {
|
|
1293
|
+
"STRATEGIES AND INSIGHTS": "STRATEGIES & INSIGHTS",
|
|
1294
|
+
"FORMULAS AND CALCULATIONS": "FORMULAS & CALCULATIONS",
|
|
1295
|
+
"CODE SNIPPETS AND TEMPLATES": "CODE SNIPPETS & TEMPLATES",
|
|
1296
|
+
}
|
|
1297
|
+
normalized = aliases.get(normalized, normalized)
|
|
1298
|
+
return normalized if normalized in _STANDARD_SECTIONS else "OTHERS"
|
|
1299
|
+
|
|
1300
|
+
|
|
1301
|
+
def parse_playbook(
|
|
1302
|
+
text: str, *, initial: Sequence[Mapping[str, Any]]
|
|
1303
|
+
) -> tuple[dict[str, Any], ...]:
|
|
1304
|
+
initial_by_id = {str(item["bullet_id"]): item for item in initial}
|
|
1305
|
+
parsed_by_id: dict[str, dict[str, Any]] = {}
|
|
1306
|
+
parsed_order: list[str] = []
|
|
1307
|
+
section = "OTHERS"
|
|
1308
|
+
for raw_line in text.splitlines():
|
|
1309
|
+
line = raw_line.strip()
|
|
1310
|
+
if not line:
|
|
1311
|
+
continue
|
|
1312
|
+
if line.startswith("##"):
|
|
1313
|
+
section = line.removeprefix("##").strip() or "OTHERS"
|
|
1314
|
+
continue
|
|
1315
|
+
match = _PLAYBOOK_LINE_RE.fullmatch(line)
|
|
1316
|
+
if match is None:
|
|
1317
|
+
raise WorkerFailure(
|
|
1318
|
+
"UPSTREAM_PLAYBOOK_INVALID", "upstream ACE emitted an unparseable playbook line"
|
|
1319
|
+
)
|
|
1320
|
+
bullet_id, helpful, harmful, content = match.groups()
|
|
1321
|
+
if not content.strip():
|
|
1322
|
+
raise WorkerFailure("UPSTREAM_PLAYBOOK_INVALID", "upstream ACE emitted an empty bullet")
|
|
1323
|
+
if bullet_id in parsed_by_id:
|
|
1324
|
+
raise WorkerFailure(
|
|
1325
|
+
"UPSTREAM_PLAYBOOK_INVALID", "upstream ACE emitted duplicate bullet IDs"
|
|
1326
|
+
)
|
|
1327
|
+
old = initial_by_id.get(bullet_id)
|
|
1328
|
+
metadata = dict(old.get("metadata", {})) if old is not None else {"section": section}
|
|
1329
|
+
parsed_by_id[bullet_id] = {
|
|
1330
|
+
"bullet_id": bullet_id,
|
|
1331
|
+
"content": content,
|
|
1332
|
+
"helpful": int(helpful),
|
|
1333
|
+
"harmful": int(harmful),
|
|
1334
|
+
"metadata": metadata,
|
|
1335
|
+
}
|
|
1336
|
+
parsed_order.append(bullet_id)
|
|
1337
|
+
stable_order = [
|
|
1338
|
+
str(item["bullet_id"]) for item in initial if str(item["bullet_id"]) in parsed_by_id
|
|
1339
|
+
]
|
|
1340
|
+
stable_order.extend(item for item in parsed_order if item not in initial_by_id)
|
|
1341
|
+
return tuple(parsed_by_id[bullet_id] for bullet_id in stable_order)
|
|
1342
|
+
|
|
1343
|
+
|
|
1344
|
+
def diff_playbooks(
|
|
1345
|
+
initial: Sequence[Mapping[str, Any]], candidate: Sequence[Mapping[str, Any]]
|
|
1346
|
+
) -> list[dict[str, Any]]:
|
|
1347
|
+
initial_by_id = {str(item["bullet_id"]): item for item in initial}
|
|
1348
|
+
candidate_by_id = {str(item["bullet_id"]): item for item in candidate}
|
|
1349
|
+
operations: list[dict[str, Any]] = []
|
|
1350
|
+
for item in initial:
|
|
1351
|
+
bullet_id = str(item["bullet_id"])
|
|
1352
|
+
replacement = candidate_by_id.get(bullet_id)
|
|
1353
|
+
if replacement is None:
|
|
1354
|
+
operations.append({"operation": "delete", "bullet_id": bullet_id})
|
|
1355
|
+
elif any(
|
|
1356
|
+
item[key] != replacement[key] for key in ("content", "helpful", "harmful", "metadata")
|
|
1357
|
+
):
|
|
1358
|
+
operations.append(
|
|
1359
|
+
{
|
|
1360
|
+
"operation": "edit",
|
|
1361
|
+
"bullet_id": bullet_id,
|
|
1362
|
+
"content": replacement["content"],
|
|
1363
|
+
"helpful": replacement["helpful"],
|
|
1364
|
+
"harmful": replacement["harmful"],
|
|
1365
|
+
"metadata": replacement["metadata"],
|
|
1366
|
+
}
|
|
1367
|
+
)
|
|
1368
|
+
for item in candidate:
|
|
1369
|
+
bullet_id = str(item["bullet_id"])
|
|
1370
|
+
if bullet_id not in initial_by_id:
|
|
1371
|
+
operations.append(
|
|
1372
|
+
{
|
|
1373
|
+
"operation": "add",
|
|
1374
|
+
"bullet_id": bullet_id,
|
|
1375
|
+
"content": item["content"],
|
|
1376
|
+
"helpful": item["helpful"],
|
|
1377
|
+
"harmful": item["harmful"],
|
|
1378
|
+
"metadata": item["metadata"],
|
|
1379
|
+
}
|
|
1380
|
+
)
|
|
1381
|
+
return operations
|
|
1382
|
+
|
|
1383
|
+
|
|
1384
|
+
def playbook_hash(bullets: Sequence[Mapping[str, Any]]) -> str:
|
|
1385
|
+
return canonical_hash({"bullets": list(bullets)})
|
|
1386
|
+
|
|
1387
|
+
|
|
1388
|
+
def extract_validation_score(value: Any) -> float:
|
|
1389
|
+
root = _require_mapping(value, "upstream ACE result")
|
|
1390
|
+
training = _require_mapping(root.get("training_results"), "upstream training_results")
|
|
1391
|
+
score = _require_non_negative_float(
|
|
1392
|
+
training.get("best_validation_accuracy"), "upstream validation accuracy"
|
|
1393
|
+
)
|
|
1394
|
+
if score > 1:
|
|
1395
|
+
raise WorkerFailure("UPSTREAM_RESULT_INVALID", "upstream validation accuracy exceeds one")
|
|
1396
|
+
return score
|
|
1397
|
+
|
|
1398
|
+
|
|
1399
|
+
def serve(runtime: ACEWorkerRuntime, input_stream: TextIO, output_stream: TextIO) -> None:
|
|
1400
|
+
for raw_line in input_stream:
|
|
1401
|
+
if not raw_line.strip():
|
|
1402
|
+
continue
|
|
1403
|
+
request_id = ""
|
|
1404
|
+
runtime.last_tracker = None
|
|
1405
|
+
try:
|
|
1406
|
+
if len(raw_line.encode("utf-8")) > _MAX_JSONL_BYTES:
|
|
1407
|
+
raise WorkerFailure("REQUEST_TOO_LARGE", "ACE JSONL request is too large")
|
|
1408
|
+
value = _loads_strict_json(raw_line)
|
|
1409
|
+
payload = _require_mapping(value, "ACE JSONL request")
|
|
1410
|
+
if isinstance(payload.get("request_id"), str):
|
|
1411
|
+
request_id = payload["request_id"][:128]
|
|
1412
|
+
response = runtime.run(payload)
|
|
1413
|
+
except WorkerFailure as exc:
|
|
1414
|
+
response = failure_response(request_id, exc.code, str(exc))
|
|
1415
|
+
except Exception as exc: # pragma: no cover - final process boundary
|
|
1416
|
+
response = failure_response(
|
|
1417
|
+
request_id,
|
|
1418
|
+
"INTERNAL_WORKER_ERROR",
|
|
1419
|
+
f"ACE worker failed with {type(exc).__name__}",
|
|
1420
|
+
)
|
|
1421
|
+
if response.get("status") == "error" and runtime.last_tracker is not None:
|
|
1422
|
+
response["usage"] = runtime.last_tracker.as_dict()
|
|
1423
|
+
response["consumed"] = runtime.last_tracker.model_calls > 0
|
|
1424
|
+
response["failed_attempt_tokens_unknown"] = (
|
|
1425
|
+
runtime.last_tracker.failed_provider_attempts > 0
|
|
1426
|
+
)
|
|
1427
|
+
output_stream.write(_canonical_json(response) + "\n")
|
|
1428
|
+
output_stream.flush()
|
|
1429
|
+
|
|
1430
|
+
|
|
1431
|
+
def failure_response(request_id: str, code: str, message: str) -> dict[str, Any]:
|
|
1432
|
+
return {
|
|
1433
|
+
"status": "error",
|
|
1434
|
+
"request_id": request_id,
|
|
1435
|
+
"protocol_version": PROTOCOL_VERSION,
|
|
1436
|
+
"upstream_commit": UPSTREAM_COMMIT,
|
|
1437
|
+
"failure_code": code,
|
|
1438
|
+
"message": message,
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
|
|
1442
|
+
def file_sha256(path: Path) -> str:
|
|
1443
|
+
digest = hashlib.sha256()
|
|
1444
|
+
with path.open("rb") as handle:
|
|
1445
|
+
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
|
1446
|
+
digest.update(chunk)
|
|
1447
|
+
return digest.hexdigest()
|
|
1448
|
+
|
|
1449
|
+
|
|
1450
|
+
def canonical_hash(value: Any) -> str:
|
|
1451
|
+
return hashlib.sha256(_canonical_json(value).encode("utf-8")).hexdigest()
|
|
1452
|
+
|
|
1453
|
+
|
|
1454
|
+
def _canonical_json(value: Any) -> str:
|
|
1455
|
+
try:
|
|
1456
|
+
return json.dumps(
|
|
1457
|
+
value,
|
|
1458
|
+
ensure_ascii=False,
|
|
1459
|
+
sort_keys=True,
|
|
1460
|
+
separators=(",", ":"),
|
|
1461
|
+
allow_nan=False,
|
|
1462
|
+
)
|
|
1463
|
+
except (TypeError, ValueError) as exc:
|
|
1464
|
+
raise WorkerFailure("NON_CANONICAL_JSON", "value is not canonical JSON") from exc
|
|
1465
|
+
|
|
1466
|
+
|
|
1467
|
+
def _loads_strict_json(raw: str) -> Any:
|
|
1468
|
+
def reject_constant(value: str) -> None:
|
|
1469
|
+
raise ValueError(f"non-finite JSON constant: {value}")
|
|
1470
|
+
|
|
1471
|
+
def reject_duplicates(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
|
|
1472
|
+
result: dict[str, Any] = {}
|
|
1473
|
+
for key, value in pairs:
|
|
1474
|
+
if key in result:
|
|
1475
|
+
raise ValueError(f"duplicate JSON key: {key}")
|
|
1476
|
+
result[key] = value
|
|
1477
|
+
return result
|
|
1478
|
+
|
|
1479
|
+
try:
|
|
1480
|
+
return json.loads(
|
|
1481
|
+
raw,
|
|
1482
|
+
parse_constant=reject_constant,
|
|
1483
|
+
object_pairs_hook=reject_duplicates,
|
|
1484
|
+
)
|
|
1485
|
+
except (TypeError, ValueError, json.JSONDecodeError) as exc:
|
|
1486
|
+
raise WorkerFailure("INVALID_JSON", "input is not strict JSON") from exc
|
|
1487
|
+
|
|
1488
|
+
|
|
1489
|
+
def _require_mapping(value: Any, label: str) -> Mapping[str, Any]:
|
|
1490
|
+
if not isinstance(value, Mapping):
|
|
1491
|
+
raise WorkerFailure("INVALID_SCHEMA", f"{label} must be a mapping")
|
|
1492
|
+
if any(not isinstance(key, str) for key in value):
|
|
1493
|
+
raise WorkerFailure("INVALID_SCHEMA", f"{label} keys must be strings")
|
|
1494
|
+
return value
|
|
1495
|
+
|
|
1496
|
+
|
|
1497
|
+
def _require_exact_keys(value: Mapping[str, Any], expected: set[str], label: str) -> None:
|
|
1498
|
+
if set(value) != expected:
|
|
1499
|
+
raise WorkerFailure("INVALID_SCHEMA", f"{label} fields do not match the protocol")
|
|
1500
|
+
|
|
1501
|
+
|
|
1502
|
+
def _require_text(value: Any, label: str) -> str:
|
|
1503
|
+
if not isinstance(value, str) or not value.strip():
|
|
1504
|
+
raise WorkerFailure("INVALID_SCHEMA", f"{label} must be non-empty text")
|
|
1505
|
+
return value
|
|
1506
|
+
|
|
1507
|
+
|
|
1508
|
+
def _require_env_name(value: Any, label: str) -> str:
|
|
1509
|
+
text = _require_text(value, label)
|
|
1510
|
+
if _ENV_RE.fullmatch(text) is None:
|
|
1511
|
+
raise WorkerFailure("INVALID_SCHEMA", f"{label} must be an environment identifier")
|
|
1512
|
+
return text
|
|
1513
|
+
|
|
1514
|
+
|
|
1515
|
+
def _require_http_url(value: Any, label: str) -> str:
|
|
1516
|
+
text = _require_text(value, label).rstrip("/")
|
|
1517
|
+
parsed = urllib.parse.urlsplit(text)
|
|
1518
|
+
if (
|
|
1519
|
+
parsed.scheme not in {"http", "https"}
|
|
1520
|
+
or not parsed.hostname
|
|
1521
|
+
or parsed.username is not None
|
|
1522
|
+
or parsed.password is not None
|
|
1523
|
+
or parsed.query
|
|
1524
|
+
or parsed.fragment
|
|
1525
|
+
):
|
|
1526
|
+
raise WorkerFailure(
|
|
1527
|
+
"INVALID_SCHEMA",
|
|
1528
|
+
f"{label} must be an HTTP(S) URL without credentials, query, or fragment",
|
|
1529
|
+
)
|
|
1530
|
+
return text
|
|
1531
|
+
|
|
1532
|
+
|
|
1533
|
+
def _require_hash(value: Any, label: str) -> str:
|
|
1534
|
+
if not isinstance(value, str) or not _HASH_RE.fullmatch(value):
|
|
1535
|
+
raise WorkerFailure("INVALID_SCHEMA", f"{label} must be a SHA-256 hex digest")
|
|
1536
|
+
return value
|
|
1537
|
+
|
|
1538
|
+
|
|
1539
|
+
def _require_positive_int(value: Any, label: str) -> int:
|
|
1540
|
+
result = _require_non_negative_int(value, label)
|
|
1541
|
+
if result == 0:
|
|
1542
|
+
raise WorkerFailure("INVALID_SCHEMA", f"{label} must be positive")
|
|
1543
|
+
return result
|
|
1544
|
+
|
|
1545
|
+
|
|
1546
|
+
def _require_non_negative_int(value: Any, label: str) -> int:
|
|
1547
|
+
if isinstance(value, bool) or not isinstance(value, int) or value < 0:
|
|
1548
|
+
raise WorkerFailure("INVALID_SCHEMA", f"{label} must be a non-negative integer")
|
|
1549
|
+
return value
|
|
1550
|
+
|
|
1551
|
+
|
|
1552
|
+
def _require_non_negative_float(value: Any, label: str) -> float:
|
|
1553
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
1554
|
+
raise WorkerFailure("INVALID_SCHEMA", f"{label} must be numeric")
|
|
1555
|
+
result = float(value)
|
|
1556
|
+
if not math.isfinite(result) or result < 0:
|
|
1557
|
+
raise WorkerFailure("INVALID_SCHEMA", f"{label} must be finite and non-negative")
|
|
1558
|
+
return result
|
|
1559
|
+
|
|
1560
|
+
|
|
1561
|
+
def _safe_relative_path(value: Any, label: str) -> str:
|
|
1562
|
+
text = _require_text(value, label)
|
|
1563
|
+
path = PurePosixPath(text)
|
|
1564
|
+
if path.is_absolute() or ".." in path.parts or "." in path.parts:
|
|
1565
|
+
raise WorkerFailure("INVALID_SCHEMA", f"{label} must be a safe relative path")
|
|
1566
|
+
return path.as_posix()
|
|
1567
|
+
|
|
1568
|
+
|
|
1569
|
+
def _is_appworld_module(name: str) -> bool:
|
|
1570
|
+
lowered = name.casefold().replace("-", "_")
|
|
1571
|
+
return lowered == "appworld" or lowered.startswith("appworld.") or "ace_appworld" in lowered
|
|
1572
|
+
|
|
1573
|
+
|
|
1574
|
+
def load_operator_data_processor(factory: str, raw_config: str = "{}") -> Any:
|
|
1575
|
+
"""Load a caller-selected feedback factory at process startup, never from JSONL."""
|
|
1576
|
+
if not isinstance(factory, str) or not re.fullmatch(
|
|
1577
|
+
r"[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*:[A-Za-z_]\w*", factory
|
|
1578
|
+
):
|
|
1579
|
+
raise WorkerFailure("INVALID_TASK_PROCESSOR", "factory must be module:function")
|
|
1580
|
+
config = _require_mapping(_loads_strict_json(raw_config), "task processor configuration")
|
|
1581
|
+
module_name, symbol = factory.split(":")
|
|
1582
|
+
diagnostics = BoundedHashSink()
|
|
1583
|
+
with contextlib.redirect_stdout(diagnostics):
|
|
1584
|
+
try:
|
|
1585
|
+
module = importlib.import_module(module_name)
|
|
1586
|
+
except ImportError as exc:
|
|
1587
|
+
raise WorkerFailure(
|
|
1588
|
+
"INVALID_TASK_PROCESSOR", "cannot import configured feedback factory"
|
|
1589
|
+
) from exc
|
|
1590
|
+
source = getattr(module, "__file__", None)
|
|
1591
|
+
if source is None or not Path(source).is_file():
|
|
1592
|
+
raise WorkerFailure("INVALID_TASK_PROCESSOR", "factory module requires a source identity")
|
|
1593
|
+
create = getattr(module, symbol, None)
|
|
1594
|
+
if not callable(create):
|
|
1595
|
+
raise WorkerFailure("INVALID_TASK_PROCESSOR", "factory symbol must be callable")
|
|
1596
|
+
with contextlib.redirect_stdout(diagnostics):
|
|
1597
|
+
delegate = create(**dict(config))
|
|
1598
|
+
for name in ("answer_is_correct", "evaluate_accuracy", "fingerprint"):
|
|
1599
|
+
if not callable(getattr(delegate, name, None)):
|
|
1600
|
+
raise WorkerFailure("INVALID_TASK_PROCESSOR", f"factory result lacks {name}()")
|
|
1601
|
+
binding = dict(
|
|
1602
|
+
factory=factory,
|
|
1603
|
+
factory_module_sha256=file_sha256(Path(source)),
|
|
1604
|
+
operator_configuration_sha256=canonical_hash(config),
|
|
1605
|
+
factory_stdout_sha256=diagnostics.sha256,
|
|
1606
|
+
factory_stdout_bytes=diagnostics.byte_count,
|
|
1607
|
+
)
|
|
1608
|
+
|
|
1609
|
+
class BoundProcessor:
|
|
1610
|
+
def fingerprint(self):
|
|
1611
|
+
return dict(operator_binding=binding, task_processor=delegate.fingerprint())
|
|
1612
|
+
|
|
1613
|
+
def __getattr__(self, name):
|
|
1614
|
+
return getattr(delegate, name)
|
|
1615
|
+
|
|
1616
|
+
return BoundProcessor()
|
|
1617
|
+
|
|
1618
|
+
|
|
1619
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
1620
|
+
parser = argparse.ArgumentParser(description="Pinned AgentInputLab ACE JSONL worker")
|
|
1621
|
+
parser.add_argument("--checkout", required=True, type=Path)
|
|
1622
|
+
parser.add_argument(
|
|
1623
|
+
"--llm-config-json",
|
|
1624
|
+
help="Explicit provider/model/fingerprint/pricing JSON; required unless --verify-only",
|
|
1625
|
+
)
|
|
1626
|
+
parser.add_argument("--verify-only", action="store_true")
|
|
1627
|
+
parser.add_argument(
|
|
1628
|
+
"--task-processor-factory",
|
|
1629
|
+
help="Operator-bound module:function feedback factory; never accepted in JSONL requests",
|
|
1630
|
+
)
|
|
1631
|
+
parser.add_argument(
|
|
1632
|
+
"--task-processor-config-json",
|
|
1633
|
+
help="Startup-only keyword JSON for the feedback factory (no credentials)",
|
|
1634
|
+
)
|
|
1635
|
+
return parser
|
|
1636
|
+
|
|
1637
|
+
|
|
1638
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
1639
|
+
args = build_parser().parse_args(argv)
|
|
1640
|
+
policy = load_compatibility_policy()
|
|
1641
|
+
checkout = verify_checkout(args.checkout, policy)
|
|
1642
|
+
if args.verify_only:
|
|
1643
|
+
load_upstream(checkout)
|
|
1644
|
+
sys.stdout.write(
|
|
1645
|
+
_canonical_json(
|
|
1646
|
+
{
|
|
1647
|
+
"status": "ok",
|
|
1648
|
+
"upstream_commit": UPSTREAM_COMMIT,
|
|
1649
|
+
"license_spdx": policy.license_spdx,
|
|
1650
|
+
"import": "ace.ace.ACE.run",
|
|
1651
|
+
}
|
|
1652
|
+
)
|
|
1653
|
+
+ "\n"
|
|
1654
|
+
)
|
|
1655
|
+
return 0
|
|
1656
|
+
if args.llm_config_json is None:
|
|
1657
|
+
raise WorkerFailure("INVALID_LLM_CONFIG", "--llm-config-json is required in serve mode")
|
|
1658
|
+
if args.task_processor_config_json is not None and args.task_processor_factory is None:
|
|
1659
|
+
raise WorkerFailure("INVALID_TASK_PROCESSOR", "processor configuration requires a factory")
|
|
1660
|
+
processor = (
|
|
1661
|
+
load_operator_data_processor(
|
|
1662
|
+
args.task_processor_factory, args.task_processor_config_json or "{}"
|
|
1663
|
+
)
|
|
1664
|
+
if args.task_processor_factory is not None
|
|
1665
|
+
else None
|
|
1666
|
+
)
|
|
1667
|
+
runtime = ACEWorkerRuntime(
|
|
1668
|
+
checkout,
|
|
1669
|
+
LLMConfig.from_json(args.llm_config_json),
|
|
1670
|
+
policy=policy,
|
|
1671
|
+
data_processor=processor,
|
|
1672
|
+
)
|
|
1673
|
+
serve(runtime, sys.stdin, sys.stdout)
|
|
1674
|
+
return 0
|
|
1675
|
+
|
|
1676
|
+
|
|
1677
|
+
if __name__ == "__main__": # pragma: no cover - exercised through JSONL smoke tests
|
|
1678
|
+
try:
|
|
1679
|
+
raise SystemExit(main())
|
|
1680
|
+
except WorkerFailure as error:
|
|
1681
|
+
sys.stderr.write(f"{error.code}: {error}\n")
|
|
1682
|
+
raise SystemExit(2) from error
|