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
@@ -0,0 +1,215 @@
1
+ """Standing an engine up in a container, which is the part no engine does differently.
2
+
3
+ Pulling an image, giving it a free port, waiting for it to actually answer, tearing it down
4
+ and not leaking it when a run is killed: none of that is about Postgres. It is the same work
5
+ for MySQL, ClickHouse, Mongo or anything else the harness is ever asked to run, so it is
6
+ written once here.
7
+
8
+ What an engine contributes is only what genuinely differs -- how to reach it, how to read what
9
+ it holds, and how to put that back. That is a small surface deliberately, because the cost of
10
+ teaching the harness a new engine is the thing that decides whether "whatever the agent uses"
11
+ is real or just an aspiration.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import os
17
+ import secrets
18
+ import subprocess
19
+ import time
20
+
21
+ from . import Held, StoreError
22
+
23
+ # How long to wait for a fresh container to start answering. The first run on a machine pulls
24
+ # the image, which dominates; afterwards this is a second or two.
25
+ READY_TIMEOUT_SECONDS = 180.0
26
+
27
+ # Marks every container this module starts, so strays from a killed run can be found and
28
+ # removed without guessing at names.
29
+ LABEL = "alk.harness.store"
30
+
31
+ # The network to join, when the harness is itself in a container. Publishing a port to the
32
+ # host's loopback is enough when the harness runs on the host, but from inside a container
33
+ # 127.0.0.1 is its own loopback and the engine is not there. Sharing a network instead lets
34
+ # the engine be reached by container name, on the port it actually listens on.
35
+ NETWORK = "ALK_DOCKER_NETWORK"
36
+
37
+
38
+ def docker(*args: str, check: bool = True) -> str:
39
+ """Run a docker command, and turn its failure into something worth reading."""
40
+ try:
41
+ done = subprocess.run( # nosec B603: list args, never shell=True
42
+ ("docker", *args), capture_output=True, text=True, check=False
43
+ )
44
+ except FileNotFoundError as exc: # pragma: no cover - depends on the machine
45
+ raise StoreError(
46
+ "docker is not on PATH, so no store can be stood up. Install Docker, or start "
47
+ "Colima, and try again."
48
+ ) from exc
49
+ if check and done.returncode != 0:
50
+ raise StoreError(
51
+ f"docker {' '.join(args)} failed ({done.returncode}): "
52
+ f"{(done.stderr or done.stdout).strip()}"
53
+ )
54
+ return done.stdout.strip()
55
+
56
+
57
+ class ContainerStore(Held):
58
+ """An engine the harness runs in a container for the agent to be pointed at.
59
+
60
+ Started once for a suite and reset between scenarios: standing an engine up costs seconds
61
+ and putting its data back costs milliseconds, so the container stays and only its contents
62
+ move.
63
+
64
+ Subclasses supply ``image``, ``container_port``, the environment the image needs, and how
65
+ to read and restore what it holds. Everything else is here.
66
+ """
67
+
68
+ engine: str = ""
69
+ image: str = ""
70
+ container_port: int = 0
71
+ # Environment the image needs to come up with a known user, password and database. Values
72
+ # are formatted with ``user``, ``password`` and ``database``.
73
+ boot_env: dict[str, str] = {}
74
+
75
+ def __init__(
76
+ self,
77
+ version: str | None = None,
78
+ image: str | None = None,
79
+ database: str = "alk",
80
+ user: str = "alk",
81
+ password: str | None = None,
82
+ ) -> None:
83
+ default = type(self).image
84
+ if image:
85
+ self.image = image
86
+ elif version:
87
+ self.image = f"{default.split(':')[0]}:{version}"
88
+ else:
89
+ self.image = default
90
+ self.database = database
91
+ self.user = user
92
+ self.password = password or secrets.token_hex(16)
93
+ self.container = f"alk-store-{secrets.token_hex(6)}"
94
+ self.network = os.environ.get(NETWORK, "").strip()
95
+ self.host = "127.0.0.1"
96
+ self.port: int | None = None
97
+ self._started = False
98
+ # Every script `apply` has run, in order. Saved beside the rows so a restore into a
99
+ # fresh container can stand the schema up before putting the rows back.
100
+ self.applied: list[str] = []
101
+
102
+ # -- lifecycle -------------------------------------------------------------------
103
+
104
+ def start(self) -> None:
105
+ """Stand the container up and block until it answers. Idempotent."""
106
+ if self._started:
107
+ return
108
+ environment: list[str] = []
109
+ for name, template in self.boot_env.items():
110
+ environment += [
111
+ "--env",
112
+ f"{name}={template.format(user=self.user, password=self.password, database=self.database)}",
113
+ ]
114
+ docker(
115
+ "run",
116
+ "--detach",
117
+ "--name",
118
+ self.container,
119
+ "--label",
120
+ f"{LABEL}=1",
121
+ *environment,
122
+ *(("--network", self.network) if self.network else ()),
123
+ # Bound to loopback and given whatever port is free, so parallel runs on one
124
+ # machine never collide. Kept even on a shared network, where it is what lets
125
+ # someone on the host open a client against a running scenario.
126
+ "--publish",
127
+ f"127.0.0.1::{self.container_port}",
128
+ self.image,
129
+ )
130
+ self._started = True
131
+ if self.network:
132
+ self.host, self.port = self.container, self.container_port
133
+ else:
134
+ self.port = self._published_port()
135
+ self._await_ready()
136
+
137
+ def stop(self) -> None:
138
+ """Remove the container. Safe when it never started, so teardown needs no guard."""
139
+ if not self._started:
140
+ return
141
+ docker("rm", "--force", "--volumes", self.container, check=False)
142
+ self._started = False
143
+ self.port = None
144
+
145
+ def _published_port(self) -> int:
146
+ mapping = docker("port", self.container, f"{self.container_port}/tcp")
147
+ if not mapping:
148
+ raise StoreError(
149
+ f"{self.container} published no port for {self.container_port}/tcp"
150
+ )
151
+ # "127.0.0.1:32768", or several lines when both stacks are bound.
152
+ return int(mapping.splitlines()[0].rsplit(":", 1)[1])
153
+
154
+ def _await_ready(self) -> None:
155
+ """Poll until the engine answers, and say what went wrong if it never does.
156
+
157
+ A container that is running is not an engine that is ready: most database images start,
158
+ run their own initialisation, restart once, and only then listen. Connecting is the
159
+ only honest test, which is why this asks the subclass to really connect rather than
160
+ checking that the process exists.
161
+ """
162
+ deadline = time.monotonic() + READY_TIMEOUT_SECONDS
163
+ last: Exception | None = None
164
+ while time.monotonic() < deadline:
165
+ try:
166
+ self.probe()
167
+ return
168
+ except Exception as exc: # noqa: BLE001 - any failure means not ready yet
169
+ last = exc
170
+ time.sleep(0.25)
171
+ logs = docker("logs", "--tail", "20", self.container, check=False)
172
+ container = self.container
173
+ # A container that never answers is not a container to leave running -- `start()` already
174
+ # set `_started`, so without this the caller's own teardown never runs (nothing ever calls
175
+ # `stop()` on a store whose `start()` raised) and the container leaks for good.
176
+ self.stop()
177
+ raise StoreError(
178
+ f"{container} did not answer within {READY_TIMEOUT_SECONDS:.0f}s: {last}\n"
179
+ f"last lines of its log:\n{logs}"
180
+ )
181
+
182
+ def probe(self) -> None:
183
+ """Really talk to the engine. Anything raised means "not ready yet"."""
184
+ raise NotImplementedError
185
+
186
+ # -- what the agent is pointed at ------------------------------------------------
187
+
188
+ def dsn(self) -> str:
189
+ """The connection string to hand the agent, in place of its own."""
190
+ raise NotImplementedError
191
+
192
+ def env(self, variable: str) -> dict[str, str]:
193
+ """The DSN under the name this agent reads it from.
194
+
195
+ Redirecting an agent is usually one environment variable, and which one is a fact about
196
+ the agent rather than about us -- so it is named by the caller, never assumed here.
197
+ """
198
+ return {variable: self.dsn()}
199
+
200
+ def address(self) -> tuple[str, int]:
201
+ if not self._started or self.port is None:
202
+ raise StoreError("the store has not been started, so it has no address yet")
203
+ return self.host, self.port
204
+
205
+
206
+ def strays() -> list[str]:
207
+ """Containers the harness started that are still running.
208
+
209
+ A killed run leaves its container behind, and the next one has no way to know it is not the
210
+ owner. Naming them is enough; removing them is the caller's decision.
211
+ """
212
+ listed = docker(
213
+ "ps", "--filter", f"label={LABEL}=1", "--format", "{{.Names}}", check=False
214
+ )
215
+ return [name for name in listed.splitlines() if name.strip()]
@@ -0,0 +1,346 @@
1
+ """The agent's own data, held where the agent holds it.
2
+
3
+ Plenty of real agents keep their state in memory, loaded from files their repository ships, and
4
+ they are not unusual. There is no engine to stand up for those, no port and no connection string.
5
+ Standing up a database for them and hoping the agent notices would be exactly the replication
6
+ this path exists to avoid.
7
+
8
+ So the store is the structure itself, and the agent's own loader is what fills it. The tools
9
+ under test then run against that structure the same way they run in production, because it *is*
10
+ the thing they run against: unmodified code, its real data, and a copy taken before each scenario
11
+ so the next one starts where the last one began.
12
+
13
+ With no loader given, this holds nothing at all and says so. That is the honest description of a
14
+ world whose records the agent's code keeps on itself rather than in anything a store can reach,
15
+ and it exists so such a world is not described as a database it does not have.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import copy
21
+ import json
22
+ from pathlib import Path
23
+ from typing import Any, Callable, Mapping, Sequence
24
+
25
+ from . import Snapshot, StoreError
26
+
27
+ # Carried alongside a record whose group is keyed rather than listed, because the key is usually
28
+ # the id a check needs to name and rebuilding the group without it would throw it away.
29
+ ID = "_id"
30
+
31
+
32
+ class InProcessStore:
33
+ """The agent's own in-memory data, as a store.
34
+
35
+ ``loader`` is the agent's function, imported from the agent's repository and called, never
36
+ reimplemented, so what is held is what the agent would hold on a cold start.
37
+ """
38
+
39
+ engine = "in_process"
40
+ key = "in_process"
41
+ # Deliberately not state.json, which the snapshot uses for the agent's own state object. Two
42
+ # different things sharing one filename means whichever is written second wins, and the world
43
+ # comes back with its records on the wrong side of the seam: the store empty, everything in
44
+ # the agent's state, and the mutation gate then emptying a store that was never holding it.
45
+ FILE = "collections.json"
46
+
47
+ def __init__(
48
+ self,
49
+ database: str | Path = "",
50
+ *,
51
+ loader: Callable[[], dict[str, Any]] | None = None,
52
+ module: str = "",
53
+ function: str = "load_data",
54
+ root: str | Path = "",
55
+ **_ignored: Any,
56
+ ) -> None:
57
+ # Takes the same arguments as any other store and uses most of them only when there is a
58
+ # loader, so opening one is the same call whichever kind it turns out to be.
59
+ self.database = str(database or "")
60
+ self.loader = loader
61
+ self.module = module
62
+ self.function = function
63
+ self.root = str(root or "")
64
+ self.data: dict[str, Any] = {}
65
+ self._started = False
66
+
67
+ # -- lifecycle -------------------------------------------------------------------
68
+
69
+ def start(self) -> None:
70
+ """Load the agent's data by calling the agent's own loader, if there is one."""
71
+ if self._started or (self.loader is None and not self.module):
72
+ return
73
+ if self.loader is None:
74
+ self.loader = self._imported()
75
+ loaded = self.loader()
76
+ if not isinstance(loaded, dict):
77
+ raise StoreError(
78
+ f"{self.function} returned {type(loaded).__name__}, not a dict of named groups, "
79
+ "so there is nothing a check could read by name"
80
+ )
81
+ self.data = loaded
82
+ self._started = True
83
+
84
+ def _imported(self) -> Callable[[], dict[str, Any]]:
85
+ """The agent's loader, imported from the agent's repository.
86
+
87
+ Deliberately an import of their code rather than a reimplementation of it. If it will not
88
+ import, that is worth stopping for: the alternative is inventing data and grading the
89
+ agent against a world it has never seen.
90
+ """
91
+ import importlib
92
+ import sys
93
+
94
+ if self.root and self.root not in sys.path:
95
+ sys.path.insert(0, self.root)
96
+ try:
97
+ found = importlib.import_module(self.module)
98
+ except ImportError as exc:
99
+ raise StoreError(
100
+ f"cannot import {self.module!r} from {self.root or 'sys.path'}: {exc}. The "
101
+ "agent's own dependencies have to be importable for its loader to run."
102
+ ) from exc
103
+ loader = getattr(found, self.function, None)
104
+ if not callable(loader):
105
+ raise StoreError(f"{self.module}.{self.function} is not a function")
106
+ return loader
107
+
108
+ def stop(self) -> None:
109
+ self.data = {}
110
+ self._started = False
111
+
112
+ def dsn(self) -> str:
113
+ """Nothing connects to this, which is the point.
114
+
115
+ Reported rather than raised: a store with no address is a fact about this kind of agent,
116
+ not a failure, and it is recorded so nothing later goes looking for a connection string
117
+ that was never going to exist.
118
+ """
119
+ return "inprocess://"
120
+
121
+ # -- statements ------------------------------------------------------------------
122
+
123
+ def apply(self, script: str) -> None:
124
+ """Run a snippet against the data, with ``data`` in scope and nothing else.
125
+
126
+ How a seed is expressed for a store with no query language: the same Python the agent's
127
+ own code would use to reach into its structures.
128
+ """
129
+ if not script.strip():
130
+ return
131
+ namespace: dict[str, Any] = {"data": self.data, "json": json}
132
+ try:
133
+ exec(compile(script, "<seed>", "exec"), namespace) # nosec B102
134
+ except Exception as exc: # noqa: BLE001 - the caller's snippet, reported as given
135
+ raise StoreError(f"{type(exc).__name__}: {exc}") from exc
136
+
137
+ def execute(self, statement: str, params: Sequence[Any] = ()) -> int:
138
+ raise StoreError(
139
+ "this agent keeps its state in its own code, so there is no query language to run "
140
+ "statements in. Change the world through the agent's own tools, or through "
141
+ "world.put, world.change and world.drop."
142
+ )
143
+
144
+ def query(self, statement: str, params: Sequence[Any] = ()) -> list[dict[str, Any]]:
145
+ return []
146
+
147
+ # -- records ---------------------------------------------------------------------
148
+
149
+ def collections(self) -> list[str]:
150
+ return sorted(self.data)
151
+
152
+ def holds(self, collection: str) -> bool:
153
+ return collection in self.data
154
+
155
+ def records(self, collection: str) -> list[dict[str, Any]]:
156
+ return self._rows(self.data.get(collection))
157
+
158
+ def state(self) -> dict[str, list[dict[str, Any]]]:
159
+ """Every group and its records, in the shape the checks already expect.
160
+
161
+ The agent's structures are usually keyed by id rather than listed, so a mapping becomes
162
+ records with the key carried along. Without that a check counting records in a group
163
+ would be counting nothing, and the id it needs to name would have been thrown away.
164
+ """
165
+ return {name: self._rows(group) for name, group in self.data.items()}
166
+
167
+ @staticmethod
168
+ def _rows(group: Any) -> list[dict[str, Any]]:
169
+ if isinstance(group, dict):
170
+ return [
171
+ {ID: key, **value}
172
+ if isinstance(value, dict)
173
+ else {ID: key, "value": value}
174
+ for key, value in group.items()
175
+ ]
176
+ if isinstance(group, list):
177
+ return [row if isinstance(row, dict) else {"value": row} for row in group]
178
+ if group is None:
179
+ return []
180
+ return [{"value": group}]
181
+
182
+ def start_collection(self, collection: str, *, keyed: bool = False) -> None:
183
+ """Make a collection that does not exist yet.
184
+
185
+ For an agent with no store of its own, every collection is one the harness invents, so
186
+ there is nothing to declare them in advance the way a schema does for a database.
187
+ """
188
+ if collection not in self.data:
189
+ self.data[collection] = {} if keyed else []
190
+
191
+ def add(self, collection: str, record: Mapping[str, Any]) -> int:
192
+ group = self.data.get(collection)
193
+ if isinstance(group, list):
194
+ group.append(dict(record))
195
+ return 1
196
+ if isinstance(group, dict):
197
+ written = dict(record)
198
+ identifier = written.pop(ID, None)
199
+ if identifier is None:
200
+ raise KeyError(
201
+ f"{collection} is keyed, so a new record needs its key given as {ID!r}"
202
+ )
203
+ group[identifier] = written
204
+ return 1
205
+ raise KeyError(f"no group {collection!r} here to add to")
206
+
207
+ def amend(
208
+ self, collection: str, key: str, changes: Mapping[str, Any], *, by: str = ""
209
+ ) -> int:
210
+ group = self.data.get(collection)
211
+ if isinstance(group, dict) and not by:
212
+ if key not in group:
213
+ return 0
214
+ group[key].update(dict(changes))
215
+ return 1
216
+ for row in self._writable(collection, group, key, by):
217
+ row.update(dict(changes))
218
+ return len(self._writable(collection, group, key, by))
219
+
220
+ def remove(self, collection: str, key: str = "", *, by: str = "") -> int:
221
+ group = self.data.get(collection)
222
+ if isinstance(group, dict):
223
+ if not key:
224
+ gone = len(group)
225
+ group.clear()
226
+ return gone
227
+ if by:
228
+ matched = [
229
+ name for name, row in group.items() if _reads(row, by) == key
230
+ ]
231
+ else:
232
+ matched = [key] if key in group else []
233
+ for name in matched:
234
+ group.pop(name, None)
235
+ return len(matched)
236
+ if isinstance(group, list):
237
+ if not key:
238
+ gone = len(group)
239
+ group.clear()
240
+ return gone
241
+ if not by:
242
+ raise KeyError(
243
+ f"{collection} is a list, so removing one record needs the field it is keyed on"
244
+ )
245
+ kept = [row for row in group if _reads(row, by) != key]
246
+ gone = len(group) - len(kept)
247
+ group[:] = kept
248
+ return gone
249
+ raise KeyError(f"no group {collection!r} here to remove from")
250
+
251
+ def _writable(
252
+ self, collection: str, group: Any, key: str, by: str
253
+ ) -> list[dict[str, Any]]:
254
+ if group is None:
255
+ raise KeyError(f"no group {collection!r} here to change")
256
+ if not by:
257
+ raise KeyError(
258
+ f"{collection} is a list, so changing a record needs the field it is keyed on"
259
+ )
260
+ if isinstance(group, dict):
261
+ # A keyed group stores the key as the mapping's key, because `add` pops ``_id`` out
262
+ # of the record to put it there. So asking to match on ``_id`` finds nothing, changes
263
+ # nothing, and returns zero, which a scenario's setup does not look at: the run is
264
+ # then graded against a world that was never set up. The key is answered here as
265
+ # though it were still a field, which is what whoever wrote it meant.
266
+ if by == ID and key in group:
267
+ row = group[key]
268
+ return [row] if isinstance(row, dict) else []
269
+ found: Any = group.values()
270
+ else:
271
+ found = group
272
+ return [
273
+ row for row in found if isinstance(row, dict) and _reads(row, by) == key
274
+ ]
275
+
276
+ # -- going back ------------------------------------------------------------------
277
+
278
+ def clear(self) -> None:
279
+ """Empty every group, keeping its shape: the agent's own code indexes into these."""
280
+ for name, group in self.data.items():
281
+ if isinstance(group, dict):
282
+ group.clear()
283
+ elif isinstance(group, list):
284
+ group.clear()
285
+ else:
286
+ self.data[name] = None
287
+
288
+ def freeze(self) -> Snapshot:
289
+ """A deep copy. Nothing sits behind these records, so there are no counters to carry."""
290
+ return Snapshot(rows=copy.deepcopy(self.state()), counters={})
291
+
292
+ def restore(self, snapshot: Snapshot) -> None:
293
+ """Put the structure back the way the agent's loader left it.
294
+
295
+ Rebuilt from the records rather than kept as a second copy, so restore is checked against
296
+ exactly what ``state`` reports: the thing the gate compares and the thing a check reads
297
+ are then the same thing, and cannot drift apart.
298
+ """
299
+ rebuilt: dict[str, Any] = {}
300
+ for name, rows in snapshot.rows.items():
301
+ original = self.data.get(name)
302
+ if isinstance(original, list):
303
+ rebuilt[name] = [
304
+ row["value"] if set(row) == {"value"} else dict(row)
305
+ for row in copy.deepcopy(rows)
306
+ ]
307
+ continue
308
+ keyed: dict[str, Any] = {}
309
+ for row in copy.deepcopy(rows):
310
+ identifier = row.pop(ID, None)
311
+ if identifier is None:
312
+ continue
313
+ keyed[identifier] = row.get("value") if set(row) == {"value"} else row
314
+ rebuilt[name] = keyed
315
+ # A group the snapshot does not mention is emptied, not carried over: restore has to be
316
+ # able to reproduce a snapshot that holds nothing, or the gate cannot empty the store to
317
+ # find out whether the checks actually bite. The key itself stays, with its original
318
+ # shape, because the agent's own code indexes into it and would not survive its absence.
319
+ for name, group in self.data.items():
320
+ if name not in rebuilt:
321
+ rebuilt[name] = [] if isinstance(group, list) else {}
322
+ self.data.clear()
323
+ self.data.update(rebuilt)
324
+
325
+ def save_to(self, path: str | Path) -> None:
326
+ if not self.data:
327
+ return
328
+ root = Path(path)
329
+ root.mkdir(parents=True, exist_ok=True)
330
+ (root / self.FILE).write_text(
331
+ json.dumps(self.data, indent=2, default=str), encoding="utf-8"
332
+ )
333
+
334
+ def load_from(self, path: str | Path) -> None:
335
+ held = Path(path) / self.FILE
336
+ if not held.exists():
337
+ return
338
+ self.data = json.loads(held.read_text(encoding="utf-8"))
339
+ self._started = True
340
+
341
+ def close(self) -> None:
342
+ return None
343
+
344
+
345
+ def _reads(row: Any, field: str) -> Any:
346
+ return row.get(field) if isinstance(row, dict) else None