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.
Files changed (642) hide show
  1. agent_learning_kit-0.1.0.dist-info/METADATA +381 -0
  2. agent_learning_kit-0.1.0.dist-info/RECORD +642 -0
  3. agent_learning_kit-0.1.0.dist-info/WHEEL +4 -0
  4. agent_learning_kit-0.1.0.dist-info/entry_points.txt +5 -0
  5. agent_learning_kit-0.1.0.dist-info/licenses/LICENSE +173 -0
  6. agent_learning_kit-0.1.0.dist-info/licenses/NOTICE +7 -0
  7. fi/__init__.py +5 -0
  8. fi/alk/__init__.py +57 -0
  9. fi/alk/_facade.py +31 -0
  10. fi/alk/_module_alias.py +68 -0
  11. fi/alk/_paths.py +14 -0
  12. fi/alk/_schema.py +522 -0
  13. fi/alk/actions.py +727 -0
  14. fi/alk/bench/__init__.py +517 -0
  15. fi/alk/bench/_codeexec.py +213 -0
  16. fi/alk/bench/_coding.py +215 -0
  17. fi/alk/bench/_docker.py +237 -0
  18. fi/alk/bench/_grader.py +286 -0
  19. fi/alk/bench/_pull.py +212 -0
  20. fi/alk/bench/_voice.py +147 -0
  21. fi/alk/capabilities.py +627 -0
  22. fi/alk/cli.py +6396 -0
  23. fi/alk/config.py +130 -0
  24. fi/alk/cua_loop.py +562 -0
  25. fi/alk/evals.py +2351 -0
  26. fi/alk/extensions.py +163 -0
  27. fi/alk/harness/ARCHITECTURE.md +231 -0
  28. fi/alk/harness/DESIGN.md +246 -0
  29. fi/alk/harness/ENVIRONMENT_CONFORMANCE.md +127 -0
  30. fi/alk/harness/HOW-IT-WORKS.md +297 -0
  31. fi/alk/harness/IMPLEMENTATION_AND_VALIDATION_STATUS.md +229 -0
  32. fi/alk/harness/README.md +417 -0
  33. fi/alk/harness/__init__.py +77 -0
  34. fi/alk/harness/__main__.py +3 -0
  35. fi/alk/harness/amend.py +312 -0
  36. fi/alk/harness/artifacts.py +319 -0
  37. fi/alk/harness/authoring_entrypoint.py +189 -0
  38. fi/alk/harness/authoring_runtime_validation.py +267 -0
  39. fi/alk/harness/backends/README.md +43 -0
  40. fi/alk/harness/backends/__init__.py +122 -0
  41. fi/alk/harness/backends/base.py +241 -0
  42. fi/alk/harness/backends/claude.py +211 -0
  43. fi/alk/harness/backends/files.py +182 -0
  44. fi/alk/harness/backends/vertex_gemini.py +457 -0
  45. fi/alk/harness/background_noise.py +95 -0
  46. fi/alk/harness/build.py +385 -0
  47. fi/alk/harness/bundle.py +593 -0
  48. fi/alk/harness/bundle_author_v2.py +1831 -0
  49. fi/alk/harness/bundle_v2.py +719 -0
  50. fi/alk/harness/call_runner.py +1440 -0
  51. fi/alk/harness/callback_http_adapter.py +111 -0
  52. fi/alk/harness/catalogue.py +287 -0
  53. fi/alk/harness/chat.py +428 -0
  54. fi/alk/harness/chat_call_runner.py +506 -0
  55. fi/alk/harness/checks.py +136 -0
  56. fi/alk/harness/cli.py +1354 -0
  57. fi/alk/harness/config.py +338 -0
  58. fi/alk/harness/contract.py +718 -0
  59. fi/alk/harness/credentials.py +674 -0
  60. fi/alk/harness/data/persona_vocabulary.json +111 -0
  61. fi/alk/harness/environment.py +99 -0
  62. fi/alk/harness/environment_plan.py +168 -0
  63. fi/alk/harness/events.py +125 -0
  64. fi/alk/harness/executor.py +304 -0
  65. fi/alk/harness/folder.py +234 -0
  66. fi/alk/harness/generated_runtime.py +815 -0
  67. fi/alk/harness/github.py +72 -0
  68. fi/alk/harness/hosted_authoring_entrypoint.py +183 -0
  69. fi/alk/harness/hosted_entrypoint.py +2402 -0
  70. fi/alk/harness/hosted_scheduler.py +2218 -0
  71. fi/alk/harness/job.py +426 -0
  72. fi/alk/harness/judge.py +184 -0
  73. fi/alk/harness/livekit_source.py +50 -0
  74. fi/alk/harness/livekit_tool_trace_bootstrap.py +71 -0
  75. fi/alk/harness/observability.py +208 -0
  76. fi/alk/harness/outbound.py +3252 -0
  77. fi/alk/harness/packaging.py +515 -0
  78. fi/alk/harness/persona_guides.py +157 -0
  79. fi/alk/harness/platform.py +692 -0
  80. fi/alk/harness/process_preflight.py +764 -0
  81. fi/alk/harness/process_runtime.py +5670 -0
  82. fi/alk/harness/prove.py +425 -0
  83. fi/alk/harness/provider_import.py +703 -0
  84. fi/alk/harness/provider_lifecycle.py +392 -0
  85. fi/alk/harness/provision.py +2896 -0
  86. fi/alk/harness/reception.py +147 -0
  87. fi/alk/harness/retell_chat_call_runner.py +373 -0
  88. fi/alk/harness/run/__init__.py +296 -0
  89. fi/alk/harness/run/alk.py +184 -0
  90. fi/alk/harness/run/call.py +162 -0
  91. fi/alk/harness/run/conversation.py +264 -0
  92. fi/alk/harness/run/data/voices_by_language_and_gender.json +693 -0
  93. fi/alk/harness/run/evidence.py +195 -0
  94. fi/alk/harness/run/grade.py +598 -0
  95. fi/alk/harness/run/live.py +297 -0
  96. fi/alk/harness/run/models.py +56 -0
  97. fi/alk/harness/run/platform_evals.py +227 -0
  98. fi/alk/harness/run/sdk_voice.py +130 -0
  99. fi/alk/harness/run/simulation.py +1209 -0
  100. fi/alk/harness/run/stage.py +91 -0
  101. fi/alk/harness/run/targets.py +508 -0
  102. fi/alk/harness/run/tools.py +601 -0
  103. fi/alk/harness/run/voice.py +340 -0
  104. fi/alk/harness/runtime.py +172 -0
  105. fi/alk/harness/sandbox_server.py +2011 -0
  106. fi/alk/harness/sandbox_worker.py +44 -0
  107. fi/alk/harness/scenario.py +1048 -0
  108. fi/alk/harness/scenario_source.py +879 -0
  109. fi/alk/harness/scenario_tools.py +1143 -0
  110. fi/alk/harness/scenarios.py +915 -0
  111. fi/alk/harness/secrets.py +168 -0
  112. fi/alk/harness/service_catalog.py +97 -0
  113. fi/alk/harness/session.py +391 -0
  114. fi/alk/harness/sessions.py +372 -0
  115. fi/alk/harness/simulator.py +76 -0
  116. fi/alk/harness/simulator_voice.py +928 -0
  117. fi/alk/harness/skills/build-environment/SKILL.md +538 -0
  118. fi/alk/harness/skills/harness.md +131 -0
  119. fi/alk/harness/skills/kinds/chat.md +48 -0
  120. fi/alk/harness/skills/kinds/voice-voicemail.md +63 -0
  121. fi/alk/harness/skills/kinds/voice.md +59 -0
  122. fi/alk/harness/skills/plan-suite/SKILL.md +103 -0
  123. fi/alk/harness/skills/provision-environment/SKILL.md +136 -0
  124. fi/alk/harness/skills/run-scenarios/SKILL.md +112 -0
  125. fi/alk/harness/skills/understand-agent/SKILL.md +251 -0
  126. fi/alk/harness/skills/write-scenarios/SKILL.md +606 -0
  127. fi/alk/harness/skills/write-scenarios/references/refusals.md +28 -0
  128. fi/alk/harness/skills/write-scenarios/references/world-api.md +92 -0
  129. fi/alk/harness/source_data_invariants.py +444 -0
  130. fi/alk/harness/source_tool_evidence.py +79 -0
  131. fi/alk/harness/sources.py +253 -0
  132. fi/alk/harness/spend.py +140 -0
  133. fi/alk/harness/tool_trace_proxy.py +104 -0
  134. fi/alk/harness/tools.py +1018 -0
  135. fi/alk/harness/understand.py +169 -0
  136. fi/alk/harness/voicemail_audio.py +74 -0
  137. fi/alk/harness/world/__init__.py +33 -0
  138. fi/alk/harness/world/errors.py +68 -0
  139. fi/alk/harness/world/expectations.py +91 -0
  140. fi/alk/harness/world/handle.py +538 -0
  141. fi/alk/harness/world/kinds.py +196 -0
  142. fi/alk/harness/world/mutate.py +186 -0
  143. fi/alk/harness/world/probe.py +413 -0
  144. fi/alk/harness/world/provision.py +511 -0
  145. fi/alk/harness/world/provisioned.py +191 -0
  146. fi/alk/harness/world/runtime.py +616 -0
  147. fi/alk/harness/world/snapshot.py +288 -0
  148. fi/alk/harness/world/stores/__init__.py +305 -0
  149. fi/alk/harness/world/stores/container.py +215 -0
  150. fi/alk/harness/world/stores/inprocess.py +346 -0
  151. fi/alk/harness/world/stores/postgres.py +481 -0
  152. fi/alk/harness/world/stores/prove.py +202 -0
  153. fi/alk/harness/world/stores/sqlite.py +245 -0
  154. fi/alk/harness/world/stores/written.py +182 -0
  155. fi/alk/harness/world/tools.py +1516 -0
  156. fi/alk/harness/world/workspace.py +144 -0
  157. fi/alk/image_loop.py +453 -0
  158. fi/alk/image_perturb.py +241 -0
  159. fi/alk/improve.py +274 -0
  160. fi/alk/live/__init__.py +154 -0
  161. fi/alk/live/_attribution.py +184 -0
  162. fi/alk/live/_capture.py +264 -0
  163. fi/alk/live/_codec.py +391 -0
  164. fi/alk/live/_contract.py +134 -0
  165. fi/alk/live/_loopback.py +316 -0
  166. fi/alk/live/_perturb.py +449 -0
  167. fi/alk/live/_runner.py +386 -0
  168. fi/alk/live/_stats.py +561 -0
  169. fi/alk/live/_transcript.py +240 -0
  170. fi/alk/live/_workers/__init__.py +9 -0
  171. fi/alk/live/_workers/a2a_worker.py +316 -0
  172. fi/alk/live/_workers/langgraph_worker.py +217 -0
  173. fi/alk/live/_workers/livekit_worker.py +207 -0
  174. fi/alk/live/_workers/mcp_loopback_server.py +46 -0
  175. fi/alk/live/_workers/mcp_worker.py +158 -0
  176. fi/alk/live/_workers/pipecat_worker.py +189 -0
  177. fi/alk/live/a2a_lane.py +138 -0
  178. fi/alk/live/langgraph_lane.py +339 -0
  179. fi/alk/live/livekit_lane.py +376 -0
  180. fi/alk/live/mcp_lane.py +172 -0
  181. fi/alk/live/pipecat_lane.py +341 -0
  182. fi/alk/live/voice_redteam.py +494 -0
  183. fi/alk/loss.py +306 -0
  184. fi/alk/optimize.py +36260 -0
  185. fi/alk/practice/__init__.py +51 -0
  186. fi/alk/practice/_assess.py +103 -0
  187. fi/alk/practice/_budget.py +81 -0
  188. fi/alk/practice/_calibrate.py +69 -0
  189. fi/alk/practice/_capstone.py +86 -0
  190. fi/alk/practice/_contract.py +91 -0
  191. fi/alk/practice/_diagnose.py +79 -0
  192. fi/alk/practice/_drill.py +196 -0
  193. fi/alk/practice/_experiment.py +720 -0
  194. fi/alk/practice/_schedule.py +102 -0
  195. fi/alk/practice/_store.py +194 -0
  196. fi/alk/practice/_trainer.py +245 -0
  197. fi/alk/practice/_update.py +125 -0
  198. fi/alk/redteam.py +2621 -0
  199. fi/alk/rewardhack.py +237 -0
  200. fi/alk/simulate.py +10351 -0
  201. fi/alk/studio/__init__.py +82 -0
  202. fi/alk/studio/_bias.py +314 -0
  203. fi/alk/studio/_calibration.py +522 -0
  204. fi/alk/studio/_coverage.py +262 -0
  205. fi/alk/studio/_download.py +665 -0
  206. fi/alk/studio/_fidelity_attack.py +114 -0
  207. fi/alk/studio/_generate.py +652 -0
  208. fi/alk/studio/_library.py +370 -0
  209. fi/alk/studio/_scan.py +134 -0
  210. fi/alk/studio/_upgrade.py +42 -0
  211. fi/alk/studio/_vendor.py +172 -0
  212. fi/alk/suite.py +4200 -0
  213. fi/alk/tasks.py +828 -0
  214. fi/alk/telemetry/__init__.py +149 -0
  215. fi/alk/telemetry/_contract.py +141 -0
  216. fi/alk/telemetry/_emit.py +182 -0
  217. fi/alk/telemetry/_ledger.py +296 -0
  218. fi/alk/telemetry/_queue.py +127 -0
  219. fi/alk/telemetry/_row.py +294 -0
  220. fi/alk/telemetry/_run.py +233 -0
  221. fi/alk/telemetry/_sync.py +193 -0
  222. fi/alk/telemetry/_url.py +119 -0
  223. fi/alk/trinity.py +49397 -0
  224. fi/alk/voice_loop.py +174 -0
  225. fi/api/__init__.py +1 -0
  226. fi/api/auth.py +137 -0
  227. fi/api/types.py +29 -0
  228. fi/cli/__init__.py +9 -0
  229. fi/cli/assertions/__init__.py +25 -0
  230. fi/cli/assertions/conditions.py +76 -0
  231. fi/cli/assertions/evaluator.py +286 -0
  232. fi/cli/assertions/exit_codes.py +20 -0
  233. fi/cli/assertions/parser.py +131 -0
  234. fi/cli/assertions/reporter.py +194 -0
  235. fi/cli/commands/__init__.py +9 -0
  236. fi/cli/commands/config.py +165 -0
  237. fi/cli/commands/export.py +208 -0
  238. fi/cli/commands/init.py +112 -0
  239. fi/cli/commands/list_cmd.py +213 -0
  240. fi/cli/commands/run.py +486 -0
  241. fi/cli/commands/validate.py +173 -0
  242. fi/cli/commands/view.py +424 -0
  243. fi/cli/config/__init__.py +6 -0
  244. fi/cli/config/defaults.py +206 -0
  245. fi/cli/config/loader.py +155 -0
  246. fi/cli/config/schema.py +174 -0
  247. fi/cli/main.py +78 -0
  248. fi/cli/output/__init__.py +6 -0
  249. fi/cli/output/formatters.py +106 -0
  250. fi/cli/output/reporters.py +46 -0
  251. fi/cli/storage/__init__.py +5 -0
  252. fi/cli/storage/run_history.py +249 -0
  253. fi/cli/utils/__init__.py +5 -0
  254. fi/cli/utils/console.py +44 -0
  255. fi/evals/__init__.py +131 -0
  256. fi/evals/autoeval/__init__.py +137 -0
  257. fi/evals/autoeval/analyzer.py +211 -0
  258. fi/evals/autoeval/config.py +244 -0
  259. fi/evals/autoeval/export.py +213 -0
  260. fi/evals/autoeval/interactive.py +283 -0
  261. fi/evals/autoeval/pipeline.py +625 -0
  262. fi/evals/autoeval/prompts.py +139 -0
  263. fi/evals/autoeval/recommender.py +242 -0
  264. fi/evals/autoeval/rules.py +589 -0
  265. fi/evals/autoeval/templates.py +299 -0
  266. fi/evals/autoeval/types.py +232 -0
  267. fi/evals/core/__init__.py +16 -0
  268. fi/evals/core/cloud_registry.py +184 -0
  269. fi/evals/core/engines.py +368 -0
  270. fi/evals/core/evaluate.py +319 -0
  271. fi/evals/core/judge_prompt.py +90 -0
  272. fi/evals/core/prompt_generator.py +83 -0
  273. fi/evals/core/registry.py +57 -0
  274. fi/evals/core/result.py +55 -0
  275. fi/evals/evaluator.py +721 -0
  276. fi/evals/execution.py +168 -0
  277. fi/evals/feedback/__init__.py +32 -0
  278. fi/evals/feedback/calibrator.py +160 -0
  279. fi/evals/feedback/collector.py +214 -0
  280. fi/evals/feedback/hooks.py +81 -0
  281. fi/evals/feedback/retriever.py +128 -0
  282. fi/evals/feedback/store.py +272 -0
  283. fi/evals/feedback/types.py +99 -0
  284. fi/evals/framework/README.md +79 -0
  285. fi/evals/framework/__init__.py +267 -0
  286. fi/evals/framework/backends/Dockerfile.eval-runner +33 -0
  287. fi/evals/framework/backends/__init__.py +99 -0
  288. fi/evals/framework/backends/_container.py +141 -0
  289. fi/evals/framework/backends/_utils.py +145 -0
  290. fi/evals/framework/backends/base.py +223 -0
  291. fi/evals/framework/backends/celery_backend.py +417 -0
  292. fi/evals/framework/backends/celery_worker.py +78 -0
  293. fi/evals/framework/backends/kubernetes_backend.py +665 -0
  294. fi/evals/framework/backends/ray_backend.py +521 -0
  295. fi/evals/framework/backends/temporal.py +350 -0
  296. fi/evals/framework/backends/temporal_worker.py +126 -0
  297. fi/evals/framework/backends/thread_pool.py +286 -0
  298. fi/evals/framework/context.py +258 -0
  299. fi/evals/framework/enrichment.py +306 -0
  300. fi/evals/framework/evals/__init__.py +68 -0
  301. fi/evals/framework/evals/agentic.py +399 -0
  302. fi/evals/framework/evals/builder.py +609 -0
  303. fi/evals/framework/evals/semantic.py +142 -0
  304. fi/evals/framework/evaluator.py +647 -0
  305. fi/evals/framework/evaluators/__init__.py +22 -0
  306. fi/evals/framework/evaluators/blocking.py +347 -0
  307. fi/evals/framework/evaluators/non_blocking.py +577 -0
  308. fi/evals/framework/propagation.py +421 -0
  309. fi/evals/framework/protocols.py +385 -0
  310. fi/evals/framework/registry.py +370 -0
  311. fi/evals/framework/resilience/__init__.py +150 -0
  312. fi/evals/framework/resilience/circuit_breaker.py +309 -0
  313. fi/evals/framework/resilience/degradation.py +355 -0
  314. fi/evals/framework/resilience/health.py +505 -0
  315. fi/evals/framework/resilience/rate_limiter.py +228 -0
  316. fi/evals/framework/resilience/retry.py +274 -0
  317. fi/evals/framework/resilience/types.py +288 -0
  318. fi/evals/framework/resilience/wrapper.py +433 -0
  319. fi/evals/framework/types.py +218 -0
  320. fi/evals/guardrails/README.md +915 -0
  321. fi/evals/guardrails/__init__.py +96 -0
  322. fi/evals/guardrails/backends/__init__.py +43 -0
  323. fi/evals/guardrails/backends/azure.py +361 -0
  324. fi/evals/guardrails/backends/base.py +88 -0
  325. fi/evals/guardrails/backends/generic_llm.py +163 -0
  326. fi/evals/guardrails/backends/granite.py +216 -0
  327. fi/evals/guardrails/backends/llamaguard.py +221 -0
  328. fi/evals/guardrails/backends/local_base.py +479 -0
  329. fi/evals/guardrails/backends/openai.py +365 -0
  330. fi/evals/guardrails/backends/qwen.py +170 -0
  331. fi/evals/guardrails/backends/shieldgemma.py +154 -0
  332. fi/evals/guardrails/backends/turing.py +235 -0
  333. fi/evals/guardrails/backends/vllm_client.py +321 -0
  334. fi/evals/guardrails/backends/wildguard.py +188 -0
  335. fi/evals/guardrails/base.py +888 -0
  336. fi/evals/guardrails/config.py +221 -0
  337. fi/evals/guardrails/discovery.py +243 -0
  338. fi/evals/guardrails/gateway.py +437 -0
  339. fi/evals/guardrails/registry.py +231 -0
  340. fi/evals/guardrails/scanners/__init__.py +127 -0
  341. fi/evals/guardrails/scanners/base.py +191 -0
  342. fi/evals/guardrails/scanners/code_injection.py +243 -0
  343. fi/evals/guardrails/scanners/eval_delegate.py +574 -0
  344. fi/evals/guardrails/scanners/invisible_chars.py +351 -0
  345. fi/evals/guardrails/scanners/jailbreak.py +412 -0
  346. fi/evals/guardrails/scanners/language.py +288 -0
  347. fi/evals/guardrails/scanners/pipeline.py +260 -0
  348. fi/evals/guardrails/scanners/regex.py +311 -0
  349. fi/evals/guardrails/scanners/secrets.py +274 -0
  350. fi/evals/guardrails/scanners/topics.py +649 -0
  351. fi/evals/guardrails/scanners/urls.py +341 -0
  352. fi/evals/guardrails/types.py +96 -0
  353. fi/evals/llm/__init__.py +3 -0
  354. fi/evals/llm/base_llm_provider.py +35 -0
  355. fi/evals/llm/providers/litellm.py +70 -0
  356. fi/evals/local/__init__.py +90 -0
  357. fi/evals/local/evaluator.py +690 -0
  358. fi/evals/local/execution_mode.py +121 -0
  359. fi/evals/local/llm.py +489 -0
  360. fi/evals/local/metrics/__init__.py +19 -0
  361. fi/evals/local/registry.py +360 -0
  362. fi/evals/manager.py +1018 -0
  363. fi/evals/manager_types.py +362 -0
  364. fi/evals/metrics/__init__.py +185 -0
  365. fi/evals/metrics/agents/__init__.py +74 -0
  366. fi/evals/metrics/agents/metrics.py +693 -0
  367. fi/evals/metrics/agents/report.py +36463 -0
  368. fi/evals/metrics/agents/types.py +160 -0
  369. fi/evals/metrics/base_llm_metric.py +111 -0
  370. fi/evals/metrics/base_metric.py +138 -0
  371. fi/evals/metrics/code_security/__init__.py +305 -0
  372. fi/evals/metrics/code_security/analyzer.py +985 -0
  373. fi/evals/metrics/code_security/benchmarks/__init__.py +73 -0
  374. fi/evals/metrics/code_security/benchmarks/builtin.py +750 -0
  375. fi/evals/metrics/code_security/benchmarks/loader.py +580 -0
  376. fi/evals/metrics/code_security/benchmarks/types.py +308 -0
  377. fi/evals/metrics/code_security/detectors/__init__.py +186 -0
  378. fi/evals/metrics/code_security/detectors/base.py +394 -0
  379. fi/evals/metrics/code_security/detectors/cryptography.py +345 -0
  380. fi/evals/metrics/code_security/detectors/injection.py +744 -0
  381. fi/evals/metrics/code_security/detectors/secrets.py +287 -0
  382. fi/evals/metrics/code_security/detectors/serialization.py +192 -0
  383. fi/evals/metrics/code_security/joint_metrics.py +588 -0
  384. fi/evals/metrics/code_security/judges/__init__.py +83 -0
  385. fi/evals/metrics/code_security/judges/base.py +238 -0
  386. fi/evals/metrics/code_security/judges/dual_judge.py +534 -0
  387. fi/evals/metrics/code_security/judges/llm_judge.py +301 -0
  388. fi/evals/metrics/code_security/judges/pattern_judge.py +515 -0
  389. fi/evals/metrics/code_security/metrics.py +388 -0
  390. fi/evals/metrics/code_security/modes/__init__.py +63 -0
  391. fi/evals/metrics/code_security/modes/adversarial.py +284 -0
  392. fi/evals/metrics/code_security/modes/autocomplete.py +198 -0
  393. fi/evals/metrics/code_security/modes/base.py +283 -0
  394. fi/evals/metrics/code_security/modes/instruct.py +253 -0
  395. fi/evals/metrics/code_security/modes/repair.py +230 -0
  396. fi/evals/metrics/code_security/reports/__init__.py +57 -0
  397. fi/evals/metrics/code_security/reports/generator.py +404 -0
  398. fi/evals/metrics/code_security/reports/leaderboard.py +509 -0
  399. fi/evals/metrics/code_security/types.py +534 -0
  400. fi/evals/metrics/function_calling/__init__.py +34 -0
  401. fi/evals/metrics/function_calling/metrics.py +573 -0
  402. fi/evals/metrics/function_calling/types.py +87 -0
  403. fi/evals/metrics/hallucination/__init__.py +54 -0
  404. fi/evals/metrics/hallucination/detector.py +149 -0
  405. fi/evals/metrics/hallucination/metrics.py +390 -0
  406. fi/evals/metrics/hallucination/nli.py +253 -0
  407. fi/evals/metrics/hallucination/sentinel.py +106 -0
  408. fi/evals/metrics/hallucination/types.py +132 -0
  409. fi/evals/metrics/heuristics/aggregation_metrics.py +85 -0
  410. fi/evals/metrics/heuristics/json_metrics.py +87 -0
  411. fi/evals/metrics/heuristics/similarity_metrics.py +375 -0
  412. fi/evals/metrics/heuristics/string_metrics.py +391 -0
  413. fi/evals/metrics/llm_as_judges/__init__.py +17 -0
  414. fi/evals/metrics/llm_as_judges/custom_judge/metric.py +112 -0
  415. fi/evals/metrics/llm_as_judges/custom_judge/prompts.py +26 -0
  416. fi/evals/metrics/llm_as_judges/types.py +48 -0
  417. fi/evals/metrics/rag/__init__.py +111 -0
  418. fi/evals/metrics/rag/advanced/__init__.py +14 -0
  419. fi/evals/metrics/rag/advanced/multi_hop.py +283 -0
  420. fi/evals/metrics/rag/advanced/source_attribution.py +344 -0
  421. fi/evals/metrics/rag/generation/__init__.py +17 -0
  422. fi/evals/metrics/rag/generation/answer_relevancy.py +176 -0
  423. fi/evals/metrics/rag/generation/context_utilization.py +245 -0
  424. fi/evals/metrics/rag/generation/faithfulness.py +241 -0
  425. fi/evals/metrics/rag/generation/groundedness.py +131 -0
  426. fi/evals/metrics/rag/rag_score.py +277 -0
  427. fi/evals/metrics/rag/retrieval/__init__.py +20 -0
  428. fi/evals/metrics/rag/retrieval/context_entity_recall.py +124 -0
  429. fi/evals/metrics/rag/retrieval/context_precision.py +158 -0
  430. fi/evals/metrics/rag/retrieval/context_recall.py +106 -0
  431. fi/evals/metrics/rag/retrieval/noise_sensitivity.py +163 -0
  432. fi/evals/metrics/rag/retrieval/ranking.py +261 -0
  433. fi/evals/metrics/rag/types.py +100 -0
  434. fi/evals/metrics/rag/utils/__init__.py +62 -0
  435. fi/evals/metrics/rag/utils/claims.py +189 -0
  436. fi/evals/metrics/rag/utils/entities.py +244 -0
  437. fi/evals/metrics/rag/utils/nli.py +92 -0
  438. fi/evals/metrics/rag/utils/similarity.py +345 -0
  439. fi/evals/metrics/structured/__init__.py +114 -0
  440. fi/evals/metrics/structured/field_completeness.py +313 -0
  441. fi/evals/metrics/structured/hierarchy_score.py +366 -0
  442. fi/evals/metrics/structured/json_validation.py +190 -0
  443. fi/evals/metrics/structured/schema_compliance.py +280 -0
  444. fi/evals/metrics/structured/structured_output_score.py +298 -0
  445. fi/evals/metrics/structured/types.py +108 -0
  446. fi/evals/metrics/structured/validators/__init__.py +30 -0
  447. fi/evals/metrics/structured/validators/base.py +189 -0
  448. fi/evals/metrics/structured/validators/json_validator.py +196 -0
  449. fi/evals/metrics/structured/validators/pydantic_validator.py +178 -0
  450. fi/evals/metrics/structured/validators/yaml_validator.py +248 -0
  451. fi/evals/otel/__init__.py +266 -0
  452. fi/evals/otel/config.py +400 -0
  453. fi/evals/otel/conventions.py +463 -0
  454. fi/evals/otel/enrichment.py +371 -0
  455. fi/evals/otel/instrumentors/__init__.py +140 -0
  456. fi/evals/otel/instrumentors/anthropic.py +517 -0
  457. fi/evals/otel/instrumentors/base.py +382 -0
  458. fi/evals/otel/instrumentors/openai.py +673 -0
  459. fi/evals/otel/processors/__init__.py +36 -0
  460. fi/evals/otel/processors/base.py +473 -0
  461. fi/evals/otel/processors/cost.py +445 -0
  462. fi/evals/otel/processors/evaluation.py +559 -0
  463. fi/evals/otel/processors/llm.py +462 -0
  464. fi/evals/otel/tracer.py +506 -0
  465. fi/evals/otel/types.py +232 -0
  466. fi/evals/otel_utils.py +23 -0
  467. fi/evals/protect.py +671 -0
  468. fi/evals/protect_input_adapter.py +154 -0
  469. fi/evals/streaming/__init__.py +88 -0
  470. fi/evals/streaming/buffer.py +213 -0
  471. fi/evals/streaming/evaluator.py +551 -0
  472. fi/evals/streaming/policy.py +307 -0
  473. fi/evals/streaming/scorers.py +368 -0
  474. fi/evals/streaming/types.py +238 -0
  475. fi/evals/templates.py +472 -0
  476. fi/evals/types.py +156 -0
  477. fi/opt/__init__.py +221 -0
  478. fi/opt/_objective_scoring.py +85 -0
  479. fi/opt/base/__init__.py +11 -0
  480. fi/opt/base/base_generator.py +33 -0
  481. fi/opt/base/base_mapper.py +26 -0
  482. fi/opt/base/base_optimizer.py +45 -0
  483. fi/opt/base/evaluator.py +211 -0
  484. fi/opt/components.py +3095 -0
  485. fi/opt/datamappers/__init__.py +3 -0
  486. fi/opt/datamappers/basic_mapper.py +40 -0
  487. fi/opt/deployment.py +1021 -0
  488. fi/opt/evidence.py +4332 -0
  489. fi/opt/generators/__init__.py +3 -0
  490. fi/opt/generators/litellm.py +66 -0
  491. fi/opt/integrations/__init__.py +23 -0
  492. fi/opt/integrations/generative_suite.py +410 -0
  493. fi/opt/integrations/simulate.py +1313 -0
  494. fi/opt/mutations.py +771 -0
  495. fi/opt/observability.py +4639 -0
  496. fi/opt/optimizer_trace.py +889 -0
  497. fi/opt/optimizers/__init__.py +80 -0
  498. fi/opt/optimizers/agent.py +331 -0
  499. fi/opt/optimizers/agent_bandit.py +392 -0
  500. fi/opt/optimizers/agent_curriculum.py +635 -0
  501. fi/opt/optimizers/agent_evolution.py +894 -0
  502. fi/opt/optimizers/agent_feedback.py +1863 -0
  503. fi/opt/optimizers/agent_pareto.py +547 -0
  504. fi/opt/optimizers/agent_social_memory.py +1113 -0
  505. fi/opt/optimizers/agent_tpe.py +321 -0
  506. fi/opt/optimizers/bayesian_search.py +449 -0
  507. fi/opt/optimizers/council.py +2075 -0
  508. fi/opt/optimizers/futureagi_replay.py +799 -0
  509. fi/opt/optimizers/gepa.py +322 -0
  510. fi/opt/optimizers/metaprompt.py +243 -0
  511. fi/opt/optimizers/promptwizard.py +417 -0
  512. fi/opt/optimizers/protegi.py +329 -0
  513. fi/opt/optimizers/random_search.py +224 -0
  514. fi/opt/research.py +518 -0
  515. fi/opt/simulation.py +260 -0
  516. fi/opt/targets.py +232 -0
  517. fi/opt/types.py +66 -0
  518. fi/opt/utils/__init__.py +4 -0
  519. fi/opt/utils/early_stopping.py +266 -0
  520. fi/opt/utils/setup_logging.py +82 -0
  521. fi/simulate/__init__.py +540 -0
  522. fi/simulate/_hashing.py +35 -0
  523. fi/simulate/_logging.py +10 -0
  524. fi/simulate/adapters.py +87 -0
  525. fi/simulate/agent/__init__.py +120 -0
  526. fi/simulate/agent/browser.py +658 -0
  527. fi/simulate/agent/definition.py +587 -0
  528. fi/simulate/agent/frameworks.py +3528 -0
  529. fi/simulate/agent/generic.py +8286 -0
  530. fi/simulate/agent/import_probe.py +227 -0
  531. fi/simulate/agent/memory.py +905 -0
  532. fi/simulate/agent/mocks.py +101 -0
  533. fi/simulate/agent/multi_agent.py +361 -0
  534. fi/simulate/agent/orchestration.py +903 -0
  535. fi/simulate/agent/realtime.py +665 -0
  536. fi/simulate/agent/wrapper.py +99 -0
  537. fi/simulate/agent/wrappers/__init__.py +18 -0
  538. fi/simulate/agent/wrappers/anthropic.py +62 -0
  539. fi/simulate/agent/wrappers/gemini.py +65 -0
  540. fi/simulate/agent/wrappers/http.py +404 -0
  541. fi/simulate/agent/wrappers/langchain.py +80 -0
  542. fi/simulate/agent/wrappers/openai.py +75 -0
  543. fi/simulate/agent/wrappers/websocket.py +326 -0
  544. fi/simulate/artifacts/__init__.py +11 -0
  545. fi/simulate/artifacts/manifest.py +62 -0
  546. fi/simulate/cli.py +20560 -0
  547. fi/simulate/endpoints/__init__.py +45 -0
  548. fi/simulate/endpoints/_http_actor.py +73 -0
  549. fi/simulate/endpoints/actor_sources.py +243 -0
  550. fi/simulate/endpoints/base.py +107 -0
  551. fi/simulate/endpoints/builtins.py +10 -0
  552. fi/simulate/endpoints/callable.py +95 -0
  553. fi/simulate/endpoints/http.py +76 -0
  554. fi/simulate/endpoints/livekit.py +138 -0
  555. fi/simulate/endpoints/originators.py +132 -0
  556. fi/simulate/endpoints/profiles.py +348 -0
  557. fi/simulate/endpoints/retell.py +633 -0
  558. fi/simulate/endpoints/vapi.py +205 -0
  559. fi/simulate/endpoints/websocket.py +76 -0
  560. fi/simulate/environment.py +33026 -0
  561. fi/simulate/environments/__init__.py +11 -0
  562. fi/simulate/environments/base.py +73 -0
  563. fi/simulate/environments/chat.py +697 -0
  564. fi/simulate/environments/voice.py +212 -0
  565. fi/simulate/evaluation/__init__.py +4 -0
  566. fi/simulate/evaluation/ai_eval.py +227 -0
  567. fi/simulate/evidence/__init__.py +35 -0
  568. fi/simulate/evidence/base.py +59 -0
  569. fi/simulate/evidence/caller_observed.py +50 -0
  570. fi/simulate/evidence/livekit_instrumentation.py +51 -0
  571. fi/simulate/evidence/livekit_room.py +50 -0
  572. fi/simulate/evidence/otel.py +49 -0
  573. fi/simulate/evidence/providers/__init__.py +24 -0
  574. fi/simulate/evidence/providers/base.py +61 -0
  575. fi/simulate/evidence/providers/retell.py +376 -0
  576. fi/simulate/evidence/providers/vapi.py +426 -0
  577. fi/simulate/hosted/__init__.py +32 -0
  578. fi/simulate/hosted/child_entrypoint.py +306 -0
  579. fi/simulate/hosted/job.py +150 -0
  580. fi/simulate/hosted/targets.py +53 -0
  581. fi/simulate/instrumentation/__init__.py +5 -0
  582. fi/simulate/instrumentation/livekit/__init__.py +122 -0
  583. fi/simulate/manifest.py +1033 -0
  584. fi/simulate/matrix_cli.py +165 -0
  585. fi/simulate/realtime/__init__.py +40 -0
  586. fi/simulate/realtime/events.py +107 -0
  587. fi/simulate/realtime/media.py +61 -0
  588. fi/simulate/realtime/session.py +91 -0
  589. fi/simulate/recording/__init__.py +5 -0
  590. fi/simulate/recording/room_recorder.py +326 -0
  591. fi/simulate/registry.py +185 -0
  592. fi/simulate/results/__init__.py +9 -0
  593. fi/simulate/results/base.py +18 -0
  594. fi/simulate/results/filesystem.py +71 -0
  595. fi/simulate/results/futureagi.py +1340 -0
  596. fi/simulate/runtime/__init__.py +85 -0
  597. fi/simulate/runtime/capabilities.py +40 -0
  598. fi/simulate/runtime/events.py +63 -0
  599. fi/simulate/runtime/failures.py +25 -0
  600. fi/simulate/runtime/ids.py +34 -0
  601. fi/simulate/runtime/plan.py +70 -0
  602. fi/simulate/runtime/planner.py +102 -0
  603. fi/simulate/runtime/report.py +174 -0
  604. fi/simulate/runtime/run.py +75 -0
  605. fi/simulate/runtime/runner.py +333 -0
  606. fi/simulate/runtime/spec.py +186 -0
  607. fi/simulate/simulation/__init__.py +30 -0
  608. fi/simulate/simulation/behavior_policy.py +425 -0
  609. fi/simulate/simulation/bridge/__init__.py +9 -0
  610. fi/simulate/simulation/bridge/audio.py +29 -0
  611. fi/simulate/simulation/bridge/connector.py +46 -0
  612. fi/simulate/simulation/bridge/livekit.py +252 -0
  613. fi/simulate/simulation/bridge/retell.py +188 -0
  614. fi/simulate/simulation/bridge/vapi.py +177 -0
  615. fi/simulate/simulation/contract.py +419 -0
  616. fi/simulate/simulation/engines/__init__.py +12 -0
  617. fi/simulate/simulation/engines/base.py +21 -0
  618. fi/simulate/simulation/engines/cloud.py +517 -0
  619. fi/simulate/simulation/engines/livekit.py +4167 -0
  620. fi/simulate/simulation/engines/local_text.py +89 -0
  621. fi/simulate/simulation/fidelity.py +374 -0
  622. fi/simulate/simulation/gemini_tts_stream.py +110 -0
  623. fi/simulate/simulation/generator.py +91 -0
  624. fi/simulate/simulation/goal_machine.py +185 -0
  625. fi/simulate/simulation/livekit_models.py +467 -0
  626. fi/simulate/simulation/matrix.py +170 -0
  627. fi/simulate/simulation/models.py +279 -0
  628. fi/simulate/simulation/runner.py +153 -0
  629. fi/simulate/simulation/synthetic.py +880 -0
  630. fi/simulate/simulation/voice_prompt.py +502 -0
  631. fi/simulate/simulator/__init__.py +55 -0
  632. fi/simulate/simulator/builtins.py +53 -0
  633. fi/simulate/suite.py +1288 -0
  634. fi/simulate/utils/routes.py +164 -0
  635. fi/simulate/voice.py +225 -0
  636. fi/simulate/voice_cli.py +182 -0
  637. fi/utils/__init__.py +1 -0
  638. fi/utils/constants.py +14 -0
  639. fi/utils/errors.py +200 -0
  640. fi/utils/executor.py +26 -0
  641. fi/utils/routes.py +119 -0
  642. fi/utils/utils.py +17 -0
