agent-learning-kit 0.1.0__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.
- agent_learning_kit-0.1.0.dist-info/METADATA +381 -0
- agent_learning_kit-0.1.0.dist-info/RECORD +642 -0
- agent_learning_kit-0.1.0.dist-info/WHEEL +4 -0
- agent_learning_kit-0.1.0.dist-info/entry_points.txt +5 -0
- agent_learning_kit-0.1.0.dist-info/licenses/LICENSE +173 -0
- agent_learning_kit-0.1.0.dist-info/licenses/NOTICE +7 -0
- fi/__init__.py +5 -0
- fi/alk/__init__.py +57 -0
- fi/alk/_facade.py +31 -0
- fi/alk/_module_alias.py +68 -0
- fi/alk/_paths.py +14 -0
- fi/alk/_schema.py +522 -0
- fi/alk/actions.py +727 -0
- fi/alk/bench/__init__.py +517 -0
- fi/alk/bench/_codeexec.py +213 -0
- fi/alk/bench/_coding.py +215 -0
- fi/alk/bench/_docker.py +237 -0
- fi/alk/bench/_grader.py +286 -0
- fi/alk/bench/_pull.py +212 -0
- fi/alk/bench/_voice.py +147 -0
- fi/alk/capabilities.py +627 -0
- fi/alk/cli.py +6396 -0
- fi/alk/config.py +130 -0
- fi/alk/cua_loop.py +562 -0
- fi/alk/evals.py +2351 -0
- fi/alk/extensions.py +163 -0
- fi/alk/harness/ARCHITECTURE.md +231 -0
- fi/alk/harness/DESIGN.md +246 -0
- fi/alk/harness/ENVIRONMENT_CONFORMANCE.md +127 -0
- fi/alk/harness/HOW-IT-WORKS.md +297 -0
- fi/alk/harness/IMPLEMENTATION_AND_VALIDATION_STATUS.md +229 -0
- fi/alk/harness/README.md +417 -0
- fi/alk/harness/__init__.py +77 -0
- fi/alk/harness/__main__.py +3 -0
- fi/alk/harness/amend.py +312 -0
- fi/alk/harness/artifacts.py +319 -0
- fi/alk/harness/authoring_entrypoint.py +189 -0
- fi/alk/harness/authoring_runtime_validation.py +267 -0
- fi/alk/harness/backends/README.md +43 -0
- fi/alk/harness/backends/__init__.py +122 -0
- fi/alk/harness/backends/base.py +241 -0
- fi/alk/harness/backends/claude.py +211 -0
- fi/alk/harness/backends/files.py +182 -0
- fi/alk/harness/backends/vertex_gemini.py +457 -0
- fi/alk/harness/background_noise.py +95 -0
- fi/alk/harness/build.py +385 -0
- fi/alk/harness/bundle.py +593 -0
- fi/alk/harness/bundle_author_v2.py +1831 -0
- fi/alk/harness/bundle_v2.py +719 -0
- fi/alk/harness/call_runner.py +1440 -0
- fi/alk/harness/callback_http_adapter.py +111 -0
- fi/alk/harness/catalogue.py +287 -0
- fi/alk/harness/chat.py +428 -0
- fi/alk/harness/chat_call_runner.py +506 -0
- fi/alk/harness/checks.py +136 -0
- fi/alk/harness/cli.py +1354 -0
- fi/alk/harness/config.py +338 -0
- fi/alk/harness/contract.py +718 -0
- fi/alk/harness/credentials.py +674 -0
- fi/alk/harness/data/persona_vocabulary.json +111 -0
- fi/alk/harness/environment.py +99 -0
- fi/alk/harness/environment_plan.py +168 -0
- fi/alk/harness/events.py +125 -0
- fi/alk/harness/executor.py +304 -0
- fi/alk/harness/folder.py +234 -0
- fi/alk/harness/generated_runtime.py +815 -0
- fi/alk/harness/github.py +72 -0
- fi/alk/harness/hosted_authoring_entrypoint.py +183 -0
- fi/alk/harness/hosted_entrypoint.py +2402 -0
- fi/alk/harness/hosted_scheduler.py +2218 -0
- fi/alk/harness/job.py +426 -0
- fi/alk/harness/judge.py +184 -0
- fi/alk/harness/livekit_source.py +50 -0
- fi/alk/harness/livekit_tool_trace_bootstrap.py +71 -0
- fi/alk/harness/observability.py +208 -0
- fi/alk/harness/outbound.py +3252 -0
- fi/alk/harness/packaging.py +515 -0
- fi/alk/harness/persona_guides.py +157 -0
- fi/alk/harness/platform.py +692 -0
- fi/alk/harness/process_preflight.py +764 -0
- fi/alk/harness/process_runtime.py +5670 -0
- fi/alk/harness/prove.py +425 -0
- fi/alk/harness/provider_import.py +703 -0
- fi/alk/harness/provider_lifecycle.py +392 -0
- fi/alk/harness/provision.py +2896 -0
- fi/alk/harness/reception.py +147 -0
- fi/alk/harness/retell_chat_call_runner.py +373 -0
- fi/alk/harness/run/__init__.py +296 -0
- fi/alk/harness/run/alk.py +184 -0
- fi/alk/harness/run/call.py +162 -0
- fi/alk/harness/run/conversation.py +264 -0
- fi/alk/harness/run/data/voices_by_language_and_gender.json +693 -0
- fi/alk/harness/run/evidence.py +195 -0
- fi/alk/harness/run/grade.py +598 -0
- fi/alk/harness/run/live.py +297 -0
- fi/alk/harness/run/models.py +56 -0
- fi/alk/harness/run/platform_evals.py +227 -0
- fi/alk/harness/run/sdk_voice.py +130 -0
- fi/alk/harness/run/simulation.py +1209 -0
- fi/alk/harness/run/stage.py +91 -0
- fi/alk/harness/run/targets.py +508 -0
- fi/alk/harness/run/tools.py +601 -0
- fi/alk/harness/run/voice.py +340 -0
- fi/alk/harness/runtime.py +172 -0
- fi/alk/harness/sandbox_server.py +2011 -0
- fi/alk/harness/sandbox_worker.py +44 -0
- fi/alk/harness/scenario.py +1048 -0
- fi/alk/harness/scenario_source.py +879 -0
- fi/alk/harness/scenario_tools.py +1143 -0
- fi/alk/harness/scenarios.py +915 -0
- fi/alk/harness/secrets.py +168 -0
- fi/alk/harness/service_catalog.py +97 -0
- fi/alk/harness/session.py +391 -0
- fi/alk/harness/sessions.py +372 -0
- fi/alk/harness/simulator.py +76 -0
- fi/alk/harness/simulator_voice.py +928 -0
- fi/alk/harness/skills/build-environment/SKILL.md +538 -0
- fi/alk/harness/skills/harness.md +131 -0
- fi/alk/harness/skills/kinds/chat.md +48 -0
- fi/alk/harness/skills/kinds/voice-voicemail.md +63 -0
- fi/alk/harness/skills/kinds/voice.md +59 -0
- fi/alk/harness/skills/plan-suite/SKILL.md +103 -0
- fi/alk/harness/skills/provision-environment/SKILL.md +136 -0
- fi/alk/harness/skills/run-scenarios/SKILL.md +112 -0
- fi/alk/harness/skills/understand-agent/SKILL.md +251 -0
- fi/alk/harness/skills/write-scenarios/SKILL.md +606 -0
- fi/alk/harness/skills/write-scenarios/references/refusals.md +28 -0
- fi/alk/harness/skills/write-scenarios/references/world-api.md +92 -0
- fi/alk/harness/source_data_invariants.py +444 -0
- fi/alk/harness/source_tool_evidence.py +79 -0
- fi/alk/harness/sources.py +253 -0
- fi/alk/harness/spend.py +140 -0
- fi/alk/harness/tool_trace_proxy.py +104 -0
- fi/alk/harness/tools.py +1018 -0
- fi/alk/harness/understand.py +169 -0
- fi/alk/harness/voicemail_audio.py +74 -0
- fi/alk/harness/world/__init__.py +33 -0
- fi/alk/harness/world/errors.py +68 -0
- fi/alk/harness/world/expectations.py +91 -0
- fi/alk/harness/world/handle.py +538 -0
- fi/alk/harness/world/kinds.py +196 -0
- fi/alk/harness/world/mutate.py +186 -0
- fi/alk/harness/world/probe.py +413 -0
- fi/alk/harness/world/provision.py +511 -0
- fi/alk/harness/world/provisioned.py +191 -0
- fi/alk/harness/world/runtime.py +616 -0
- fi/alk/harness/world/snapshot.py +288 -0
- fi/alk/harness/world/stores/__init__.py +305 -0
- fi/alk/harness/world/stores/container.py +215 -0
- fi/alk/harness/world/stores/inprocess.py +346 -0
- fi/alk/harness/world/stores/postgres.py +481 -0
- fi/alk/harness/world/stores/prove.py +202 -0
- fi/alk/harness/world/stores/sqlite.py +245 -0
- fi/alk/harness/world/stores/written.py +182 -0
- fi/alk/harness/world/tools.py +1516 -0
- fi/alk/harness/world/workspace.py +144 -0
- fi/alk/image_loop.py +453 -0
- fi/alk/image_perturb.py +241 -0
- fi/alk/improve.py +274 -0
- fi/alk/live/__init__.py +154 -0
- fi/alk/live/_attribution.py +184 -0
- fi/alk/live/_capture.py +264 -0
- fi/alk/live/_codec.py +391 -0
- fi/alk/live/_contract.py +134 -0
- fi/alk/live/_loopback.py +316 -0
- fi/alk/live/_perturb.py +449 -0
- fi/alk/live/_runner.py +386 -0
- fi/alk/live/_stats.py +561 -0
- fi/alk/live/_transcript.py +240 -0
- fi/alk/live/_workers/__init__.py +9 -0
- fi/alk/live/_workers/a2a_worker.py +316 -0
- fi/alk/live/_workers/langgraph_worker.py +217 -0
- fi/alk/live/_workers/livekit_worker.py +207 -0
- fi/alk/live/_workers/mcp_loopback_server.py +46 -0
- fi/alk/live/_workers/mcp_worker.py +158 -0
- fi/alk/live/_workers/pipecat_worker.py +189 -0
- fi/alk/live/a2a_lane.py +138 -0
- fi/alk/live/langgraph_lane.py +339 -0
- fi/alk/live/livekit_lane.py +376 -0
- fi/alk/live/mcp_lane.py +172 -0
- fi/alk/live/pipecat_lane.py +341 -0
- fi/alk/live/voice_redteam.py +494 -0
- fi/alk/loss.py +306 -0
- fi/alk/optimize.py +36260 -0
- fi/alk/practice/__init__.py +51 -0
- fi/alk/practice/_assess.py +103 -0
- fi/alk/practice/_budget.py +81 -0
- fi/alk/practice/_calibrate.py +69 -0
- fi/alk/practice/_capstone.py +86 -0
- fi/alk/practice/_contract.py +91 -0
- fi/alk/practice/_diagnose.py +79 -0
- fi/alk/practice/_drill.py +196 -0
- fi/alk/practice/_experiment.py +720 -0
- fi/alk/practice/_schedule.py +102 -0
- fi/alk/practice/_store.py +194 -0
- fi/alk/practice/_trainer.py +245 -0
- fi/alk/practice/_update.py +125 -0
- fi/alk/redteam.py +2621 -0
- fi/alk/rewardhack.py +237 -0
- fi/alk/simulate.py +10351 -0
- fi/alk/studio/__init__.py +82 -0
- fi/alk/studio/_bias.py +314 -0
- fi/alk/studio/_calibration.py +522 -0
- fi/alk/studio/_coverage.py +262 -0
- fi/alk/studio/_download.py +665 -0
- fi/alk/studio/_fidelity_attack.py +114 -0
- fi/alk/studio/_generate.py +652 -0
- fi/alk/studio/_library.py +370 -0
- fi/alk/studio/_scan.py +134 -0
- fi/alk/studio/_upgrade.py +42 -0
- fi/alk/studio/_vendor.py +172 -0
- fi/alk/suite.py +4200 -0
- fi/alk/tasks.py +828 -0
- fi/alk/telemetry/__init__.py +149 -0
- fi/alk/telemetry/_contract.py +141 -0
- fi/alk/telemetry/_emit.py +182 -0
- fi/alk/telemetry/_ledger.py +296 -0
- fi/alk/telemetry/_queue.py +127 -0
- fi/alk/telemetry/_row.py +294 -0
- fi/alk/telemetry/_run.py +233 -0
- fi/alk/telemetry/_sync.py +193 -0
- fi/alk/telemetry/_url.py +119 -0
- fi/alk/trinity.py +49397 -0
- fi/alk/voice_loop.py +174 -0
- fi/api/__init__.py +1 -0
- fi/api/auth.py +137 -0
- fi/api/types.py +29 -0
- fi/cli/__init__.py +9 -0
- fi/cli/assertions/__init__.py +25 -0
- fi/cli/assertions/conditions.py +76 -0
- fi/cli/assertions/evaluator.py +286 -0
- fi/cli/assertions/exit_codes.py +20 -0
- fi/cli/assertions/parser.py +131 -0
- fi/cli/assertions/reporter.py +194 -0
- fi/cli/commands/__init__.py +9 -0
- fi/cli/commands/config.py +165 -0
- fi/cli/commands/export.py +208 -0
- fi/cli/commands/init.py +112 -0
- fi/cli/commands/list_cmd.py +213 -0
- fi/cli/commands/run.py +486 -0
- fi/cli/commands/validate.py +173 -0
- fi/cli/commands/view.py +424 -0
- fi/cli/config/__init__.py +6 -0
- fi/cli/config/defaults.py +206 -0
- fi/cli/config/loader.py +155 -0
- fi/cli/config/schema.py +174 -0
- fi/cli/main.py +78 -0
- fi/cli/output/__init__.py +6 -0
- fi/cli/output/formatters.py +106 -0
- fi/cli/output/reporters.py +46 -0
- fi/cli/storage/__init__.py +5 -0
- fi/cli/storage/run_history.py +249 -0
- fi/cli/utils/__init__.py +5 -0
- fi/cli/utils/console.py +44 -0
- fi/evals/__init__.py +131 -0
- fi/evals/autoeval/__init__.py +137 -0
- fi/evals/autoeval/analyzer.py +211 -0
- fi/evals/autoeval/config.py +244 -0
- fi/evals/autoeval/export.py +213 -0
- fi/evals/autoeval/interactive.py +283 -0
- fi/evals/autoeval/pipeline.py +625 -0
- fi/evals/autoeval/prompts.py +139 -0
- fi/evals/autoeval/recommender.py +242 -0
- fi/evals/autoeval/rules.py +589 -0
- fi/evals/autoeval/templates.py +299 -0
- fi/evals/autoeval/types.py +232 -0
- fi/evals/core/__init__.py +16 -0
- fi/evals/core/cloud_registry.py +184 -0
- fi/evals/core/engines.py +368 -0
- fi/evals/core/evaluate.py +319 -0
- fi/evals/core/judge_prompt.py +90 -0
- fi/evals/core/prompt_generator.py +83 -0
- fi/evals/core/registry.py +57 -0
- fi/evals/core/result.py +55 -0
- fi/evals/evaluator.py +721 -0
- fi/evals/execution.py +168 -0
- fi/evals/feedback/__init__.py +32 -0
- fi/evals/feedback/calibrator.py +160 -0
- fi/evals/feedback/collector.py +214 -0
- fi/evals/feedback/hooks.py +81 -0
- fi/evals/feedback/retriever.py +128 -0
- fi/evals/feedback/store.py +272 -0
- fi/evals/feedback/types.py +99 -0
- fi/evals/framework/README.md +79 -0
- fi/evals/framework/__init__.py +267 -0
- fi/evals/framework/backends/Dockerfile.eval-runner +33 -0
- fi/evals/framework/backends/__init__.py +99 -0
- fi/evals/framework/backends/_container.py +141 -0
- fi/evals/framework/backends/_utils.py +145 -0
- fi/evals/framework/backends/base.py +223 -0
- fi/evals/framework/backends/celery_backend.py +417 -0
- fi/evals/framework/backends/celery_worker.py +78 -0
- fi/evals/framework/backends/kubernetes_backend.py +665 -0
- fi/evals/framework/backends/ray_backend.py +521 -0
- fi/evals/framework/backends/temporal.py +350 -0
- fi/evals/framework/backends/temporal_worker.py +126 -0
- fi/evals/framework/backends/thread_pool.py +286 -0
- fi/evals/framework/context.py +258 -0
- fi/evals/framework/enrichment.py +306 -0
- fi/evals/framework/evals/__init__.py +68 -0
- fi/evals/framework/evals/agentic.py +399 -0
- fi/evals/framework/evals/builder.py +609 -0
- fi/evals/framework/evals/semantic.py +142 -0
- fi/evals/framework/evaluator.py +647 -0
- fi/evals/framework/evaluators/__init__.py +22 -0
- fi/evals/framework/evaluators/blocking.py +347 -0
- fi/evals/framework/evaluators/non_blocking.py +577 -0
- fi/evals/framework/propagation.py +421 -0
- fi/evals/framework/protocols.py +385 -0
- fi/evals/framework/registry.py +370 -0
- fi/evals/framework/resilience/__init__.py +150 -0
- fi/evals/framework/resilience/circuit_breaker.py +309 -0
- fi/evals/framework/resilience/degradation.py +355 -0
- fi/evals/framework/resilience/health.py +505 -0
- fi/evals/framework/resilience/rate_limiter.py +228 -0
- fi/evals/framework/resilience/retry.py +274 -0
- fi/evals/framework/resilience/types.py +288 -0
- fi/evals/framework/resilience/wrapper.py +433 -0
- fi/evals/framework/types.py +218 -0
- fi/evals/guardrails/README.md +915 -0
- fi/evals/guardrails/__init__.py +96 -0
- fi/evals/guardrails/backends/__init__.py +43 -0
- fi/evals/guardrails/backends/azure.py +361 -0
- fi/evals/guardrails/backends/base.py +88 -0
- fi/evals/guardrails/backends/generic_llm.py +163 -0
- fi/evals/guardrails/backends/granite.py +216 -0
- fi/evals/guardrails/backends/llamaguard.py +221 -0
- fi/evals/guardrails/backends/local_base.py +479 -0
- fi/evals/guardrails/backends/openai.py +365 -0
- fi/evals/guardrails/backends/qwen.py +170 -0
- fi/evals/guardrails/backends/shieldgemma.py +154 -0
- fi/evals/guardrails/backends/turing.py +235 -0
- fi/evals/guardrails/backends/vllm_client.py +321 -0
- fi/evals/guardrails/backends/wildguard.py +188 -0
- fi/evals/guardrails/base.py +888 -0
- fi/evals/guardrails/config.py +221 -0
- fi/evals/guardrails/discovery.py +243 -0
- fi/evals/guardrails/gateway.py +437 -0
- fi/evals/guardrails/registry.py +231 -0
- fi/evals/guardrails/scanners/__init__.py +127 -0
- fi/evals/guardrails/scanners/base.py +191 -0
- fi/evals/guardrails/scanners/code_injection.py +243 -0
- fi/evals/guardrails/scanners/eval_delegate.py +574 -0
- fi/evals/guardrails/scanners/invisible_chars.py +351 -0
- fi/evals/guardrails/scanners/jailbreak.py +412 -0
- fi/evals/guardrails/scanners/language.py +288 -0
- fi/evals/guardrails/scanners/pipeline.py +260 -0
- fi/evals/guardrails/scanners/regex.py +311 -0
- fi/evals/guardrails/scanners/secrets.py +274 -0
- fi/evals/guardrails/scanners/topics.py +649 -0
- fi/evals/guardrails/scanners/urls.py +341 -0
- fi/evals/guardrails/types.py +96 -0
- fi/evals/llm/__init__.py +3 -0
- fi/evals/llm/base_llm_provider.py +35 -0
- fi/evals/llm/providers/litellm.py +70 -0
- fi/evals/local/__init__.py +90 -0
- fi/evals/local/evaluator.py +690 -0
- fi/evals/local/execution_mode.py +121 -0
- fi/evals/local/llm.py +489 -0
- fi/evals/local/metrics/__init__.py +19 -0
- fi/evals/local/registry.py +360 -0
- fi/evals/manager.py +1018 -0
- fi/evals/manager_types.py +362 -0
- fi/evals/metrics/__init__.py +185 -0
- fi/evals/metrics/agents/__init__.py +74 -0
- fi/evals/metrics/agents/metrics.py +693 -0
- fi/evals/metrics/agents/report.py +36463 -0
- fi/evals/metrics/agents/types.py +160 -0
- fi/evals/metrics/base_llm_metric.py +111 -0
- fi/evals/metrics/base_metric.py +138 -0
- fi/evals/metrics/code_security/__init__.py +305 -0
- fi/evals/metrics/code_security/analyzer.py +985 -0
- fi/evals/metrics/code_security/benchmarks/__init__.py +73 -0
- fi/evals/metrics/code_security/benchmarks/builtin.py +750 -0
- fi/evals/metrics/code_security/benchmarks/loader.py +580 -0
- fi/evals/metrics/code_security/benchmarks/types.py +308 -0
- fi/evals/metrics/code_security/detectors/__init__.py +186 -0
- fi/evals/metrics/code_security/detectors/base.py +394 -0
- fi/evals/metrics/code_security/detectors/cryptography.py +345 -0
- fi/evals/metrics/code_security/detectors/injection.py +744 -0
- fi/evals/metrics/code_security/detectors/secrets.py +287 -0
- fi/evals/metrics/code_security/detectors/serialization.py +192 -0
- fi/evals/metrics/code_security/joint_metrics.py +588 -0
- fi/evals/metrics/code_security/judges/__init__.py +83 -0
- fi/evals/metrics/code_security/judges/base.py +238 -0
- fi/evals/metrics/code_security/judges/dual_judge.py +534 -0
- fi/evals/metrics/code_security/judges/llm_judge.py +301 -0
- fi/evals/metrics/code_security/judges/pattern_judge.py +515 -0
- fi/evals/metrics/code_security/metrics.py +388 -0
- fi/evals/metrics/code_security/modes/__init__.py +63 -0
- fi/evals/metrics/code_security/modes/adversarial.py +284 -0
- fi/evals/metrics/code_security/modes/autocomplete.py +198 -0
- fi/evals/metrics/code_security/modes/base.py +283 -0
- fi/evals/metrics/code_security/modes/instruct.py +253 -0
- fi/evals/metrics/code_security/modes/repair.py +230 -0
- fi/evals/metrics/code_security/reports/__init__.py +57 -0
- fi/evals/metrics/code_security/reports/generator.py +404 -0
- fi/evals/metrics/code_security/reports/leaderboard.py +509 -0
- fi/evals/metrics/code_security/types.py +534 -0
- fi/evals/metrics/function_calling/__init__.py +34 -0
- fi/evals/metrics/function_calling/metrics.py +573 -0
- fi/evals/metrics/function_calling/types.py +87 -0
- fi/evals/metrics/hallucination/__init__.py +54 -0
- fi/evals/metrics/hallucination/detector.py +149 -0
- fi/evals/metrics/hallucination/metrics.py +390 -0
- fi/evals/metrics/hallucination/nli.py +253 -0
- fi/evals/metrics/hallucination/sentinel.py +106 -0
- fi/evals/metrics/hallucination/types.py +132 -0
- fi/evals/metrics/heuristics/aggregation_metrics.py +85 -0
- fi/evals/metrics/heuristics/json_metrics.py +87 -0
- fi/evals/metrics/heuristics/similarity_metrics.py +375 -0
- fi/evals/metrics/heuristics/string_metrics.py +391 -0
- fi/evals/metrics/llm_as_judges/__init__.py +17 -0
- fi/evals/metrics/llm_as_judges/custom_judge/metric.py +112 -0
- fi/evals/metrics/llm_as_judges/custom_judge/prompts.py +26 -0
- fi/evals/metrics/llm_as_judges/types.py +48 -0
- fi/evals/metrics/rag/__init__.py +111 -0
- fi/evals/metrics/rag/advanced/__init__.py +14 -0
- fi/evals/metrics/rag/advanced/multi_hop.py +283 -0
- fi/evals/metrics/rag/advanced/source_attribution.py +344 -0
- fi/evals/metrics/rag/generation/__init__.py +17 -0
- fi/evals/metrics/rag/generation/answer_relevancy.py +176 -0
- fi/evals/metrics/rag/generation/context_utilization.py +245 -0
- fi/evals/metrics/rag/generation/faithfulness.py +241 -0
- fi/evals/metrics/rag/generation/groundedness.py +131 -0
- fi/evals/metrics/rag/rag_score.py +277 -0
- fi/evals/metrics/rag/retrieval/__init__.py +20 -0
- fi/evals/metrics/rag/retrieval/context_entity_recall.py +124 -0
- fi/evals/metrics/rag/retrieval/context_precision.py +158 -0
- fi/evals/metrics/rag/retrieval/context_recall.py +106 -0
- fi/evals/metrics/rag/retrieval/noise_sensitivity.py +163 -0
- fi/evals/metrics/rag/retrieval/ranking.py +261 -0
- fi/evals/metrics/rag/types.py +100 -0
- fi/evals/metrics/rag/utils/__init__.py +62 -0
- fi/evals/metrics/rag/utils/claims.py +189 -0
- fi/evals/metrics/rag/utils/entities.py +244 -0
- fi/evals/metrics/rag/utils/nli.py +92 -0
- fi/evals/metrics/rag/utils/similarity.py +345 -0
- fi/evals/metrics/structured/__init__.py +114 -0
- fi/evals/metrics/structured/field_completeness.py +313 -0
- fi/evals/metrics/structured/hierarchy_score.py +366 -0
- fi/evals/metrics/structured/json_validation.py +190 -0
- fi/evals/metrics/structured/schema_compliance.py +280 -0
- fi/evals/metrics/structured/structured_output_score.py +298 -0
- fi/evals/metrics/structured/types.py +108 -0
- fi/evals/metrics/structured/validators/__init__.py +30 -0
- fi/evals/metrics/structured/validators/base.py +189 -0
- fi/evals/metrics/structured/validators/json_validator.py +196 -0
- fi/evals/metrics/structured/validators/pydantic_validator.py +178 -0
- fi/evals/metrics/structured/validators/yaml_validator.py +248 -0
- fi/evals/otel/__init__.py +266 -0
- fi/evals/otel/config.py +400 -0
- fi/evals/otel/conventions.py +463 -0
- fi/evals/otel/enrichment.py +371 -0
- fi/evals/otel/instrumentors/__init__.py +140 -0
- fi/evals/otel/instrumentors/anthropic.py +517 -0
- fi/evals/otel/instrumentors/base.py +382 -0
- fi/evals/otel/instrumentors/openai.py +673 -0
- fi/evals/otel/processors/__init__.py +36 -0
- fi/evals/otel/processors/base.py +473 -0
- fi/evals/otel/processors/cost.py +445 -0
- fi/evals/otel/processors/evaluation.py +559 -0
- fi/evals/otel/processors/llm.py +462 -0
- fi/evals/otel/tracer.py +506 -0
- fi/evals/otel/types.py +232 -0
- fi/evals/otel_utils.py +23 -0
- fi/evals/protect.py +671 -0
- fi/evals/protect_input_adapter.py +154 -0
- fi/evals/streaming/__init__.py +88 -0
- fi/evals/streaming/buffer.py +213 -0
- fi/evals/streaming/evaluator.py +551 -0
- fi/evals/streaming/policy.py +307 -0
- fi/evals/streaming/scorers.py +368 -0
- fi/evals/streaming/types.py +238 -0
- fi/evals/templates.py +472 -0
- fi/evals/types.py +156 -0
- fi/opt/__init__.py +221 -0
- fi/opt/_objective_scoring.py +85 -0
- fi/opt/base/__init__.py +11 -0
- fi/opt/base/base_generator.py +33 -0
- fi/opt/base/base_mapper.py +26 -0
- fi/opt/base/base_optimizer.py +45 -0
- fi/opt/base/evaluator.py +211 -0
- fi/opt/components.py +3095 -0
- fi/opt/datamappers/__init__.py +3 -0
- fi/opt/datamappers/basic_mapper.py +40 -0
- fi/opt/deployment.py +1021 -0
- fi/opt/evidence.py +4332 -0
- fi/opt/generators/__init__.py +3 -0
- fi/opt/generators/litellm.py +66 -0
- fi/opt/integrations/__init__.py +23 -0
- fi/opt/integrations/generative_suite.py +410 -0
- fi/opt/integrations/simulate.py +1313 -0
- fi/opt/mutations.py +771 -0
- fi/opt/observability.py +4639 -0
- fi/opt/optimizer_trace.py +889 -0
- fi/opt/optimizers/__init__.py +80 -0
- fi/opt/optimizers/agent.py +331 -0
- fi/opt/optimizers/agent_bandit.py +392 -0
- fi/opt/optimizers/agent_curriculum.py +635 -0
- fi/opt/optimizers/agent_evolution.py +894 -0
- fi/opt/optimizers/agent_feedback.py +1863 -0
- fi/opt/optimizers/agent_pareto.py +547 -0
- fi/opt/optimizers/agent_social_memory.py +1113 -0
- fi/opt/optimizers/agent_tpe.py +321 -0
- fi/opt/optimizers/bayesian_search.py +449 -0
- fi/opt/optimizers/council.py +2075 -0
- fi/opt/optimizers/futureagi_replay.py +799 -0
- fi/opt/optimizers/gepa.py +322 -0
- fi/opt/optimizers/metaprompt.py +243 -0
- fi/opt/optimizers/promptwizard.py +417 -0
- fi/opt/optimizers/protegi.py +329 -0
- fi/opt/optimizers/random_search.py +224 -0
- fi/opt/research.py +518 -0
- fi/opt/simulation.py +260 -0
- fi/opt/targets.py +232 -0
- fi/opt/types.py +66 -0
- fi/opt/utils/__init__.py +4 -0
- fi/opt/utils/early_stopping.py +266 -0
- fi/opt/utils/setup_logging.py +82 -0
- fi/simulate/__init__.py +540 -0
- fi/simulate/_hashing.py +35 -0
- fi/simulate/_logging.py +10 -0
- fi/simulate/adapters.py +87 -0
- fi/simulate/agent/__init__.py +120 -0
- fi/simulate/agent/browser.py +658 -0
- fi/simulate/agent/definition.py +587 -0
- fi/simulate/agent/frameworks.py +3528 -0
- fi/simulate/agent/generic.py +8286 -0
- fi/simulate/agent/import_probe.py +227 -0
- fi/simulate/agent/memory.py +905 -0
- fi/simulate/agent/mocks.py +101 -0
- fi/simulate/agent/multi_agent.py +361 -0
- fi/simulate/agent/orchestration.py +903 -0
- fi/simulate/agent/realtime.py +665 -0
- fi/simulate/agent/wrapper.py +99 -0
- fi/simulate/agent/wrappers/__init__.py +18 -0
- fi/simulate/agent/wrappers/anthropic.py +62 -0
- fi/simulate/agent/wrappers/gemini.py +65 -0
- fi/simulate/agent/wrappers/http.py +404 -0
- fi/simulate/agent/wrappers/langchain.py +80 -0
- fi/simulate/agent/wrappers/openai.py +75 -0
- fi/simulate/agent/wrappers/websocket.py +326 -0
- fi/simulate/artifacts/__init__.py +11 -0
- fi/simulate/artifacts/manifest.py +62 -0
- fi/simulate/cli.py +20560 -0
- fi/simulate/endpoints/__init__.py +45 -0
- fi/simulate/endpoints/_http_actor.py +73 -0
- fi/simulate/endpoints/actor_sources.py +243 -0
- fi/simulate/endpoints/base.py +107 -0
- fi/simulate/endpoints/builtins.py +10 -0
- fi/simulate/endpoints/callable.py +95 -0
- fi/simulate/endpoints/http.py +76 -0
- fi/simulate/endpoints/livekit.py +138 -0
- fi/simulate/endpoints/originators.py +132 -0
- fi/simulate/endpoints/profiles.py +348 -0
- fi/simulate/endpoints/retell.py +633 -0
- fi/simulate/endpoints/vapi.py +205 -0
- fi/simulate/endpoints/websocket.py +76 -0
- fi/simulate/environment.py +33026 -0
- fi/simulate/environments/__init__.py +11 -0
- fi/simulate/environments/base.py +73 -0
- fi/simulate/environments/chat.py +697 -0
- fi/simulate/environments/voice.py +212 -0
- fi/simulate/evaluation/__init__.py +4 -0
- fi/simulate/evaluation/ai_eval.py +227 -0
- fi/simulate/evidence/__init__.py +35 -0
- fi/simulate/evidence/base.py +59 -0
- fi/simulate/evidence/caller_observed.py +50 -0
- fi/simulate/evidence/livekit_instrumentation.py +51 -0
- fi/simulate/evidence/livekit_room.py +50 -0
- fi/simulate/evidence/otel.py +49 -0
- fi/simulate/evidence/providers/__init__.py +24 -0
- fi/simulate/evidence/providers/base.py +61 -0
- fi/simulate/evidence/providers/retell.py +376 -0
- fi/simulate/evidence/providers/vapi.py +426 -0
- fi/simulate/hosted/__init__.py +32 -0
- fi/simulate/hosted/child_entrypoint.py +306 -0
- fi/simulate/hosted/job.py +150 -0
- fi/simulate/hosted/targets.py +53 -0
- fi/simulate/instrumentation/__init__.py +5 -0
- fi/simulate/instrumentation/livekit/__init__.py +122 -0
- fi/simulate/manifest.py +1033 -0
- fi/simulate/matrix_cli.py +165 -0
- fi/simulate/realtime/__init__.py +40 -0
- fi/simulate/realtime/events.py +107 -0
- fi/simulate/realtime/media.py +61 -0
- fi/simulate/realtime/session.py +91 -0
- fi/simulate/recording/__init__.py +5 -0
- fi/simulate/recording/room_recorder.py +326 -0
- fi/simulate/registry.py +185 -0
- fi/simulate/results/__init__.py +9 -0
- fi/simulate/results/base.py +18 -0
- fi/simulate/results/filesystem.py +71 -0
- fi/simulate/results/futureagi.py +1340 -0
- fi/simulate/runtime/__init__.py +85 -0
- fi/simulate/runtime/capabilities.py +40 -0
- fi/simulate/runtime/events.py +63 -0
- fi/simulate/runtime/failures.py +25 -0
- fi/simulate/runtime/ids.py +34 -0
- fi/simulate/runtime/plan.py +70 -0
- fi/simulate/runtime/planner.py +102 -0
- fi/simulate/runtime/report.py +174 -0
- fi/simulate/runtime/run.py +75 -0
- fi/simulate/runtime/runner.py +333 -0
- fi/simulate/runtime/spec.py +186 -0
- fi/simulate/simulation/__init__.py +30 -0
- fi/simulate/simulation/behavior_policy.py +425 -0
- fi/simulate/simulation/bridge/__init__.py +9 -0
- fi/simulate/simulation/bridge/audio.py +29 -0
- fi/simulate/simulation/bridge/connector.py +46 -0
- fi/simulate/simulation/bridge/livekit.py +252 -0
- fi/simulate/simulation/bridge/retell.py +188 -0
- fi/simulate/simulation/bridge/vapi.py +177 -0
- fi/simulate/simulation/contract.py +419 -0
- fi/simulate/simulation/engines/__init__.py +12 -0
- fi/simulate/simulation/engines/base.py +21 -0
- fi/simulate/simulation/engines/cloud.py +517 -0
- fi/simulate/simulation/engines/livekit.py +4167 -0
- fi/simulate/simulation/engines/local_text.py +89 -0
- fi/simulate/simulation/fidelity.py +374 -0
- fi/simulate/simulation/gemini_tts_stream.py +110 -0
- fi/simulate/simulation/generator.py +91 -0
- fi/simulate/simulation/goal_machine.py +185 -0
- fi/simulate/simulation/livekit_models.py +467 -0
- fi/simulate/simulation/matrix.py +170 -0
- fi/simulate/simulation/models.py +279 -0
- fi/simulate/simulation/runner.py +153 -0
- fi/simulate/simulation/synthetic.py +880 -0
- fi/simulate/simulation/voice_prompt.py +502 -0
- fi/simulate/simulator/__init__.py +55 -0
- fi/simulate/simulator/builtins.py +53 -0
- fi/simulate/suite.py +1288 -0
- fi/simulate/utils/routes.py +164 -0
- fi/simulate/voice.py +225 -0
- fi/simulate/voice_cli.py +182 -0
- fi/utils/__init__.py +1 -0
- fi/utils/constants.py +14 -0
- fi/utils/errors.py +200 -0
- fi/utils/executor.py +26 -0
- fi/utils/routes.py +119 -0
- fi/utils/utils.py +17 -0
|
@@ -0,0 +1,1143 @@
|
|
|
1
|
+
"""The tools that write scenarios, and the gates that decide one may be kept.
|
|
2
|
+
|
|
3
|
+
A scenario is accepted by being *proved*, not by looking right. ``submit_scenario`` puts it
|
|
4
|
+
through three gates, in order: the world must end up holding what the scenario presumes, the
|
|
5
|
+
reference solution must pass the scenario's own checks, and those same checks must fail when
|
|
6
|
+
nothing is done at all.
|
|
7
|
+
|
|
8
|
+
Every gate is code. No model is asked whether a scenario is good; the environment decides. A
|
|
9
|
+
scenario that clears all three is written out as its own folder of runnable files.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import logging
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from .backends import tool, tool_server
|
|
20
|
+
|
|
21
|
+
from .amend import add_rule, drop_rule, fix_tool, widen
|
|
22
|
+
from .catalogue import (
|
|
23
|
+
Catalogue,
|
|
24
|
+
SubGoal,
|
|
25
|
+
load_catalogue,
|
|
26
|
+
save_catalogue,
|
|
27
|
+
validate_sub_goal,
|
|
28
|
+
)
|
|
29
|
+
from .contract import CALL_DIRECTIONS, AgentContract
|
|
30
|
+
from .folder import INDEX, SCENARIOS, apply_setup, read_all, write_folder, write_index
|
|
31
|
+
from .prove import play_reference_step, prepared, prove
|
|
32
|
+
from .scenario import (
|
|
33
|
+
ANSWERED_BY,
|
|
34
|
+
CALLER_AWARENESS,
|
|
35
|
+
VOICEMAIL_STYLES,
|
|
36
|
+
Scenario,
|
|
37
|
+
Step,
|
|
38
|
+
contract_sequence_problems,
|
|
39
|
+
suite_diversity_problems,
|
|
40
|
+
validate_scenario,
|
|
41
|
+
voicemail_enabled,
|
|
42
|
+
)
|
|
43
|
+
from .simulator import load_simulator_prompt
|
|
44
|
+
from .tools import brief, schema
|
|
45
|
+
from .world.snapshot import read_manifest, restore
|
|
46
|
+
|
|
47
|
+
logger = logging.getLogger(__name__)
|
|
48
|
+
|
|
49
|
+
SCENARIO_SERVER = "scenarios"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _mailbox_fields() -> dict[str, Any]:
|
|
53
|
+
"""The mailbox-only fields, withheld entirely when the switch is off: a field is an invitation."""
|
|
54
|
+
if not voicemail_enabled():
|
|
55
|
+
return {}
|
|
56
|
+
return {
|
|
57
|
+
"answered_by": {
|
|
58
|
+
"type": "string",
|
|
59
|
+
"enum": list(ANSWERED_BY),
|
|
60
|
+
"description": "Who picked up, and only for a scenario that states call_direction "
|
|
61
|
+
"outbound. Leave it out for the ordinary case where a person answers. 'voicemail' "
|
|
62
|
+
"replaces the person with a mailbox that plays its greeting once and then says nothing "
|
|
63
|
+
"whatever the agent asks, which tests whether the agent notices it is talking to a "
|
|
64
|
+
"machine, leaves a message that stands on its own, and stops. A mailbox can supply "
|
|
65
|
+
"nothing, so such a scenario never asks the agent to collect a value or reach agreement.",
|
|
66
|
+
},
|
|
67
|
+
"voicemail_style": {
|
|
68
|
+
"type": "string",
|
|
69
|
+
"enum": list(VOICEMAIL_STYLES),
|
|
70
|
+
"description": "Which kind of mailbox answered. 'personal' carries the person's name, "
|
|
71
|
+
"'carrier' names nobody, 'operator' is a long announcement a careless agent talks over, "
|
|
72
|
+
"'full' cannot record at all and is the only style with no tone. State one rather than "
|
|
73
|
+
"leaving it out: left out it is personal, the easiest of the four, and most suites have "
|
|
74
|
+
"room for only one mailbox.",
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _ok(text: str) -> dict[str, Any]:
|
|
80
|
+
return {"content": [{"type": "text", "text": text}]}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _err(text: str) -> dict[str, Any]:
|
|
84
|
+
return {"content": [{"type": "text", "text": text}], "is_error": True}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# Below this, fanning out costs more than it saves: measured at ten scenarios, fifty four turns
|
|
88
|
+
# without writers against a hundred and nineteen with, for output that was identical scenario by
|
|
89
|
+
# scenario. Above it, one at a time runs out of turns long before the number is reached.
|
|
90
|
+
FEWEST_WORTH_DELEGATING = 20
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def worth_delegating(wanted: int) -> bool:
|
|
94
|
+
"""Whether a request of this size should be written by several writers at once.
|
|
95
|
+
|
|
96
|
+
Decided from the number asked for, which is the one fact that settles it, rather than from a
|
|
97
|
+
setting. An environment variable had to survive four separate allowlists between the platform
|
|
98
|
+
and the process that reads it, three of which silently dropped it, and it exposed as an
|
|
99
|
+
operator choice something no operator should have to make.
|
|
100
|
+
"""
|
|
101
|
+
return int(wanted or 0) >= FEWEST_WORTH_DELEGATING
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def persona_field(name: str) -> dict[str, Any]:
|
|
105
|
+
"""The schema for one persona field, carrying the platform's own values where it has them.
|
|
106
|
+
|
|
107
|
+
Offered as an enum so the values arrive right the first time. Without the platform's model
|
|
108
|
+
to read, it stays a plain string rather than an enum of nothing.
|
|
109
|
+
"""
|
|
110
|
+
from .persona_guides import offered
|
|
111
|
+
|
|
112
|
+
allowed = offered(name)
|
|
113
|
+
return {"type": "string", "enum": allowed} if allowed else {"type": "string"}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def persona_vocabulary_note() -> str:
|
|
117
|
+
"""A sentence about why the persona fields are constrained, when they are."""
|
|
118
|
+
from .persona_guides import vocabulary
|
|
119
|
+
|
|
120
|
+
if not vocabulary():
|
|
121
|
+
return ""
|
|
122
|
+
return (
|
|
123
|
+
" The listed values are the ones the platform understands: they carry behaviour "
|
|
124
|
+
"guidance into the call and select the caller's voice. Anything else about this person "
|
|
125
|
+
"goes in metadata, where it is free text."
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def write_scenarios(
|
|
130
|
+
scenarios: list[Scenario], destination: Path, catalogue: Catalogue | None = None
|
|
131
|
+
) -> Path:
|
|
132
|
+
"""Write every scenario out as its own folder, and regenerate the index over them."""
|
|
133
|
+
catalogue = catalogue if catalogue is not None else load_catalogue(destination)
|
|
134
|
+
if not scenarios and (Path(destination) / SCENARIOS).is_dir():
|
|
135
|
+
# An empty save would take every folder with it, because dropping a scenario is expressed by
|
|
136
|
+
# saving the suite without it. Nothing legitimately saves an empty suite over a full one: a
|
|
137
|
+
# session whose own list is empty is a session that has not written anything yet, and on a run
|
|
138
|
+
# this emptied 30 folders and then let a second fan-out pass write the suite again from zero.
|
|
139
|
+
logger.warning(
|
|
140
|
+
"refusing to save an empty suite over %s existing scenarios",
|
|
141
|
+
len(load_scenarios(destination)),
|
|
142
|
+
)
|
|
143
|
+
return Path(destination) / INDEX
|
|
144
|
+
for one in scenarios:
|
|
145
|
+
write_folder(one, catalogue, destination)
|
|
146
|
+
_forget_dropped(scenarios, destination)
|
|
147
|
+
return write_index(scenarios, destination)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _forget_dropped(scenarios: list[Scenario], destination: Path) -> None:
|
|
151
|
+
"""Remove the folders of scenarios that are no longer in the suite.
|
|
152
|
+
|
|
153
|
+
The folders are the truth, and they are what gets read back. Writing the survivors without
|
|
154
|
+
taking the others away means a dropped scenario returns on the next load, still failing, and
|
|
155
|
+
dropping it appears to do nothing at all.
|
|
156
|
+
"""
|
|
157
|
+
import shutil
|
|
158
|
+
|
|
159
|
+
root = Path(destination) / SCENARIOS
|
|
160
|
+
if not root.exists():
|
|
161
|
+
return
|
|
162
|
+
keeping = {one.name for one in scenarios}
|
|
163
|
+
for folder in root.iterdir():
|
|
164
|
+
if folder.is_dir() and folder.name not in keeping:
|
|
165
|
+
shutil.rmtree(folder)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def load_scenarios(destination: Path) -> list[Scenario]:
|
|
169
|
+
"""Every scenario on disk, read from its folder.
|
|
170
|
+
|
|
171
|
+
The folders are the truth. The index beside them is regenerated from these, so it can
|
|
172
|
+
describe them but never contradict them.
|
|
173
|
+
"""
|
|
174
|
+
return read_all(destination)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
JOURNAL = "written.jsonl"
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def journal_scenario(scenario: Scenario, destination: Path) -> None:
|
|
181
|
+
"""Append one proved scenario to the journal, which is the only record a dead writer leaves.
|
|
182
|
+
|
|
183
|
+
A delegated writer cannot write folders: saving the suite deletes every folder it does not know
|
|
184
|
+
about, so a writer persisting its own would delete its siblings' work. It therefore keeps what it
|
|
185
|
+
proved in memory, and until now a writer whose session died took its scenarios with it. A whole
|
|
186
|
+
hundred-scenario run was lost that way. This file is append-only and nothing prunes it, so what
|
|
187
|
+
was proved survives the session that proved it.
|
|
188
|
+
"""
|
|
189
|
+
try:
|
|
190
|
+
path = Path(destination) / JOURNAL
|
|
191
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
192
|
+
with path.open("a", encoding="utf-8") as journal:
|
|
193
|
+
journal.write(json.dumps(scenario.model_dump(), ensure_ascii=False) + "\n")
|
|
194
|
+
except Exception as broke: # noqa: BLE001 - a scenario is never lost over bookkeeping
|
|
195
|
+
logger.warning("could not journal %s: %s", scenario.name, broke)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def journalled(destination: Path) -> list[Scenario]:
|
|
199
|
+
"""Every scenario the journal holds, newest wins, skipping anything unreadable.
|
|
200
|
+
|
|
201
|
+
Appended by writers as they prove, so a retried slice re-journals and the same name appears more
|
|
202
|
+
than once. Read by the caller that saves, to recover what a writer proved and never returned.
|
|
203
|
+
"""
|
|
204
|
+
path = Path(destination) / JOURNAL
|
|
205
|
+
if not path.is_file():
|
|
206
|
+
return []
|
|
207
|
+
found: dict[str, Scenario] = {}
|
|
208
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
209
|
+
if not line.strip():
|
|
210
|
+
continue
|
|
211
|
+
try:
|
|
212
|
+
one = Scenario.model_validate(json.loads(line))
|
|
213
|
+
except Exception: # noqa: BLE001 - a half-written line is expected while writers run
|
|
214
|
+
continue
|
|
215
|
+
found[one.name] = one
|
|
216
|
+
return list(found.values())
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def accept_scenario(
|
|
220
|
+
payload: dict[str, Any],
|
|
221
|
+
*,
|
|
222
|
+
world_root: Path,
|
|
223
|
+
catalogue: Catalogue,
|
|
224
|
+
kept: list[Scenario],
|
|
225
|
+
simulator_prompt: str = "",
|
|
226
|
+
hard_constraints: list[str] | None = None,
|
|
227
|
+
allow_empty_solution: bool = False,
|
|
228
|
+
persist: bool = True,
|
|
229
|
+
) -> dict[str, Any]:
|
|
230
|
+
"""Validate one scenario, then prove it. A plain function so both halves are testable.
|
|
231
|
+
|
|
232
|
+
``persist`` is off for a writer that shares the destination with siblings: writing the suite
|
|
233
|
+
removes every folder not in the writer's own list, so persisting here would delete whatever
|
|
234
|
+
the others have proved. Those writers keep their work in ``kept`` and the caller saves once.
|
|
235
|
+
"""
|
|
236
|
+
try:
|
|
237
|
+
scenario = Scenario.model_validate(payload)
|
|
238
|
+
except Exception as invalid:
|
|
239
|
+
return _err(f"Not kept. {invalid}"[:600])
|
|
240
|
+
|
|
241
|
+
# Read against the world this scenario actually runs in, so a setup that creates the table
|
|
242
|
+
# a check reads is not reported as referring to something that does not exist.
|
|
243
|
+
trial, _applied, _ready = prepared(scenario, world_root)
|
|
244
|
+
try:
|
|
245
|
+
problems = validate_scenario(
|
|
246
|
+
scenario,
|
|
247
|
+
catalogue,
|
|
248
|
+
trial.state(),
|
|
249
|
+
simulator_prompt,
|
|
250
|
+
allow_empty_solution=allow_empty_solution,
|
|
251
|
+
)
|
|
252
|
+
problems.extend(contract_sequence_problems(scenario, hard_constraints or []))
|
|
253
|
+
finally:
|
|
254
|
+
trial.close()
|
|
255
|
+
|
|
256
|
+
if problems:
|
|
257
|
+
return _err(
|
|
258
|
+
"Not kept. Fix these and submit again:\n - " + "\n - ".join(problems)
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
proof = prove(
|
|
262
|
+
scenario,
|
|
263
|
+
catalogue,
|
|
264
|
+
world_root,
|
|
265
|
+
allow_judged_only_with_state=allow_empty_solution,
|
|
266
|
+
)
|
|
267
|
+
if not proof.holds:
|
|
268
|
+
said = f"Not kept. {proof.why()}"
|
|
269
|
+
# Code written against the wrong collection shape is the commonest way setup, ready and a
|
|
270
|
+
# check fail here, and the exception alone does not say which collections are mappings and
|
|
271
|
+
# which are lists. The world is asked, so the answer names them.
|
|
272
|
+
if "attribute" in said.lower() or "not subscriptable" in said.lower():
|
|
273
|
+
world = restore(world_root)
|
|
274
|
+
try:
|
|
275
|
+
said += f"\n\n{world.shapes()}"
|
|
276
|
+
finally:
|
|
277
|
+
world.close()
|
|
278
|
+
return _err(said)
|
|
279
|
+
|
|
280
|
+
replaced = any(one.name == scenario.name for one in kept)
|
|
281
|
+
kept[:] = [one for one in kept if one.name != scenario.name]
|
|
282
|
+
kept.append(scenario)
|
|
283
|
+
# A proved scenario is already valuable work. Persist it immediately so a stopped model,
|
|
284
|
+
# browser refresh, process restart, or later scenario failure cannot make the UI say none
|
|
285
|
+
# were written. ``save_scenarios`` remains the suite-level diversity/finality gate.
|
|
286
|
+
if persist:
|
|
287
|
+
write_scenarios(kept, world_root, catalogue)
|
|
288
|
+
else:
|
|
289
|
+
# A writer sharing the destination cannot write folders, so the journal is where its proved
|
|
290
|
+
# work survives the session that proved it.
|
|
291
|
+
journal_scenario(scenario, world_root)
|
|
292
|
+
# Say what the proof did not cover. On a lane where the target's tools have no endpoints, every
|
|
293
|
+
# solution step is recorded without running, so "all three gates pass" is true and misleading:
|
|
294
|
+
# the checks were exercised, the solution was not.
|
|
295
|
+
unproved = (
|
|
296
|
+
"\nNOT PROVED: "
|
|
297
|
+
+ f"{len(proof.assumed)} of {len(scenario.solution)} solution steps were recorded without "
|
|
298
|
+
"running, because these tools have no endpoint in this environment: "
|
|
299
|
+
+ ", ".join(proof.assumed)
|
|
300
|
+
+ ". The checks ran, the solution did not, so this scenario is kept as written rather than "
|
|
301
|
+
"as demonstrated."
|
|
302
|
+
if proof.assumed
|
|
303
|
+
else ""
|
|
304
|
+
)
|
|
305
|
+
proof_summary = (
|
|
306
|
+
"The fixture is ready and its behavioral sub-goals will be judged from the transcript; "
|
|
307
|
+
"this target has no environment tool trajectory to replay."
|
|
308
|
+
if proof.judged_only
|
|
309
|
+
else "All three gates pass: the world is ready for it, the reference solution passes "
|
|
310
|
+
"its checks, and those checks fail when nothing is done."
|
|
311
|
+
)
|
|
312
|
+
return _ok(
|
|
313
|
+
f"{scenario.name} {'replaced' if replaced else 'kept'}. {proof_summary}"
|
|
314
|
+
f"{unproved}\n{len(kept)} so far: " + ", ".join(one.name for one in kept)
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def not_ready(kept: list[Scenario], wanted: int, catalogue: Catalogue) -> list[str]:
|
|
319
|
+
"""Why this suite is not worth saving yet."""
|
|
320
|
+
problems: list[str] = []
|
|
321
|
+
if len(kept) < wanted:
|
|
322
|
+
problems.append(
|
|
323
|
+
f"{len(kept)} of the {wanted} asked for. The ones that find something are usually "
|
|
324
|
+
"the awkward ones, so this is worth finishing rather than stopping here. If nobody "
|
|
325
|
+
f"asked for {wanted}, record what they did ask for with aim_for."
|
|
326
|
+
)
|
|
327
|
+
elif len(kept) > wanted:
|
|
328
|
+
problems.append(
|
|
329
|
+
f"{len(kept)} scenarios against a target of {wanted}. If they asked for more, "
|
|
330
|
+
"aim_for records the new size; reopening a suite starts with the target set to what "
|
|
331
|
+
"is already there, so adding to one always reads like this. If you wrote extra "
|
|
332
|
+
"nobody asked for, drop_scenario takes them off."
|
|
333
|
+
)
|
|
334
|
+
# Two scenarios claiming the same use case are either the same test twice, or one of them is
|
|
335
|
+
# mislabelled. Both happened in the same suite: a delivered-order refusal was filed under
|
|
336
|
+
# "cancel a pending order", which is neither what it tests nor distinguishable afterwards
|
|
337
|
+
# from the scenario that really does test that. A use case is how coverage is counted, so a
|
|
338
|
+
# duplicate quietly overstates it.
|
|
339
|
+
# Keyed on the pair, not the use case alone. A use case fans out into several branches and
|
|
340
|
+
# each is a separate test, so keying on the use case alone caps a suite at one scenario per
|
|
341
|
+
# use case — which is how a request for forty against fourteen use cases became unsaveable.
|
|
342
|
+
claimed: dict[tuple[str, str], list[str]] = {}
|
|
343
|
+
for one in kept:
|
|
344
|
+
case = (one.use_case or "").strip().lower()
|
|
345
|
+
branch = (one.branch or "").strip().lower()
|
|
346
|
+
if case:
|
|
347
|
+
claimed.setdefault((case, branch), []).append(one.name)
|
|
348
|
+
for (case, branch), names in claimed.items():
|
|
349
|
+
if len(names) > 1:
|
|
350
|
+
where = f"{case!r}" if not branch else f"{case!r} / {branch!r}"
|
|
351
|
+
problems.append(
|
|
352
|
+
f"{' and '.join(names)} both claim {where}. Give each the branch it actually "
|
|
353
|
+
"exercises, or drop the one that duplicates the other. Coverage is counted by "
|
|
354
|
+
"use case and branch, so two scenarios sharing both hides a gap."
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
# Sub-goals are shared so results roll up. A suite where every scenario invents its own is a
|
|
358
|
+
# suite whose results cannot be added together.
|
|
359
|
+
used = [name for one in kept for name in one.sub_goals]
|
|
360
|
+
if kept and len(used) > 2 and len(set(used)) == len(used):
|
|
361
|
+
problems.append(
|
|
362
|
+
"no sub-goal is used by more than one scenario, so nothing rolls up across the "
|
|
363
|
+
"suite. Reuse the catalogue where the same thing is being checked."
|
|
364
|
+
)
|
|
365
|
+
return problems
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def scenario_tools(
|
|
369
|
+
contract: AgentContract,
|
|
370
|
+
world_root: Path,
|
|
371
|
+
destination: Path,
|
|
372
|
+
*,
|
|
373
|
+
wanted: int,
|
|
374
|
+
can_save: bool = True,
|
|
375
|
+
start_from: list[Scenario] | None = None,
|
|
376
|
+
) -> tuple[Any, list[Scenario]]:
|
|
377
|
+
"""A server for writing scenarios against one built environment.
|
|
378
|
+
|
|
379
|
+
``can_save`` is what makes several writers safe at once. Saving rewrites the index and
|
|
380
|
+
removes any folder not in the saver's own list, so two writers saving concurrently delete
|
|
381
|
+
each other's work. A writer that only submits keeps its scenarios in ``kept``, and whoever
|
|
382
|
+
spawned it merges the lists and writes once.
|
|
383
|
+
|
|
384
|
+
``start_from`` seeds that list. A parallel writer starts empty rather than from disk, so it
|
|
385
|
+
is never counted as already having what a sibling wrote.
|
|
386
|
+
"""
|
|
387
|
+
kept: list[Scenario] = (
|
|
388
|
+
list(start_from) if start_from is not None else load_scenarios(destination)
|
|
389
|
+
)
|
|
390
|
+
catalogue = load_catalogue(destination)
|
|
391
|
+
simulator_prompt = load_simulator_prompt(destination)
|
|
392
|
+
target = {"count": wanted}
|
|
393
|
+
exploration = {"since_submit": 0}
|
|
394
|
+
external_runtime_target = bool(
|
|
395
|
+
read_manifest(world_root).get("external_runtime", False)
|
|
396
|
+
)
|
|
397
|
+
tool_free_target = not bool(contract.tools) or external_runtime_target
|
|
398
|
+
|
|
399
|
+
# ``branch`` is required because coverage is counted on the use case and branch pair, and the
|
|
400
|
+
# merge drops a repeat of that pair. A writer that leaves it out gives every scenario in its
|
|
401
|
+
# slice the same pair, and all but the first are silently thrown away.
|
|
402
|
+
scenario_required = ["name", "branch", "instruction", "solution", "sub_goals"]
|
|
403
|
+
if contract.conversational:
|
|
404
|
+
scenario_required.append("persona")
|
|
405
|
+
|
|
406
|
+
@tool(
|
|
407
|
+
"inspect_world",
|
|
408
|
+
"Look at what is in the world. Without a table, lists the tables and how many rows each "
|
|
409
|
+
"holds; with one, returns rows from it. `matching` is plain text, not SQL.",
|
|
410
|
+
schema({"table": str, "limit": int, "matching": str}, []),
|
|
411
|
+
)
|
|
412
|
+
async def inspect_world(args: dict[str, Any]) -> dict[str, Any]:
|
|
413
|
+
world = restore(world_root)
|
|
414
|
+
try:
|
|
415
|
+
state = world.state()
|
|
416
|
+
table = str(args.get("table") or "")
|
|
417
|
+
if not table:
|
|
418
|
+
lines = [f"{n}: {len(r)} rows" for n, r in sorted(state.items())]
|
|
419
|
+
if catalogue.sub_goals:
|
|
420
|
+
lines.append(
|
|
421
|
+
"\nsub-goals available: " + ", ".join(sorted(catalogue.names()))
|
|
422
|
+
)
|
|
423
|
+
return _ok("\n".join(lines) or "this world has no tables")
|
|
424
|
+
if table not in state:
|
|
425
|
+
return _err(
|
|
426
|
+
f"no table {table!r}; this world has {', '.join(sorted(state))}"
|
|
427
|
+
)
|
|
428
|
+
rows = state[table]
|
|
429
|
+
# A provisioned store keys rows by id; the generated one keeps a list. Either
|
|
430
|
+
# way what gets shown is rows, not the index over them.
|
|
431
|
+
if isinstance(rows, dict):
|
|
432
|
+
rows = list(rows.values())
|
|
433
|
+
matching = str(args.get("matching") or "").strip()
|
|
434
|
+
if matching:
|
|
435
|
+
needle = matching.lower()
|
|
436
|
+
found = [
|
|
437
|
+
r for r in rows if needle in json.dumps(r, default=str).lower()
|
|
438
|
+
]
|
|
439
|
+
if not found:
|
|
440
|
+
return _ok(
|
|
441
|
+
f"nothing in {table} contains {matching!r}, but it holds {len(rows)} rows."
|
|
442
|
+
)
|
|
443
|
+
rows = found
|
|
444
|
+
shown = rows[: int(args.get("limit") or 20)]
|
|
445
|
+
return _ok(
|
|
446
|
+
f"{len(rows)} rows, showing {len(shown)}:\n"
|
|
447
|
+
+ "\n".join(json.dumps(r, default=str) for r in shown)
|
|
448
|
+
)
|
|
449
|
+
finally:
|
|
450
|
+
world.close()
|
|
451
|
+
|
|
452
|
+
@tool(
|
|
453
|
+
"inspect_scenario",
|
|
454
|
+
"Read one already-kept scenario in full before replacing it. This is the source of "
|
|
455
|
+
"truth for incremental edits after a restart; do not reconstruct a saved scenario from "
|
|
456
|
+
"memory or from its one-line suite summary.",
|
|
457
|
+
schema({"name": str}, ["name"]),
|
|
458
|
+
)
|
|
459
|
+
async def inspect_scenario(args: dict[str, Any]) -> dict[str, Any]:
|
|
460
|
+
name = str(args.get("name") or "")
|
|
461
|
+
found = next((one for one in kept if one.name == name), None)
|
|
462
|
+
if found is None:
|
|
463
|
+
return _err(
|
|
464
|
+
f"no scenario called {name!r}; available: "
|
|
465
|
+
+ (", ".join(one.name for one in kept) or "none")
|
|
466
|
+
)
|
|
467
|
+
return _ok(found.model_dump_json(indent=2))
|
|
468
|
+
|
|
469
|
+
@tool(
|
|
470
|
+
"try_calls",
|
|
471
|
+
"Run calls against a throwaway copy of the world and see the state they leave. Use it to "
|
|
472
|
+
"work out a scenario's solution and what its checks should assert.\n\n"
|
|
473
|
+
"`setup_code` is optional: pass the same code you intend to give the scenario and the "
|
|
474
|
+
"calls run against a world it has already changed, so you can see what the agent would "
|
|
475
|
+
"actually face. Nothing is saved.",
|
|
476
|
+
schema({"calls": list, "setup_code": str}, ["calls"]),
|
|
477
|
+
)
|
|
478
|
+
async def try_calls(args: dict[str, Any]) -> dict[str, Any]:
|
|
479
|
+
if exploration["since_submit"] >= 4:
|
|
480
|
+
return _err(
|
|
481
|
+
"Four throwaway probes have run since the last saved scenario. Submit and prove "
|
|
482
|
+
"one scenario now; if its gate identifies a concrete problem, use the next "
|
|
483
|
+
"probe to correct that problem. Do not map the whole suite before saving work."
|
|
484
|
+
)
|
|
485
|
+
exploration["since_submit"] += 1
|
|
486
|
+
world = restore(world_root)
|
|
487
|
+
try:
|
|
488
|
+
world.reset()
|
|
489
|
+
trial = Scenario(name="trial", setup_code=str(args.get("setup_code") or ""))
|
|
490
|
+
applied = apply_setup(trial, world)
|
|
491
|
+
if not applied.ok:
|
|
492
|
+
return _err(f"the setup did not run: {applied.said}")
|
|
493
|
+
world.calls = []
|
|
494
|
+
lines: list[str] = []
|
|
495
|
+
for step in args.get("calls") or []:
|
|
496
|
+
if not isinstance(step, dict):
|
|
497
|
+
return _err("each call must be an object with a tool and arguments")
|
|
498
|
+
try:
|
|
499
|
+
reference_step = Step.model_validate(step)
|
|
500
|
+
except Exception as invalid:
|
|
501
|
+
return _err(f"invalid reference call: {invalid}"[:600])
|
|
502
|
+
call = play_reference_step(world, reference_step)
|
|
503
|
+
if call.refused:
|
|
504
|
+
lines.append(f"{call.name}: refused — {call.error}")
|
|
505
|
+
elif not call.ok:
|
|
506
|
+
lines.append(f"{call.name}: CRASHED — {call.error}")
|
|
507
|
+
else:
|
|
508
|
+
lines.append(f"{call.name}: ok — {brief(call.result)}")
|
|
509
|
+
state = world.state()
|
|
510
|
+
lines.append(
|
|
511
|
+
"state afterwards: "
|
|
512
|
+
+ ", ".join(f"{n}.count={len(r)}" for n, r in sorted(state.items()))
|
|
513
|
+
)
|
|
514
|
+
for name, rows in sorted(state.items()):
|
|
515
|
+
if rows and len(rows) <= 6:
|
|
516
|
+
lines.append(f"{name}: " + brief(rows, limit=1200))
|
|
517
|
+
return _ok("\n".join(lines) or "no calls were made")
|
|
518
|
+
finally:
|
|
519
|
+
world.close()
|
|
520
|
+
|
|
521
|
+
@tool(
|
|
522
|
+
"add_sub_goal",
|
|
523
|
+
"Add a named thing this agent can be checked on, shared by every scenario that needs it. "
|
|
524
|
+
"`check` is Python: define check(world, calls) returning a sentence when something is "
|
|
525
|
+
"wrong, or None when it held. `world` is the environment afterwards; `calls` is every "
|
|
526
|
+
"tool call made, each with .name, .arguments, .ok and .refused — so a check can insist a "
|
|
527
|
+
"call happened with the right arguments, not merely that it happened. Check the named "
|
|
528
|
+
"outcome using the smallest sufficient evidence. Do not require preparatory or discovery "
|
|
529
|
+
"calls when a later successful state-changing call already proves the outcome; valid "
|
|
530
|
+
"agents may reach the same result through different safe trajectories.\n\n"
|
|
531
|
+
"Use `judged` only where nothing observable settles it, saying what a model must decide "
|
|
532
|
+
"and why code cannot.",
|
|
533
|
+
schema(
|
|
534
|
+
{"name": str, "what": str, "check": str, "judged": str}, ["name", "what"]
|
|
535
|
+
),
|
|
536
|
+
)
|
|
537
|
+
async def add_sub_goal(args: dict[str, Any]) -> dict[str, Any]:
|
|
538
|
+
sub_goal = SubGoal(
|
|
539
|
+
name=str(args.get("name") or ""),
|
|
540
|
+
what=str(args.get("what") or ""),
|
|
541
|
+
check=str(args.get("check") or ""),
|
|
542
|
+
judged=str(args.get("judged") or ""),
|
|
543
|
+
)
|
|
544
|
+
problems = validate_sub_goal(sub_goal)
|
|
545
|
+
if problems:
|
|
546
|
+
return _err("Not added:\n - " + "\n - ".join(problems))
|
|
547
|
+
catalogue.sub_goals = [
|
|
548
|
+
one for one in catalogue.sub_goals if one.name != sub_goal.name
|
|
549
|
+
]
|
|
550
|
+
catalogue.sub_goals.append(sub_goal)
|
|
551
|
+
save_catalogue(catalogue, destination)
|
|
552
|
+
return _ok(
|
|
553
|
+
f"{sub_goal.name} added"
|
|
554
|
+
+ ("" if sub_goal.deterministic() else " (judged, not deterministic)")
|
|
555
|
+
+ f". The catalogue has {len(catalogue.sub_goals)}: "
|
|
556
|
+
+ ", ".join(sorted(catalogue.names()))
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
@tool(
|
|
560
|
+
"submit_scenario",
|
|
561
|
+
"Keep one scenario. It is put through three gates before it is kept, and told which one "
|
|
562
|
+
"failed if any does:\n"
|
|
563
|
+
" 1. ready — the world is restored, setup_code runs, then ready_code. The world "
|
|
564
|
+
"must end up holding what this scenario presumes.\n"
|
|
565
|
+
" 2. solvable — the reference solution is played through that world and the checks of "
|
|
566
|
+
"every sub-goal named must pass.\n"
|
|
567
|
+
" 3. not vacuous — the same checks run again with nothing done at all, and must fail.\n\n"
|
|
568
|
+
"A scenario that clears all three is written out as its own folder of runnable files.",
|
|
569
|
+
schema(
|
|
570
|
+
{
|
|
571
|
+
"name": {
|
|
572
|
+
"type": "string",
|
|
573
|
+
"description": "Short identifier, lower case with hyphens or underscores. "
|
|
574
|
+
"It becomes this scenario's folder name.",
|
|
575
|
+
},
|
|
576
|
+
"use_case": {
|
|
577
|
+
"type": "string",
|
|
578
|
+
"description": "Which of the agent's use cases this belongs to.",
|
|
579
|
+
},
|
|
580
|
+
"branch": {
|
|
581
|
+
"type": "string",
|
|
582
|
+
"description": "The condition that makes this scenario different from the "
|
|
583
|
+
"others in the same use case, in one line: what is true here that is not "
|
|
584
|
+
"true of its siblings.",
|
|
585
|
+
},
|
|
586
|
+
"tests": {
|
|
587
|
+
"type": "string",
|
|
588
|
+
"description": "One line: what this scenario is trying to find out.",
|
|
589
|
+
},
|
|
590
|
+
"background_noise": {
|
|
591
|
+
"type": "string",
|
|
592
|
+
"description": "Where the caller is phoning from: street, transit, vehicle, "
|
|
593
|
+
"outdoors, retail, office or home. Name it whenever the instruction implies "
|
|
594
|
+
"somewhere, a caller leaving a hotel or standing on a street is not in a "
|
|
595
|
+
"quiet room. Left out, it is decided from the scenario name.",
|
|
596
|
+
},
|
|
597
|
+
"call_direction": {
|
|
598
|
+
"type": "string",
|
|
599
|
+
"enum": list(CALL_DIRECTIONS),
|
|
600
|
+
"description": "Who placed the call. Match the contract unless this scenario "
|
|
601
|
+
"deliberately tests the other one. Outbound changes what the instruction has "
|
|
602
|
+
"to be: a person who did not dial has no objective to pursue.",
|
|
603
|
+
},
|
|
604
|
+
"caller_awareness": {
|
|
605
|
+
"type": "string",
|
|
606
|
+
"enum": list(CALLER_AWARENESS),
|
|
607
|
+
"description": "Outbound only, and the thing the scenario is really varying: "
|
|
608
|
+
"whether this person was told to expect the call, half remembers arranging "
|
|
609
|
+
"something, or has no idea why anyone is ringing. Left out it is unaware, "
|
|
610
|
+
"which the agent has to work hardest for.",
|
|
611
|
+
},
|
|
612
|
+
**_mailbox_fields(),
|
|
613
|
+
"instruction": {
|
|
614
|
+
"type": "string",
|
|
615
|
+
"description": "What this person is trying to achieve, written to them. "
|
|
616
|
+
"State the objective first, in their own terms, so they pursue it rather "
|
|
617
|
+
"than narrate a situation: 'Get the cancellation fee refunded', not 'You "
|
|
618
|
+
"were charged a fee'. On an OUTBOUND scenario invert that: they did not "
|
|
619
|
+
"call anyone and have no objective, so give them their situation and what "
|
|
620
|
+
"they would agree to if asked, never an opening request. Then give them "
|
|
621
|
+
"everything they need to hold the "
|
|
622
|
+
"conversation without inventing anything: the facts they know, the values "
|
|
623
|
+
"they can be asked for, and what they will only say once asked. Every value "
|
|
624
|
+
"real and read out of the world.\n"
|
|
625
|
+
"Write only what this person knows before the call starts. Never write what "
|
|
626
|
+
"the agent will do, in any phrasing: not what it will send, offer, ask for, "
|
|
627
|
+
"disclose or decide, and no closing line about what counts as done. Those "
|
|
628
|
+
"are the behaviours under test, and a person primed to expect them plays "
|
|
629
|
+
"along whether or not they happen, so the check passes on a conversation "
|
|
630
|
+
"that never earned it. Give them the value, the preference or the problem "
|
|
631
|
+
"they arrived with, and let the agent's handling of it be what is measured.\n"
|
|
632
|
+
"Test every sentence by asking whether this person could say it out loud. "
|
|
633
|
+
"They have never seen the agent's design, so a parenthetical explaining "
|
|
634
|
+
"where the agent should find a value fails that test just as much as a "
|
|
635
|
+
"sentence predicting what it will say. Worst of all is agreeing in advance "
|
|
636
|
+
"to something the agent has not done yet: that hands over a pass the "
|
|
637
|
+
"conversation never earned.",
|
|
638
|
+
},
|
|
639
|
+
"persona": {
|
|
640
|
+
"type": "object",
|
|
641
|
+
"description": "Who the simulated person is, separate from the task. Use "
|
|
642
|
+
"the established voice-scenario shape and only grounded, test-relevant "
|
|
643
|
+
"details. This fills the simulator prompt's persona slot."
|
|
644
|
+
+ persona_vocabulary_note(),
|
|
645
|
+
"properties": {
|
|
646
|
+
"name": {"type": "string"},
|
|
647
|
+
"gender": persona_field("gender"),
|
|
648
|
+
"age_group": persona_field("age_group"),
|
|
649
|
+
"occupation": persona_field("occupation"),
|
|
650
|
+
"location": persona_field("location"),
|
|
651
|
+
"personality": persona_field("personality"),
|
|
652
|
+
"communication_style": persona_field("communication_style"),
|
|
653
|
+
"initial_message": {
|
|
654
|
+
"type": "string",
|
|
655
|
+
"description": "The caller's natural opening request, specific to "
|
|
656
|
+
"this scenario. Do not use a generic greeting.",
|
|
657
|
+
},
|
|
658
|
+
"keywords": {"type": "array", "items": {"type": "string"}},
|
|
659
|
+
"languages": {
|
|
660
|
+
"type": "array",
|
|
661
|
+
"items": persona_field("languages"),
|
|
662
|
+
},
|
|
663
|
+
"accent": persona_field("accent"),
|
|
664
|
+
"multilingual": {"type": "boolean"},
|
|
665
|
+
"metadata": {"type": "object"},
|
|
666
|
+
},
|
|
667
|
+
"required": [
|
|
668
|
+
"name",
|
|
669
|
+
"personality",
|
|
670
|
+
"communication_style",
|
|
671
|
+
"initial_message",
|
|
672
|
+
"languages",
|
|
673
|
+
"accent",
|
|
674
|
+
"keywords",
|
|
675
|
+
],
|
|
676
|
+
},
|
|
677
|
+
"variables": {
|
|
678
|
+
"type": "object",
|
|
679
|
+
"description": "Any other slot the simulator prompt asks for, by name. Do "
|
|
680
|
+
"not put persona here; use the structured persona field.",
|
|
681
|
+
},
|
|
682
|
+
"fixture": {
|
|
683
|
+
"type": "object",
|
|
684
|
+
"description": "Readable manifest of the concrete data behind this test. "
|
|
685
|
+
"Include origin (seed/generated/mixed) and the identity, credentials, "
|
|
686
|
+
"location, account state or other facts the instruction/setup depends on. "
|
|
687
|
+
"Never put hidden pass/fail checks here.",
|
|
688
|
+
},
|
|
689
|
+
"setup_code": {
|
|
690
|
+
"type": "string",
|
|
691
|
+
"description": "Python defining setup(world): the changes this scenario "
|
|
692
|
+
"makes to the environment before the run. Leave empty to run on the base "
|
|
693
|
+
"world unchanged. Use world.call(tool, args) to act through the agent's own "
|
|
694
|
+
"tools, or world.put, world.change and world.drop for what no tool can produce. This is code and not a list of "
|
|
695
|
+
"rows because a scenario may need more than a table changed.",
|
|
696
|
+
},
|
|
697
|
+
"ready_code": {
|
|
698
|
+
"type": "string",
|
|
699
|
+
"description": "Python defining ready(world): return None when the world "
|
|
700
|
+
"holds what this scenario presumes, or a sentence naming what is missing. "
|
|
701
|
+
"This is the precondition. If the scenario is about the last five items, "
|
|
702
|
+
"check there are five. A scenario whose world was never right tests us, not "
|
|
703
|
+
"the agent.",
|
|
704
|
+
},
|
|
705
|
+
"solution": {
|
|
706
|
+
"type": "array",
|
|
707
|
+
"description": "What a correct agent would do: the reference trajectory. "
|
|
708
|
+
"Never run against the agent under test; it exists to prove the scenario "
|
|
709
|
+
"can be passed at all.",
|
|
710
|
+
"items": {
|
|
711
|
+
"type": "object",
|
|
712
|
+
"properties": {
|
|
713
|
+
"tool": {"type": "string"},
|
|
714
|
+
"arguments": {
|
|
715
|
+
"type": "object",
|
|
716
|
+
"description": "Exactly the model-facing arguments defined by "
|
|
717
|
+
"the agent's tool schema. Never include hidden session state.",
|
|
718
|
+
},
|
|
719
|
+
"environment_arguments": {
|
|
720
|
+
"type": "object",
|
|
721
|
+
"description": "Only for a source-provisioned tool whose raw "
|
|
722
|
+
"dependency needs fields the worker injects: the complete raw "
|
|
723
|
+
"dependency payload used to prove the real state effect. This "
|
|
724
|
+
"is never shown to or credited to the agent. Omit for local "
|
|
725
|
+
"tools and when the two payloads are identical. A value like "
|
|
726
|
+
"`$call.book_ride.booking_ref` resolves that field from the "
|
|
727
|
+
"most recent successful earlier reference call.",
|
|
728
|
+
},
|
|
729
|
+
},
|
|
730
|
+
"required": ["tool", "arguments"],
|
|
731
|
+
},
|
|
732
|
+
},
|
|
733
|
+
"sub_goals": {
|
|
734
|
+
"type": "array",
|
|
735
|
+
"items": {"type": "string"},
|
|
736
|
+
"description": "Names from the shared catalogue that must hold. Use the "
|
|
737
|
+
"existing names wherever one fits, so results add up across the suite.",
|
|
738
|
+
},
|
|
739
|
+
"max_turns": {"type": "integer"},
|
|
740
|
+
},
|
|
741
|
+
scenario_required,
|
|
742
|
+
),
|
|
743
|
+
)
|
|
744
|
+
async def submit_scenario(args: dict[str, Any]) -> dict[str, Any]:
|
|
745
|
+
if external_runtime_target and args.get("solution"):
|
|
746
|
+
return _err(
|
|
747
|
+
"This is a connect-only external provider runtime. Reference tool calls cannot "
|
|
748
|
+
"be replayed against a local world and would be assumed rather than proved. "
|
|
749
|
+
"Use solution: [] and judged sub-goals; the live call supplies the evidence."
|
|
750
|
+
)
|
|
751
|
+
# A writer working one slice of a suite stops at the size it was given. Its turn budget is
|
|
752
|
+
# far larger than its slice, and left to itself it keeps writing: one run proved 559
|
|
753
|
+
# scenarios against a target of 200, spending three times the quota and three times the wall
|
|
754
|
+
# clock, and the surplus is trimmed at the end anyway. Replacing a scenario it already has
|
|
755
|
+
# stays allowed, because fixing a refused one is how a writer finishes its slice.
|
|
756
|
+
if not can_save and wanted:
|
|
757
|
+
named = str(args.get("name") or "").strip()
|
|
758
|
+
already = any(one.name == named for one in kept)
|
|
759
|
+
if not already and len(kept) >= wanted:
|
|
760
|
+
return _err(
|
|
761
|
+
f"This slice is complete: {len(kept)} of {wanted} written. Do not write another. "
|
|
762
|
+
"Say what you covered and what you could not, and stop. Submitting again under "
|
|
763
|
+
"an existing name is the only submission left to you, for fixing one of yours."
|
|
764
|
+
)
|
|
765
|
+
result = accept_scenario(
|
|
766
|
+
args,
|
|
767
|
+
world_root=world_root,
|
|
768
|
+
catalogue=catalogue,
|
|
769
|
+
kept=kept,
|
|
770
|
+
simulator_prompt=simulator_prompt,
|
|
771
|
+
hard_constraints=contract.hard_constraints,
|
|
772
|
+
allow_empty_solution=tool_free_target,
|
|
773
|
+
persist=can_save,
|
|
774
|
+
)
|
|
775
|
+
if not result.get("is_error"):
|
|
776
|
+
exploration["since_submit"] = 0
|
|
777
|
+
return result
|
|
778
|
+
|
|
779
|
+
@tool(
|
|
780
|
+
"amend_contract",
|
|
781
|
+
"Let one of the agent's tools accept values it did not before, when the world holds "
|
|
782
|
+
"something the agent has no way to name. Say why; it is recorded on the contract.",
|
|
783
|
+
schema(
|
|
784
|
+
{"tool_name": str, "argument": str, "values": list, "why": str},
|
|
785
|
+
["tool_name", "argument", "values", "why"],
|
|
786
|
+
),
|
|
787
|
+
)
|
|
788
|
+
async def amend_contract(args: dict[str, Any]) -> dict[str, Any]:
|
|
789
|
+
done, said = widen(
|
|
790
|
+
contract,
|
|
791
|
+
world_root,
|
|
792
|
+
tool_name=str(args.get("tool_name") or ""),
|
|
793
|
+
argument=str(args.get("argument") or ""),
|
|
794
|
+
values=[str(v) for v in (args.get("values") or [])],
|
|
795
|
+
why=str(args.get("why") or ""),
|
|
796
|
+
)
|
|
797
|
+
return _ok(said) if done else _err(said)
|
|
798
|
+
|
|
799
|
+
@tool(
|
|
800
|
+
"add_rule",
|
|
801
|
+
"Give the agent a hard rule its source did not state, when asked for one. It is told to "
|
|
802
|
+
"the agent under test and graded, so this changes what is being tested. Say why.",
|
|
803
|
+
schema({"rule": str, "why": str}, ["rule", "why"]),
|
|
804
|
+
)
|
|
805
|
+
async def add_rule_tool(args: dict[str, Any]) -> dict[str, Any]:
|
|
806
|
+
done, said = add_rule(
|
|
807
|
+
contract,
|
|
808
|
+
world_root,
|
|
809
|
+
rule=str(args.get("rule") or ""),
|
|
810
|
+
why=str(args.get("why") or ""),
|
|
811
|
+
)
|
|
812
|
+
return _ok(said) if done else _err(said)
|
|
813
|
+
|
|
814
|
+
@tool(
|
|
815
|
+
"drop_rule",
|
|
816
|
+
"Take away a hard rule the agent does not really have. Say why.",
|
|
817
|
+
schema({"rule": str, "why": str}, ["rule", "why"]),
|
|
818
|
+
)
|
|
819
|
+
async def drop_rule_tool(args: dict[str, Any]) -> dict[str, Any]:
|
|
820
|
+
done, said = drop_rule(
|
|
821
|
+
contract,
|
|
822
|
+
world_root,
|
|
823
|
+
rule=str(args.get("rule") or ""),
|
|
824
|
+
why=str(args.get("why") or ""),
|
|
825
|
+
)
|
|
826
|
+
return _ok(said) if done else _err(said)
|
|
827
|
+
|
|
828
|
+
@tool(
|
|
829
|
+
"fix_tool",
|
|
830
|
+
"Correct a tool that was read wrong, or remove one the agent does not have. Everything "
|
|
831
|
+
"is built from these, so a wrong argument name produces a world that refuses everything.",
|
|
832
|
+
schema(
|
|
833
|
+
{
|
|
834
|
+
"tool_name": str,
|
|
835
|
+
"args": list,
|
|
836
|
+
"arg_types": dict,
|
|
837
|
+
"description": str,
|
|
838
|
+
"remove": bool,
|
|
839
|
+
"why": str,
|
|
840
|
+
},
|
|
841
|
+
["tool_name", "why"],
|
|
842
|
+
),
|
|
843
|
+
)
|
|
844
|
+
async def fix_tool_tool(args: dict[str, Any]) -> dict[str, Any]:
|
|
845
|
+
done, said = fix_tool(
|
|
846
|
+
contract,
|
|
847
|
+
world_root,
|
|
848
|
+
tool_name=str(args.get("tool_name") or ""),
|
|
849
|
+
why=str(args.get("why") or ""),
|
|
850
|
+
args=[str(a) for a in args["args"]] if args.get("args") else None,
|
|
851
|
+
arg_types={
|
|
852
|
+
str(k): str(v) for k, v in (args.get("arg_types") or {}).items()
|
|
853
|
+
},
|
|
854
|
+
description=str(args.get("description") or ""),
|
|
855
|
+
remove=bool(args.get("remove")),
|
|
856
|
+
)
|
|
857
|
+
return _ok(said) if done else _err(said)
|
|
858
|
+
|
|
859
|
+
@tool(
|
|
860
|
+
"aim_for",
|
|
861
|
+
"Set how many scenarios are wanted. Call it whenever the person changes what they are "
|
|
862
|
+
"asking for: a number outright, or asking for more without naming one, in which case the "
|
|
863
|
+
"count is the size of the suite once you have written them. Adding to an existing suite "
|
|
864
|
+
"always needs this, because reopening one starts with the target set to what is already "
|
|
865
|
+
"there.\n\n"
|
|
866
|
+
"What it is not for is saving a suite nobody asked for. Writing extra and then raising "
|
|
867
|
+
"the target to match is how a request for four becomes thirteen that nobody reviews.",
|
|
868
|
+
schema({"count": int}, ["count"]),
|
|
869
|
+
)
|
|
870
|
+
async def aim_for(args: dict[str, Any]) -> dict[str, Any]:
|
|
871
|
+
count = int(args.get("count") or 0)
|
|
872
|
+
if count < 1:
|
|
873
|
+
return _err("that is not a number of scenarios worth writing")
|
|
874
|
+
target["count"] = count
|
|
875
|
+
return _ok(f"aiming for {count}. {len(kept)} written so far")
|
|
876
|
+
|
|
877
|
+
@tool(
|
|
878
|
+
"drop_scenario",
|
|
879
|
+
"Remove a scenario by name, or all of them with name '*'.",
|
|
880
|
+
schema({"name": str}, ["name"]),
|
|
881
|
+
)
|
|
882
|
+
async def drop_scenario(args: dict[str, Any]) -> dict[str, Any]:
|
|
883
|
+
name = str(args.get("name") or "")
|
|
884
|
+
if name == "*":
|
|
885
|
+
kept.clear()
|
|
886
|
+
write_scenarios(kept, destination, catalogue)
|
|
887
|
+
return _ok("all scenarios dropped")
|
|
888
|
+
before = len(kept)
|
|
889
|
+
kept[:] = [one for one in kept if one.name != name]
|
|
890
|
+
if len(kept) == before:
|
|
891
|
+
return _err(f"no scenario called {name!r}")
|
|
892
|
+
write_scenarios(kept, destination, catalogue)
|
|
893
|
+
return _ok(f"{name} dropped. {len(kept)} left")
|
|
894
|
+
|
|
895
|
+
@tool(
|
|
896
|
+
"generate_suite",
|
|
897
|
+
"Write a whole suite at once by splitting it across the agent's use cases, one writer "
|
|
898
|
+
"per slice, several running at the same time, then reviewing what came back and "
|
|
899
|
+
"filling what it missed. Use this whenever somebody asks for a number of scenarios "
|
|
900
|
+
"rather than one in particular: writing twenty or fifty one at a time runs out of "
|
|
901
|
+
"turns long before it finishes.\n\n"
|
|
902
|
+
"Pass `slices` when you know how the suite should be divided, which you do once you "
|
|
903
|
+
"have looked at the world: give each use case a share in proportion to how much can "
|
|
904
|
+
"genuinely go wrong in it, and name the angle each slice should take. Without it the "
|
|
905
|
+
"work is divided evenly, which pads the thin use cases and under-covers the rich ones. "
|
|
906
|
+
"Everything produced clears the same three gates, and the suite is saved.",
|
|
907
|
+
schema(
|
|
908
|
+
{
|
|
909
|
+
"count": int,
|
|
910
|
+
"at_once": int,
|
|
911
|
+
"slices": {
|
|
912
|
+
"type": ["array", "null"],
|
|
913
|
+
"description": "How to divide the suite. One entry per writer.",
|
|
914
|
+
"items": {
|
|
915
|
+
"type": "object",
|
|
916
|
+
"properties": {
|
|
917
|
+
"use_case": {
|
|
918
|
+
"type": "string",
|
|
919
|
+
"description": "One of the agent's use cases, worded as the "
|
|
920
|
+
"contract words it.",
|
|
921
|
+
},
|
|
922
|
+
"angle": {
|
|
923
|
+
"type": "string",
|
|
924
|
+
"description": "What this slice should look for: the ordinary "
|
|
925
|
+
"path, the branch that cannot be completed, the rule under "
|
|
926
|
+
"pressure, state that has to carry.",
|
|
927
|
+
},
|
|
928
|
+
"count": {
|
|
929
|
+
"type": "integer",
|
|
930
|
+
"description": "How many scenarios this slice is worth, in "
|
|
931
|
+
"proportion to how much can genuinely go wrong in it.",
|
|
932
|
+
},
|
|
933
|
+
"why": {
|
|
934
|
+
"type": "string",
|
|
935
|
+
"description": "Why it earns that share.",
|
|
936
|
+
},
|
|
937
|
+
},
|
|
938
|
+
"required": ["use_case", "count"],
|
|
939
|
+
},
|
|
940
|
+
},
|
|
941
|
+
},
|
|
942
|
+
["count"],
|
|
943
|
+
),
|
|
944
|
+
)
|
|
945
|
+
async def generate_suite(args: dict[str, Any]) -> dict[str, Any]:
|
|
946
|
+
from .scenarios import MOST_AT_ONCE, MOST_IN_ONE_GO, write_in_parallel
|
|
947
|
+
|
|
948
|
+
asked = int(args.get("count") or 0)
|
|
949
|
+
if asked < 1:
|
|
950
|
+
return _err("say how many scenarios the suite should have")
|
|
951
|
+
cases = [one for one in contract.real_use_cases if one.strip()]
|
|
952
|
+
given = args.get("slices") or None
|
|
953
|
+
if not cases and not given:
|
|
954
|
+
return _err(
|
|
955
|
+
"this contract names no use cases, so there is nothing to split the work "
|
|
956
|
+
"across. Write them one at a time with submit_scenario, or fix the contract."
|
|
957
|
+
)
|
|
958
|
+
|
|
959
|
+
# Never write more than the suite still needs. A pass that comes back short is told how many
|
|
960
|
+
# are outstanding and to call again, and a model that calls again with the original number
|
|
961
|
+
# instead of the remainder gets a second full suite: measured at 377 kept against a target of
|
|
962
|
+
# 200, three times the quota for scenarios that are trimmed away again. The count on disk is
|
|
963
|
+
# the only honest measure of what is left, so it is read here rather than trusted from the
|
|
964
|
+
# argument.
|
|
965
|
+
if wanted:
|
|
966
|
+
# Counted from the folders, which are what a suite actually is.
|
|
967
|
+
#
|
|
968
|
+
# Counting the journal as well looked better, because a save prunes the directory before
|
|
969
|
+
# rewriting it and a pass asking during that window reads zero and writes a second suite.
|
|
970
|
+
# It was worse: a retried attempt starts with the folders gone and the journal intact, so
|
|
971
|
+
# the count said the suite was complete, this refused to write anything, and the attempt
|
|
972
|
+
# saved 14 of 200 and failed the platform's cardinality check. Overproduction wastes
|
|
973
|
+
# quota; refusing to produce loses the run, so this counts the conservative thing and the
|
|
974
|
+
# prune window stays a known cost.
|
|
975
|
+
asked = min(asked, max(0, wanted - len(load_scenarios(destination))))
|
|
976
|
+
if not asked:
|
|
977
|
+
return _ok(
|
|
978
|
+
f"The suite already holds the {wanted} it was asked for. Nothing more to write: "
|
|
979
|
+
"review what is there, replace any scenario you are unhappy with by name, and "
|
|
980
|
+
"call save_scenarios."
|
|
981
|
+
)
|
|
982
|
+
# A large ask is served a batch at a time. Spinning up a writer per scenario would put
|
|
983
|
+
# hundreds of model sessions on one machine, and the person waiting would see nothing
|
|
984
|
+
# for an hour. A batch they can read, and an offer of the rest, is the better trade.
|
|
985
|
+
count = min(asked, MOST_IN_ONE_GO)
|
|
986
|
+
at_once = max(1, min(int(args.get("at_once") or 0) or 4, MOST_AT_ONCE))
|
|
987
|
+
|
|
988
|
+
produced = await write_in_parallel(
|
|
989
|
+
contract,
|
|
990
|
+
out=destination,
|
|
991
|
+
wanted=count,
|
|
992
|
+
use_cases=cases,
|
|
993
|
+
slices=given,
|
|
994
|
+
at_once=at_once,
|
|
995
|
+
)
|
|
996
|
+
# The suite is already on disk. The open session's own list has to be brought level with
|
|
997
|
+
# it, or a later save_scenarios here would write out the stale list and delete every
|
|
998
|
+
# folder the fan-out just produced.
|
|
999
|
+
kept[:] = produced
|
|
1000
|
+
target["count"] = len(produced)
|
|
1001
|
+
|
|
1002
|
+
by_case: dict[str, int] = {}
|
|
1003
|
+
for one in produced:
|
|
1004
|
+
name = one.use_case or "unassigned"
|
|
1005
|
+
by_case[name] = by_case.get(name, 0) + 1
|
|
1006
|
+
lines = "\n".join(f" {n} x {case[:70]}" for case, n in sorted(by_case.items()))
|
|
1007
|
+
said = (
|
|
1008
|
+
f"{len(produced)} scenarios across {len(by_case)} use cases, {at_once} writers at a "
|
|
1009
|
+
f"time. Each cleared all three gates and the suite is saved.\n{lines}"
|
|
1010
|
+
)
|
|
1011
|
+
if asked > count:
|
|
1012
|
+
# Never ask here. A hosted run has nobody to answer and no ask tool, so asking ends the
|
|
1013
|
+
# stage with fewer scenarios than were requested and no explanation of why.
|
|
1014
|
+
said += (
|
|
1015
|
+
f"\n\n{asked - count} of the {asked} asked for are still to write. Call "
|
|
1016
|
+
f"generate_suite again now for the remaining {asked - count}, with slices for the "
|
|
1017
|
+
"use cases still short. Do not stop at this batch and do not ask first."
|
|
1018
|
+
)
|
|
1019
|
+
return _ok(said)
|
|
1020
|
+
|
|
1021
|
+
@tool(
|
|
1022
|
+
"save_scenarios",
|
|
1023
|
+
"Write the kept scenarios out. Every one has already been proved by submit_scenario, so "
|
|
1024
|
+
"this always saves; anything else worth knowing comes back alongside.",
|
|
1025
|
+
schema({}, []),
|
|
1026
|
+
)
|
|
1027
|
+
async def save_scenarios(_args: dict[str, Any]) -> dict[str, Any]:
|
|
1028
|
+
# Always written. Each of these already cleared all three gates on its way in, so this is
|
|
1029
|
+
# persistence and not a second opinion: refusing here left proved work in memory only,
|
|
1030
|
+
# which is how a suite that asked for fifty and reached twenty-eight saved nothing at all.
|
|
1031
|
+
# What is off about the suite is said, not enforced.
|
|
1032
|
+
noted = not_ready(kept, target["count"], catalogue)
|
|
1033
|
+
path = write_scenarios(kept, destination, catalogue)
|
|
1034
|
+
diversity = suite_diversity_problems(kept)
|
|
1035
|
+
judged = sum(
|
|
1036
|
+
1
|
|
1037
|
+
for one in kept
|
|
1038
|
+
for name in one.sub_goals
|
|
1039
|
+
if (found := catalogue.named(name)) and not found.deterministic()
|
|
1040
|
+
)
|
|
1041
|
+
said = (
|
|
1042
|
+
f"Saved {len(kept)} scenarios. Each has its own folder under "
|
|
1043
|
+
f"{destination / 'scenarios'} holding scenario.json, setup.py, ready.py and one "
|
|
1044
|
+
f"runnable file per check; {path.name} indexes them.\n"
|
|
1045
|
+
"Every one cleared all three gates: the world is ready for it, the reference "
|
|
1046
|
+
"solution passes its checks, and those checks fail when nothing is done.\n"
|
|
1047
|
+
f"{judged} sub-goal references are judged rather than settled by code."
|
|
1048
|
+
)
|
|
1049
|
+
if noted:
|
|
1050
|
+
said += (
|
|
1051
|
+
"\n\nWorth looking at, none of it stopping the save:\n - "
|
|
1052
|
+
+ "\n - ".join(noted)
|
|
1053
|
+
)
|
|
1054
|
+
if diversity:
|
|
1055
|
+
return _err(
|
|
1056
|
+
said
|
|
1057
|
+
+ "\n\nSaved as a checkpoint, but the suite is not ready to run because its "
|
|
1058
|
+
"fixtures/personas are repetitive:\n - "
|
|
1059
|
+
+ "\n - ".join(diversity)
|
|
1060
|
+
+ "\nReplace the repeated scenarios, then save again."
|
|
1061
|
+
)
|
|
1062
|
+
return _ok(said)
|
|
1063
|
+
|
|
1064
|
+
server = tool_server(
|
|
1065
|
+
name=SCENARIO_SERVER,
|
|
1066
|
+
version="0.1.0",
|
|
1067
|
+
tools=[
|
|
1068
|
+
inspect_world,
|
|
1069
|
+
inspect_scenario,
|
|
1070
|
+
try_calls,
|
|
1071
|
+
add_sub_goal,
|
|
1072
|
+
submit_scenario,
|
|
1073
|
+
amend_contract,
|
|
1074
|
+
add_rule_tool,
|
|
1075
|
+
drop_rule_tool,
|
|
1076
|
+
fix_tool_tool,
|
|
1077
|
+
aim_for,
|
|
1078
|
+
drop_scenario,
|
|
1079
|
+
]
|
|
1080
|
+
# Only the session a person is talking to may fan out. A writer that is itself one slice
|
|
1081
|
+
# of a fan-out calling this would split its own slice again, and so on.
|
|
1082
|
+
+ (
|
|
1083
|
+
[generate_suite, save_scenarios]
|
|
1084
|
+
if can_save and worth_delegating(wanted)
|
|
1085
|
+
else [save_scenarios]
|
|
1086
|
+
if can_save
|
|
1087
|
+
else []
|
|
1088
|
+
),
|
|
1089
|
+
)
|
|
1090
|
+
return server, kept
|
|
1091
|
+
|
|
1092
|
+
|
|
1093
|
+
_ALWAYS = (
|
|
1094
|
+
"inspect_world",
|
|
1095
|
+
"inspect_scenario",
|
|
1096
|
+
"try_calls",
|
|
1097
|
+
"add_sub_goal",
|
|
1098
|
+
"submit_scenario",
|
|
1099
|
+
"amend_contract",
|
|
1100
|
+
"add_rule",
|
|
1101
|
+
"drop_rule",
|
|
1102
|
+
"fix_tool",
|
|
1103
|
+
"aim_for",
|
|
1104
|
+
"drop_scenario",
|
|
1105
|
+
"save_scenarios",
|
|
1106
|
+
)
|
|
1107
|
+
|
|
1108
|
+
|
|
1109
|
+
def tool_names(wanted: int = 0) -> tuple[str, ...]:
|
|
1110
|
+
"""The tools a saving session publishes, which depends on how large a suite it was asked for."""
|
|
1111
|
+
if worth_delegating(wanted):
|
|
1112
|
+
return (*_ALWAYS[:-1], "generate_suite", "save_scenarios")
|
|
1113
|
+
return _ALWAYS
|
|
1114
|
+
|
|
1115
|
+
|
|
1116
|
+
# The whole surface, for anything that needs to name every tool this module can publish rather than
|
|
1117
|
+
# the subset one request gets. Which of them a session actually receives is decided per request.
|
|
1118
|
+
TOOL_NAMES = tool_names(FEWEST_WORTH_DELEGATING)
|
|
1119
|
+
|
|
1120
|
+
|
|
1121
|
+
def world_summary(world_root: Path) -> str:
|
|
1122
|
+
"""What is in the built environment, for grounding the writer before it asks."""
|
|
1123
|
+
world = restore(world_root)
|
|
1124
|
+
try:
|
|
1125
|
+
state = world.state()
|
|
1126
|
+
lines = [f" {name}: {len(rows)} rows" for name, rows in sorted(state.items())]
|
|
1127
|
+
catalogue = load_catalogue(world_root)
|
|
1128
|
+
if catalogue.sub_goals:
|
|
1129
|
+
lines.append(
|
|
1130
|
+
"\nSUB-GOALS already defined (reuse these, do not restate them):"
|
|
1131
|
+
)
|
|
1132
|
+
lines += [f" {one.name}: {one.what}" for one in catalogue.sub_goals]
|
|
1133
|
+
external = bool(read_manifest(world_root).get("external_runtime", False))
|
|
1134
|
+
prefix = "THE BUILT WORLD (restored fresh for every scenario):\n"
|
|
1135
|
+
if external:
|
|
1136
|
+
prefix += (
|
|
1137
|
+
"EXTERNAL PROVIDER RUNTIME: this intentionally empty world cannot seed, "
|
|
1138
|
+
"replay, or inspect provider-owned tools/state. Use solution: [] and judged "
|
|
1139
|
+
"sub-goals; live conversation/tool events are the outcome evidence.\n"
|
|
1140
|
+
)
|
|
1141
|
+
return prefix + "\n".join(lines)
|
|
1142
|
+
finally:
|
|
1143
|
+
world.close()
|