fi/alk/tasks.py ADDED
@@ -0,0 +1,828 @@
1
+ """First-class Task + TaskDataset — the benchmark surface.
2
+
3
+ A Task is a THIN COMPOSITION over models that already exist and are content-
4
+ addressed (no new invention — empirically validated by spike):
5
+
6
+ * the existing typed ``Scenario(kind="task")`` (one effective task row) carries
7
+ goal + verification (fi.simulate.simulation.models);
8
+ * the objective is compiled by ``loss.compile_objective`` VERBATIM, so the
9
+ Goodhart-guard discipline ("There is no override.") holds for a Task exactly
10
+ as for any training loss — a guardless objective is REJECTED here too;
11
+ * the world kind must be a member of ``contract.resolved_world_kinds()`` (the
12
+ frozen closed set + R4 extras) — never widened here;
13
+ * ``execution_class`` is DERIVED from the world kind (+ a fixture flag), never
14
+ asserted above the substrate's truth: ``browser``/``computer_use``/
15
+ ``code_exec``/``voice_telephony`` are TYPED-ONLY in v1 and can be at most
16
+ ``typed_only``; only ``conversation``/``tool_api`` may be ``executable``.
17
+ This is the kit's honesty moat: a typed-only task can NEVER masquerade
18
+ as a live-executed one.
19
+
20
+ Tasks and datasets are content-addressed (sha256 over the canonical payload minus
21
+ ``version``), using the same rounding/canonicalization idiom as ``loss.py``. NO
22
+ provenance/capture fields are baked in yet (the real-execution fork is deferred;
23
+ keeping them out means a later fork decision costs nothing here).
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import hashlib
29
+ import json
30
+ from pathlib import Path
31
+ from typing import Any, Mapping, Sequence
32
+
33
+ from .loss import ObjectiveError, compile_objective
34
+
35
+ AGENT_LEARNING_TASK_KIND = "agent-learning.task.v1"
36
+ AGENT_LEARNING_TASK_DATASET_KIND = "agent-learning.task-dataset.v1"
37
+
38
+ # closed sets (home here; trinity.py mirrors literals for the gate — GUNA_AXES
39
+ # cross-pin pattern; trinity never imports this module).
40
+ V1_TASK_DIFFICULTIES = ("easy", "medium", "hard")
41
+ V1_TASK_EXECUTION_CLASSES = ("executable", "typed_only", "fixture")
42
+
43
+
44
+ class TaskError(ValueError):
45
+ """Raised when a Task violates the §B1 contract. A ``ValueError`` subclass so
46
+ callers can ``except ValueError`` exactly as for ``ObjectiveError``."""
47
+
48
+
49
+ class TaskDatasetError(TaskError):
50
+ """Raised when a TaskDataset violates the contract."""
51
+
52
+
53
+ # --- canonicalization (the loss.py idiom, factored locally) -----------------
54
+ def _round_floats(value: Any) -> Any:
55
+ if isinstance(value, bool):
56
+ return value
57
+ if isinstance(value, float):
58
+ return round(value, 6)
59
+ if isinstance(value, Mapping):
60
+ return {k: _round_floats(v) for k, v in value.items()}
61
+ if isinstance(value, (list, tuple)):
62
+ return [_round_floats(v) for v in value]
63
+ return value
64
+
65
+
66
+ def _content_hash(payload: Mapping[str, Any]) -> str:
67
+ rounded = _round_floats(dict(payload))
68
+ canonical = json.dumps(rounded, sort_keys=True, separators=(",", ":"), default=str)
69
+ return "sha256:" + hashlib.sha256(canonical.encode("utf-8")).hexdigest()
70
+
71
+
72
+ def _resolved_world_kinds() -> tuple[str, ...]:
73
+ """Lazy downward import (the image_loop.py idiom) — the closed set + R4
74
+ extras, never widened here."""
75
+ from fi.simulate.simulation import contract as _contract
76
+
77
+ return tuple(_contract.resolved_world_kinds())
78
+
79
+
80
+ def _executable_world_kinds() -> tuple[str, ...]:
81
+ from fi.simulate.simulation import contract as _contract
82
+
83
+ return tuple(_contract.EXECUTABLE_WORLD_KINDS_V1)
84
+
85
+
86
+ def derive_execution_class(world_kind: str, *, fixture_only: bool = False) -> str:
87
+ """Derive the HONEST substrate stamp. ``fixture_only`` (a task that only ever
88
+ replays a committed fixture) → ``fixture``; a world kind that EXECUTES in v1
89
+ (``conversation``/``tool_api``) → ``executable``; everything else (the
90
+ typed-only kinds) → ``typed_only``. NEVER returns a class above the
91
+ substrate's real capability."""
92
+
93
+ if fixture_only:
94
+ return "fixture"
95
+ if world_kind in _executable_world_kinds():
96
+ return "executable"
97
+ return "typed_only"
98
+
99
+
100
+ def _task_anchor_terms(objective: Mapping[str, Any]) -> list[str]:
101
+ """Terms explicitly flagged as deterministic ground-truth anchors
102
+ (``anchor: true``). The reward-hacking-resistance-by-construction rule below
103
+ requires >= 1; an objective scored only by un-anchored (e.g. judge) terms is
104
+ a Task contract violation — the Task analogue of the image judge-only ban."""
105
+
106
+ return [
107
+ str(term.get("eval"))
108
+ for term in (objective.get("evals") or [])
109
+ if isinstance(term, Mapping) and term.get("anchor") is True and term.get("eval")
110
+ ]
111
+
112
+
113
+ def compile_task(payload: Mapping[str, Any]) -> dict:
114
+ """Validate + stamp a Task. Enforces, ON TOP of the verbatim
115
+ ``loss.compile_objective`` Goodhart guard:
116
+
117
+ (a) ``scenario.kind == "task"`` with exactly ONE effective task row;
118
+ (b) ``world.kind`` is a member of ``resolved_world_kinds()``;
119
+ (c) ``difficulty`` in ``V1_TASK_DIFFICULTIES``;
120
+ (d) the objective carries >= 1 deterministic ground-truth ANCHOR term
121
+ (``anchor: true``) — reward-hacking-resistance by construction;
122
+ (e) ``execution_class`` is DERIVED, never asserted above the substrate
123
+ (a caller-supplied class that overclaims the world kind is rejected).
124
+ Then delegates objective compilation to ``loss.compile_objective`` VERBATIM."""
125
+
126
+ raw = dict(payload)
127
+
128
+ task_id = str(raw.get("id") or "").strip()
129
+ if not task_id:
130
+ raise TaskError("task.id is required")
131
+ title = str(raw.get("title") or "").strip()
132
+ if not title:
133
+ raise TaskError("task.title is required")
134
+
135
+ # (b) world.kind
136
+ world = dict(raw.get("world") or {})
137
+ world_kind = str(world.get("kind") or "")
138
+ resolved = _resolved_world_kinds()
139
+ if world_kind not in resolved:
140
+ raise TaskError(
141
+ f"task.world.kind {world_kind!r} not in resolved world kinds {resolved}"
142
+ )
143
+
144
+ # (c) difficulty
145
+ difficulty = str(raw.get("difficulty") or "medium")
146
+ if difficulty not in V1_TASK_DIFFICULTIES:
147
+ raise TaskError(f"task.difficulty {difficulty!r} not in {V1_TASK_DIFFICULTIES}")
148
+
149
+ # (a) scenario.kind == "task", exactly one effective row
150
+ scenario = dict(raw.get("scenario") or {})
151
+ if str(scenario.get("kind") or "") != "task":
152
+ raise TaskError("task.scenario.kind must be 'task'")
153
+ dataset_rows = list(scenario.get("dataset") or [])
154
+ if len(dataset_rows) != 1:
155
+ raise TaskError(
156
+ f"task.scenario must carry exactly one task row; got {len(dataset_rows)}"
157
+ )
158
+
159
+ # objective: compile VERBATIM (guards enforced; guardless REJECTED here too)
160
+ objective_payload = raw.get("objective")
161
+ if not isinstance(objective_payload, Mapping):
162
+ raise TaskError("task.objective is required (a declared ObjectiveSpec)")
163
+ try:
164
+ compiled_objective = compile_objective(objective_payload)
165
+ except ObjectiveError as exc:
166
+ # surface as a Task error while preserving the guard message
167
+ raise TaskError(f"task.objective invalid: {exc}") from exc
168
+
169
+ # (d) >= 1 deterministic anchor term
170
+ anchors = _task_anchor_terms(objective_payload)
171
+ if not anchors:
172
+ raise TaskError(
173
+ "task.objective must carry >= 1 deterministic ground-truth anchor "
174
+ "term (mark it `anchor: true`); a task scored only by un-anchored "
175
+ "(e.g. judge) terms is reward-hackable by construction"
176
+ )
177
+
178
+ # (e) execution_class derived; reject overclaim
179
+ fixture_only = bool(raw.get("fixture_only"))
180
+ derived_class = derive_execution_class(world_kind, fixture_only=fixture_only)
181
+ asserted = raw.get("execution_class")
182
+ if asserted is not None:
183
+ asserted = str(asserted)
184
+ if asserted not in V1_TASK_EXECUTION_CLASSES:
185
+ raise TaskError(
186
+ f"task.execution_class {asserted!r} not in {V1_TASK_EXECUTION_CLASSES}"
187
+ )
188
+ rank = {"fixture": 0, "typed_only": 1, "executable": 2}
189
+ if rank[asserted] > rank[derived_class]:
190
+ raise TaskError(
191
+ f"task.execution_class {asserted!r} overclaims the substrate; "
192
+ f"world.kind {world_kind!r} (fixture_only={fixture_only}) supports "
193
+ f"at most {derived_class!r}"
194
+ )
195
+
196
+ compiled: dict[str, Any] = {
197
+ "kind": AGENT_LEARNING_TASK_KIND,
198
+ "id": task_id,
199
+ "title": title,
200
+ "world": {"kind": world_kind, **({"spec": dict(world["spec"])} if world.get("spec") else {})},
201
+ "difficulty": difficulty,
202
+ "tags": [str(t) for t in (raw.get("tags") or [])],
203
+ "scenario": scenario,
204
+ "objective": compiled_objective,
205
+ "anchor_terms": anchors,
206
+ "execution_class": derived_class,
207
+ }
208
+ if raw.get("goal") is not None:
209
+ compiled["goal"] = dict(raw["goal"])
210
+ if raw.get("verification") is not None:
211
+ compiled["verification"] = dict(raw["verification"])
212
+ # environments: mock-tool / world env specs the runner wires into the manifest
213
+ # so a TOOL-USING task (mock_tools + a tool-calling agent) runs through the
214
+ # benchmark/RSI loop, not just direct simulate.
215
+ if raw.get("environments"):
216
+ envs = raw["environments"]
217
+ if not isinstance(envs, Sequence) or isinstance(envs, (str, bytes)):
218
+ raise TaskError("task.environments must be a list of env specs")
219
+ compiled["environments"] = [dict(e) for e in envs if isinstance(e, Mapping)]
220
+ compiled["version"] = _content_hash(
221
+ {k: v for k, v in compiled.items() if k != "version"}
222
+ )
223
+ return compiled
224
+
225
+
226
+ def compile_task_dataset(payload: Mapping[str, Any]) -> dict:
227
+ """Validate + stamp a TaskDataset: every task compiles via ``compile_task``;
228
+ ids are unique; any ``splits`` reference existing ids; coverage by world.kind
229
+ and difficulty is computed. Content-addressed over the compiled tasks."""
230
+
231
+ raw = dict(payload)
232
+ name = str(raw.get("name") or "").strip()
233
+ if not name:
234
+ raise TaskDatasetError("dataset.name is required")
235
+
236
+ task_payloads = list(raw.get("tasks") or [])
237
+ if not task_payloads:
238
+ raise TaskDatasetError("dataset.tasks must list >= 1 task")
239
+
240
+ compiled_tasks: list[dict] = []
241
+ seen_ids: set[str] = set()
242
+ by_world_kind: dict[str, int] = {}
243
+ by_difficulty: dict[str, int] = {}
244
+ for index, task_payload in enumerate(task_payloads, start=1):
245
+ if not isinstance(task_payload, Mapping):
246
+ raise TaskDatasetError(f"dataset.tasks[{index}] must be a mapping")
247
+ try:
248
+ task = compile_task(task_payload)
249
+ except TaskError as exc:
250
+ raise TaskDatasetError(f"dataset.tasks[{index}] invalid: {exc}") from exc
251
+ if task["id"] in seen_ids:
252
+ raise TaskDatasetError(f"duplicate task id {task['id']!r}")
253
+ seen_ids.add(task["id"])
254
+ compiled_tasks.append(task)
255
+ by_world_kind[task["world"]["kind"]] = by_world_kind.get(task["world"]["kind"], 0) + 1
256
+ by_difficulty[task["difficulty"]] = by_difficulty.get(task["difficulty"], 0) + 1
257
+
258
+ splits = raw.get("splits")
259
+ normalized_splits: dict[str, list[str]] = {}
260
+ if splits is not None:
261
+ if not isinstance(splits, Mapping):
262
+ raise TaskDatasetError("dataset.splits must be a mapping of name -> [id]")
263
+ for split_name, ids in splits.items():
264
+ id_list = [str(i) for i in (ids or [])]
265
+ missing = [i for i in id_list if i not in seen_ids]
266
+ if missing:
267
+ raise TaskDatasetError(
268
+ f"dataset.splits[{split_name!r}] references unknown ids {missing}"
269
+ )
270
+ normalized_splits[str(split_name)] = id_list
271
+
272
+ compiled: dict[str, Any] = {
273
+ "kind": AGENT_LEARNING_TASK_DATASET_KIND,
274
+ "name": name,
275
+ "tasks": compiled_tasks,
276
+ "coverage": {
277
+ "by_world_kind": dict(sorted(by_world_kind.items())),
278
+ "by_difficulty": dict(sorted(by_difficulty.items())),
279
+ "count": len(compiled_tasks),
280
+ },
281
+ }
282
+ if normalized_splits:
283
+ compiled["splits"] = normalized_splits
284
+ if raw.get("license") is not None:
285
+ compiled["license"] = str(raw["license"])
286
+ compiled["version"] = _content_hash(
287
+ {k: v for k, v in compiled.items() if k != "version"}
288
+ )
289
+ return compiled
290
+
291
+
292
+ def task_world_kinds(dataset: Mapping[str, Any]) -> Sequence[str]:
293
+ """Convenience: the sorted set of world kinds a (compiled) dataset spans."""
294
+
295
+ return sorted({task["world"]["kind"] for task in (dataset.get("tasks") or [])})
296
+
297
+
298
+ def load_task_dataset(path: str | Path) -> dict[str, Any]:
299
+ """Load + compile a TaskDataset from a JSON file (the shipped out-of-the-box
300
+ datasets live under ``examples/task_datasets/``)."""
301
+
302
+ payload = json.loads(Path(path).expanduser().read_text(encoding="utf-8"))
303
+ return compile_task_dataset(payload)
304
+
305
+
306
+ # ===========================================================================
307
+ # B2 — the benchmark runner (run an agent across a dataset, honest scoring).
308
+ # ===========================================================================
309
+ AGENT_LEARNING_BENCHMARK_RESULT_KIND = "agent-learning.benchmark-result.v1"
310
+
311
+ # the closed evidence-class set is OWNED by live/_contract (frozen 4-tuple); the
312
+ # runner never invents one. Non-live execution_classes may NEVER carry a live
313
+ # evidence_class — that is the honesty rule the gate's overclaim tripwire checks.
314
+ _NON_LIVE_EXECUTION_CLASSES = ("fixture", "typed_only")
315
+ _LIVE_EVIDENCE_CLASSES = ("live_lane", "live_stressed")
316
+
317
+
318
+ def _evidence_classes() -> tuple[str, ...]:
319
+ from .live._contract import EVIDENCE_CLASSES
320
+
321
+ return tuple(EVIDENCE_CLASSES)
322
+
323
+
324
+ def _resolve_task_list(
325
+ dataset: Mapping[str, Any],
326
+ *,
327
+ split: str | None,
328
+ max_tasks: int | None,
329
+ ) -> list[dict]:
330
+ tasks_all = list(dataset.get("tasks") or [])
331
+ if split is not None:
332
+ ids = set((dataset.get("splits") or {}).get(split) or [])
333
+ if not ids:
334
+ raise TaskDatasetError(f"benchmark split {split!r} is empty or undefined")
335
+ tasks_all = [t for t in tasks_all if t.get("id") in ids]
336
+ tasks_all = sorted(tasks_all, key=lambda t: str(t.get("id"))) # deterministic order
337
+ if max_tasks is not None:
338
+ tasks_all = tasks_all[: int(max_tasks)]
339
+ return tasks_all
340
+
341
+
342
+ # Canonical objective-anchored scoring lives in ``fi`` (the engine) so the
343
+ # optimizer integration can use it WITHOUT importing fi.alk (the
344
+ # vendored_engine_boundary). Re-exported here so B1/B2/B6 call sites are unchanged.
345
+ from fi.opt._objective_scoring import ( # noqa: E402,F401
346
+ METRIC_ALIASES,
347
+ has_declared_anchor_objective,
348
+ objective_score,
349
+ resolve_metric,
350
+ )
351
+
352
+
353
+ def _score_from_result(
354
+ result: Mapping[str, Any],
355
+ *,
356
+ objective: Mapping[str, Any] | None = None,
357
+ threshold: float = 0.5,
358
+ ) -> tuple[str, float, dict, dict]:
359
+ """Extract (verdict, score, metric_averages, scoring) from a run result.
360
+
361
+ The headline ``score`` is the OBJECTIVE score (weighted mean over the task's
362
+ declared terms) when the objective resolves >=1 term — the signal with real
363
+ dynamic range. It falls back to the engine ``evaluation_score`` only when no
364
+ declared term maps to a metric. ``verdict`` is pass iff score >= threshold."""
365
+
366
+ summary = result.get("summary") or {}
367
+ metric_averages = dict(summary.get("metric_averages") or {})
368
+ raw_eval = float(summary.get("evaluation_score") or 0.0)
369
+
370
+ obj = objective_score(metric_averages, objective or {})
371
+ if obj["score"] is not None:
372
+ score = float(obj["score"])
373
+ basis = "objective"
374
+ else:
375
+ score = raw_eval
376
+ basis = "evaluation_score_fallback"
377
+ verdict = "pass" if score >= threshold else "fail"
378
+ scoring = {
379
+ "basis": basis,
380
+ "raw_evaluation_score": round(raw_eval, 6),
381
+ "threshold": threshold,
382
+ "terms_resolved": obj["terms_resolved"],
383
+ "terms_total": obj["terms_total"],
384
+ "per_term": obj["per_term"],
385
+ }
386
+ return verdict, round(score, 6), metric_averages, scoring
387
+
388
+
389
+ def run_benchmark(
390
+ dataset: Mapping[str, Any],
391
+ agent: Mapping[str, Any],
392
+ *,
393
+ split: str | None = None,
394
+ max_tasks: int | None = None,
395
+ seed: int = 42,
396
+ evidence_class: str = "captured_fixture",
397
+ detect_reward_hacks: bool = False,
398
+ runner: Any = None,
399
+ emit_telemetry: bool = True,
400
+ project_name: str | None = None,
401
+ ) -> dict[str, Any]:
402
+ """Run one ``agent`` across a (compiled) TaskDataset and return a scored,
403
+ comparable result. Each per-task result is stamped with the task's HONEST
404
+ ``execution_class`` and the run's ``evidence_class`` — and a non-live
405
+ execution_class carrying a live evidence_class is flagged ``overclaim=True``
406
+ (never silently downgraded; the gate asserts none in the fixture lane).
407
+
408
+ The actual run goes through the EXISTING engine (``simulate.build_task_run_
409
+ manifest`` -> ``simulate.run_manifest``), proven feasible on conversation/
410
+ tool_api by the execution spike. ``runner`` is an injectable seam (a
411
+ callable ``manifest -> result``) for deterministic testing without the
412
+ engine; when ``None`` the real engine is used.
413
+ """
414
+
415
+ if evidence_class not in _evidence_classes():
416
+ raise TaskError(
417
+ f"evidence_class {evidence_class!r} not in {_evidence_classes()}"
418
+ )
419
+
420
+ task_list = _resolve_task_list(dataset, split=split, max_tasks=max_tasks)
421
+ if not task_list:
422
+ raise TaskDatasetError("benchmark resolved zero tasks to run")
423
+
424
+ per_task: list[dict[str, Any]] = []
425
+ for task in task_list:
426
+ verdict, score, metric_averages, scoring, tool_calls, run_error = _run_one_task(
427
+ task, agent, seed=seed, runner=runner
428
+ )
429
+ execution_class = str(task.get("execution_class") or "typed_only")
430
+ overclaim = (
431
+ execution_class in _NON_LIVE_EXECUTION_CLASSES
432
+ and evidence_class in _LIVE_EVIDENCE_CLASSES
433
+ )
434
+ row: dict[str, Any] = {
435
+ "task_id": str(task.get("id")),
436
+ "world_kind": str(task.get("world", {}).get("kind")),
437
+ "difficulty": str(task.get("difficulty")),
438
+ "verdict": verdict,
439
+ "score": score,
440
+ "execution_class": execution_class,
441
+ "evidence_class": evidence_class,
442
+ "overclaim": bool(overclaim),
443
+ "metric_averages": metric_averages,
444
+ "tool_calls": list(tool_calls or []),
445
+ "scoring": scoring,
446
+ }
447
+ # #3: auto-wire the reward-hack detector — a candidate that GAMES the
448
+ # scorer (e.g. claims completion with zero tool calls on a tool-anchored
449
+ # objective) is FAILED even if objective_score passed (the deterministic
450
+ # anti-gaming anchor objective_score alone misses, e.g. vacuous
451
+ # tool_selection_accuracy=1.0 for an agent that never called a tool).
452
+ if detect_reward_hacks and verdict == "pass":
453
+ from . import rewardhack as _rh
454
+
455
+ traj = {"metric_averages": metric_averages, "tool_calls": list(tool_calls or []),
456
+ "score": score}
457
+ verdict_obj = _rh.score_trajectory(traj, objective=task.get("objective") or {})
458
+ if verdict_obj["hacked"]:
459
+ row["verdict"] = "fail"
460
+ row["rewardhack"] = verdict_obj
461
+ row["score"] = 0.0
462
+ if run_error is not None:
463
+ row["error"] = run_error
464
+ per_task.append(row)
465
+
466
+ aggregate = _aggregate(per_task, evidence_class)
467
+ result = {
468
+ "kind": AGENT_LEARNING_BENCHMARK_RESULT_KIND,
469
+ "dataset_version": str(dataset.get("version") or ""),
470
+ "dataset_name": str(dataset.get("name") or ""),
471
+ "split": split,
472
+ "seed": int(seed),
473
+ "agent_kind": str(agent.get("type") or ""),
474
+ "per_task": per_task,
475
+ "aggregate": aggregate,
476
+ }
477
+ if emit_telemetry:
478
+ # W&B/promptfoo dashboard wiring (Phase 14): one run -> root + per-task
479
+ # spans + a dashboard URL when keyed (mode=auto), else a local log. A
480
+ # side-channel — never alters the returned scores.
481
+ from .telemetry import emit_run
482
+
483
+ summary = emit_run(
484
+ kind="benchmark",
485
+ name=result["dataset_name"] or "benchmark",
486
+ metrics={
487
+ "n_tasks": aggregate.get("count", 0),
488
+ "pass_rate": aggregate.get("pass_rate", 0.0),
489
+ "mean_score": aggregate.get("mean_score", 0.0),
490
+ },
491
+ verdict="pass" if aggregate.get("pass_rate", 0.0) >= 0.5 else "fail",
492
+ children=[
493
+ (
494
+ f"task:{r['task_id']}",
495
+ {
496
+ "verdict": r.get("verdict"),
497
+ "score": r.get("score"),
498
+ "world_kind": r.get("world_kind"),
499
+ },
500
+ )
501
+ for r in per_task
502
+ ],
503
+ project_name=project_name,
504
+ )
505
+ result["telemetry"] = summary.as_dict()
506
+ return result
507
+
508
+
509
+ def _run_one_task(
510
+ task: Mapping[str, Any],
511
+ agent: Mapping[str, Any],
512
+ *,
513
+ seed: int,
514
+ runner: Any,
515
+ ) -> tuple[str, float, dict, dict, str | None]:
516
+ scenario = dict(task.get("scenario") or {})
517
+ verification = dict(task.get("verification") or {})
518
+ threshold = float(verification.get("threshold", 0.7))
519
+ objective = dict(task.get("objective") or {})
520
+ # translate the task's verification + scenario row into a REAL evaluation
521
+ # config so the run is scored (not a hollow status='passed' with score 0.0).
522
+ row = (scenario.get("dataset") or [{}])[0] if scenario.get("dataset") else {}
523
+ task_description = str(row.get("situation") or task.get("title") or task.get("id"))
524
+ expected_result = str(row.get("outcome") or "")
525
+ success_criteria = [
526
+ str(c.get("value"))
527
+ for c in (verification.get("checks") or [])
528
+ if isinstance(c, Mapping) and c.get("value") is not None
529
+ ]
530
+ try:
531
+ if runner is not None:
532
+ result = runner(task, agent)
533
+ else:
534
+ import asyncio
535
+
536
+ from . import simulate as _simulate
537
+
538
+ manifest = _simulate.build_task_run_manifest(
539
+ name=str(task.get("id")),
540
+ agent=dict(agent),
541
+ scenario=scenario,
542
+ task_description=task_description,
543
+ expected_result=expected_result or None,
544
+ success_criteria=success_criteria,
545
+ threshold=threshold,
546
+ environments=list(task.get("environments") or ()),
547
+ auto_execute_tools=True,
548
+ max_turns=int((task.get("world", {}).get("spec") or {}).get("max_turns", 1)),
549
+ simulation_engine=str(
550
+ (task.get("world", {}).get("spec") or {}).get("engine")
551
+ or "local_text"
552
+ ),
553
+ )
554
+ # run_manifest is async; the benchmark runner is a synchronous
555
+ # top-level API, so drive the coroutine to completion here.
556
+ try:
557
+ asyncio.get_running_loop()
558
+ except RuntimeError:
559
+ result = asyncio.run(_simulate.run_manifest(manifest))
560
+ else: # pragma: no cover - benchmark is not called inside a loop
561
+ raise RuntimeError(
562
+ "run_benchmark cannot run inside an active event loop; "
563
+ "call it from synchronous code"
564
+ )
565
+ verdict, score, metric_averages, scoring = _score_from_result(
566
+ result, objective=objective, threshold=threshold
567
+ )
568
+ return verdict, score, metric_averages, scoring, _result_tool_calls(result), None
569
+ except Exception as exc: # noqa: BLE001 — a failed task scores void, never crashes the sweep
570
+ return "void", 0.0, {}, {}, [], f"{type(exc).__name__}: {str(exc)[:160]}"
571
+
572
+
573
+ def _result_tool_calls(result: Mapping[str, Any]) -> list:
574
+ """Extract the agent's tool_calls from a run result (needed by the reward-hack
575
+ detector to measure effort). Shape-tolerant across the runner seam."""
576
+ if not isinstance(result, Mapping):
577
+ return []
578
+ direct = result.get("tool_calls")
579
+ if isinstance(direct, list):
580
+ return direct
581
+ results = (result.get("report") or {}).get("results") if isinstance(result.get("report"), Mapping) else None
582
+ if isinstance(results, list) and results and isinstance(results[0], Mapping):
583
+ return list(results[0].get("tool_calls") or [])
584
+ return []
585
+
586
+
587
+ def _aggregate(per_task: Sequence[Mapping[str, Any]], evidence_class: str) -> dict:
588
+ n = len(per_task)
589
+ passed = sum(1 for r in per_task if r["verdict"] == "pass")
590
+ mean_score = round(sum(float(r["score"]) for r in per_task) / n, 6) if n else 0.0
591
+
592
+ def _rollup(key: str) -> dict[str, dict]:
593
+ out: dict[str, dict] = {}
594
+ for r in per_task:
595
+ bucket = out.setdefault(str(r[key]), {"count": 0, "passed": 0, "score_sum": 0.0})
596
+ bucket["count"] += 1
597
+ bucket["passed"] += 1 if r["verdict"] == "pass" else 0
598
+ bucket["score_sum"] += float(r["score"])
599
+ for bucket in out.values():
600
+ bucket["mean_score"] = round(bucket.pop("score_sum") / bucket["count"], 6)
601
+ return dict(sorted(out.items()))
602
+
603
+ any_live = any(r["evidence_class"] in _LIVE_EVIDENCE_CLASSES for r in per_task)
604
+ any_overclaim = any(r.get("overclaim") for r in per_task)
605
+ return {
606
+ "count": n,
607
+ "passed": passed,
608
+ "pass_rate": round(passed / n, 6) if n else 0.0,
609
+ "mean_score": mean_score,
610
+ "by_world_kind": _rollup("world_kind"),
611
+ "by_difficulty": _rollup("difficulty"),
612
+ "by_execution_class": _rollup("execution_class"),
613
+ "honesty": {
614
+ "evidence_class": evidence_class,
615
+ "fixture_only": not any_live,
616
+ "any_live": any_live,
617
+ "any_overclaim": any_overclaim,
618
+ },
619
+ }
620
+
621
+
622
+ # ===========================================================================
623
+ # RSI loop — close the loop: dataset -> optimize -> verify on HELD-OUT.
624
+ # ===========================================================================
625
+ AGENT_LEARNING_RSI_REPORT_KIND = "agent-learning.rsi-report.v1"
626
+
627
+
628
+ def _apply_candidate(base_agent: Mapping[str, Any], assignment: Mapping[str, Any]) -> dict:
629
+ """Materialize one agent config from the base + a search-space assignment.
630
+ Keys may be dotted ``agent.<field>`` (the kit's whole-agent convention) or a
631
+ bare field; both write onto the agent dict."""
632
+
633
+ agent = dict(base_agent)
634
+ for path, value in assignment.items():
635
+ field = str(path).split(".", 1)[1] if str(path).startswith("agent.") else str(path)
636
+ agent[field] = value
637
+ return agent
638
+
639
+
640
+ def _candidate_grid(search_space: Mapping[str, Sequence[Any]], *, cap: int) -> list[dict]:
641
+ """Cartesian product of the (finite) search space, deterministically ordered,
642
+ capped at ``cap`` candidates (logged when truncated)."""
643
+
644
+ import itertools
645
+
646
+ keys = sorted(search_space)
647
+ value_lists = [list(search_space[k]) for k in keys]
648
+ grid = [dict(zip(keys, combo)) for combo in itertools.product(*value_lists)]
649
+ return grid[: max(1, int(cap))]
650
+
651
+
652
+ def optimize_against_dataset(
653
+ dataset: Mapping[str, Any],
654
+ base_agent: Mapping[str, Any],
655
+ search_space: Mapping[str, Sequence[Any]],
656
+ *,
657
+ train_split: str = "train",
658
+ test_split: str = "test",
659
+ max_candidates: int = 16,
660
+ seed: int = 42,
661
+ evidence_class: str = "captured_fixture",
662
+ runner: Any = None,
663
+ emit_telemetry: bool = True,
664
+ project_name: str | None = None,
665
+ detect_reward_hacks: bool = False,
666
+ ) -> dict[str, Any]:
667
+ """Close the RSI loop: search agent configs, score each on the TRAIN split via
668
+ the (objective-anchored, discriminating) ``run_benchmark``, pick the winner,
669
+ then VERIFY it beats the baseline on the HELD-OUT TEST split.
670
+
671
+ The held-out check is the honest RSI guard (the advisor's bar): report lift on
672
+ tasks the optimizer never optimized against, never the metric it climbed. If
673
+ the dataset has no ``splits``, train==test is used and ``held_out`` is False
674
+ (a non-generalization-proving run, flagged as such — no overclaim)."""
675
+
676
+ splits = dataset.get("splits") or {}
677
+ has_split = bool(splits.get(train_split)) and bool(splits.get(test_split))
678
+ train = train_split if has_split else None
679
+ test = test_split if has_split else None
680
+
681
+ def _score(agent: Mapping[str, Any], split: str | None) -> dict:
682
+ # emit_telemetry=False: the optimizer emits ONE run for the whole search
683
+ # (below), not one per candidate benchmark (Phase 14).
684
+ # detect_reward_hacks: when on, a candidate that GAMES a declared anchor
685
+ # (e.g. claims completion with zero tool calls on a tool-anchored
686
+ # objective) is FAILED — an optimizer that climbs a gameable metric is the
687
+ # textbook reward-hacking failure, so honest config search opts in.
688
+ return run_benchmark(
689
+ dataset, agent, split=split, seed=seed,
690
+ evidence_class=evidence_class, runner=runner, emit_telemetry=False,
691
+ detect_reward_hacks=detect_reward_hacks,
692
+ )["aggregate"]
693
+
694
+ candidates = _candidate_grid(search_space, cap=max_candidates)
695
+ truncated = len(_candidate_grid(search_space, cap=10**9)) > len(candidates)
696
+
697
+ leaderboard: list[dict[str, Any]] = []
698
+ for idx, assignment in enumerate(candidates):
699
+ agent = _apply_candidate(base_agent, assignment)
700
+ train_agg = _score(agent, train)
701
+ leaderboard.append({
702
+ "candidate_index": idx,
703
+ "assignment": assignment,
704
+ "train_mean_score": train_agg["mean_score"],
705
+ "train_pass_rate": train_agg["pass_rate"],
706
+ })
707
+ leaderboard.sort(key=lambda r: (-r["train_mean_score"], r["candidate_index"]))
708
+ winner = leaderboard[0]
709
+ winner_agent = _apply_candidate(base_agent, winner["assignment"])
710
+
711
+ # held-out verification: winner vs baseline (the base_agent) on TEST
712
+ baseline_test = _score(base_agent, test)
713
+ winner_test = _score(winner_agent, test)
714
+ lift = round(winner_test["mean_score"] - baseline_test["mean_score"], 6)
715
+
716
+ result = {
717
+ "kind": AGENT_LEARNING_RSI_REPORT_KIND,
718
+ "dataset_version": str(dataset.get("version") or ""),
719
+ "candidates_evaluated": len(candidates),
720
+ "candidates_truncated": truncated,
721
+ "leaderboard": leaderboard,
722
+ "winner": {"assignment": winner["assignment"],
723
+ "train_mean_score": winner["train_mean_score"]},
724
+ "held_out": {
725
+ "verified": bool(has_split), # a real held-out split existed
726
+ "test_split": test_split if has_split else "(train==test; not held-out)",
727
+ "baseline_mean_score": baseline_test["mean_score"],
728
+ "winner_mean_score": winner_test["mean_score"],
729
+ "lift": lift,
730
+ "improved": lift > 0,
731
+ },
732
+ }
733
+ if emit_telemetry:
734
+ # ONE dashboard run for the whole search: root + per-candidate spans (P14).
735
+ from .telemetry import emit_run
736
+
737
+ summary = emit_run(
738
+ kind="optimize",
739
+ name=str(dataset.get("name") or "optimize"),
740
+ metrics={
741
+ "candidates": len(candidates),
742
+ "winner_train_score": winner["train_mean_score"],
743
+ "held_out_lift": lift,
744
+ "improved": lift > 0,
745
+ },
746
+ verdict="pass" if lift > 0 else "fail",
747
+ children=[
748
+ (
749
+ f"candidate:{row['candidate_index']}",
750
+ {"train_mean_score": row["train_mean_score"],
751
+ "train_pass_rate": row["train_pass_rate"]},
752
+ )
753
+ for row in leaderboard
754
+ ],
755
+ project_name=project_name,
756
+ )
757
+ result["telemetry"] = summary.as_dict()
758
+ return result
759
+
760
+
761
+ # ===========================================================================
762
+ # #2 — real cell_scorer for the 13D practice loop (replaces the all-pass no-op).
763
+ # ===========================================================================
764
+ def make_cell_scorer(
765
+ *,
766
+ agent: Mapping[str, Any],
767
+ objective: Mapping[str, Any],
768
+ scenario: Mapping[str, Any] | None = None,
769
+ environments: Sequence[Mapping[str, Any]] = (),
770
+ threshold: float = 0.5,
771
+ evidence_class: str = "captured_fixture",
772
+ runner: Any = None,
773
+ ) -> Any:
774
+ """Build a REAL ``cell_scorer`` for ``practice.run_practice_loop`` that runs the
775
+ agent per cell and scores via ``objective_score`` (engine ``metric_averages`` →
776
+ the ``{scalar, verdict, evidence_class}`` shape the loop consumes).
777
+
778
+ Replaces the loop's all-pass no-op default (``scalar 1.0, verdict pass``) so the
779
+ 13D practice loop — assess/diagnose/spaced-regression-replay/consolidate —
780
+ measures REAL fitness instead of accepting everything (audit gap #2). Joins
781
+ diagnose-from-trace + no-forgetting (13D) with the discriminating objective
782
+ score. ``runner`` is an injectable seam for deterministic tests."""
783
+
784
+ def cell_scorer(cell: Mapping[str, Any]) -> dict[str, Any]:
785
+ sc = dict(scenario or {})
786
+ # the cell's persona overrides the scenario's row when present, so distinct
787
+ # obligation cells exercise distinct conditions (not one fixed run).
788
+ if cell.get("persona") and sc.get("dataset"):
789
+ row0 = dict((sc["dataset"] or [{}])[0])
790
+ row0["persona"] = {"name": str(cell["persona"])[:24]}
791
+ sc = {**sc, "dataset": [row0]}
792
+ if runner is not None:
793
+ result = runner(cell, agent)
794
+ else:
795
+ import asyncio
796
+
797
+ from . import simulate as _sim
798
+
799
+ row = (sc.get("dataset") or [{}])[0] if sc.get("dataset") else {}
800
+ manifest = _sim.build_task_run_manifest(
801
+ name=str(cell.get("intent") or "cell"),
802
+ agent=dict(agent),
803
+ scenario=sc or None,
804
+ task_description=str(row.get("situation") or cell.get("intent") or "task"),
805
+ expected_result=str(row.get("outcome") or "") or None,
806
+ environments=list(environments),
807
+ auto_execute_tools=True,
808
+ threshold=threshold,
809
+ )
810
+ try:
811
+ asyncio.get_running_loop()
812
+ except RuntimeError:
813
+ result = asyncio.run(_sim.run_manifest(manifest))
814
+ else: # pragma: no cover
815
+ raise RuntimeError("make_cell_scorer cannot run inside an event loop")
816
+
817
+ metrics = (result.get("summary") or {}).get("metric_averages") or {}
818
+ s = objective_score(metrics, objective).get("score")
819
+ scalar = float(s) if s is not None else 0.0
820
+ return {
821
+ "eval": "agent_report",
822
+ "scalar": round(scalar, 6),
823
+ "verdict": "pass" if scalar >= threshold else "fail",
824
+ "evidence_class": evidence_class,
825
+ "metric_averages": metrics,
826
+ }
827
+
828
+ return cell_scorer