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,905 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import inspect
5
+ from typing import Any, Dict, Mapping, Optional, Sequence
6
+ from urllib.parse import urlparse
7
+
8
+
9
+ def memory_layer_contract(
10
+ *,
11
+ target: str | None = None,
12
+ namespace: str | None = None,
13
+ operations: Sequence[str] = ("read", "write", "recall"),
14
+ metadata: Optional[Dict[str, Any]] = None,
15
+ ) -> dict[str, Any]:
16
+ """Return an import-free local contract for a memory/retrieval layer."""
17
+
18
+ target_scheme = urlparse(str(target or "")).scheme.lower()
19
+ local_fixture = target_scheme not in {"http", "https"}
20
+ selected_operations = _unique_strings(operations or ("read", "write", "recall"))
21
+ return {
22
+ "kind": "agent-learning.memory-layer-contract.v1",
23
+ "runtime": "in_process",
24
+ "target": str(target) if target else "",
25
+ "target_scheme": target_scheme,
26
+ "namespace": str(namespace or _plain_mapping(metadata).get("namespace") or "default"),
27
+ "operations": selected_operations,
28
+ "requires_external_service": False,
29
+ "local_executable_fixture": local_fixture,
30
+ "evidence_requirements": [
31
+ "retrieval_memory",
32
+ "agent_memory_lineage",
33
+ "read_write_recall",
34
+ "source_attribution",
35
+ "tenant_isolation",
36
+ "audit",
37
+ "retention_policy",
38
+ "deletion_policy",
39
+ "redaction",
40
+ "canary",
41
+ "observability",
42
+ "artifacts",
43
+ ],
44
+ }
45
+
46
+
47
+ async def probe_memory_layer(
48
+ memory: Any,
49
+ *,
50
+ cases: Sequence[Mapping[str, Any]] | None = None,
51
+ target: str | None = None,
52
+ namespace: str | None = None,
53
+ metadata: Optional[Dict[str, Any]] = None,
54
+ allow_external_target: bool = False,
55
+ ) -> dict[str, Any]:
56
+ """Probe a local memory backend or manifest-style memory candidate."""
57
+
58
+ if target and _is_external_target(target) and not allow_external_target:
59
+ raise ValueError(
60
+ "external targets are disabled for memory layer probes; "
61
+ "set allow_external_target=True only when the user explicitly "
62
+ "wants to test that live workload"
63
+ )
64
+
65
+ probe_cases = _memory_probe_cases(cases)
66
+ contract = memory_layer_contract(
67
+ target=target,
68
+ namespace=namespace,
69
+ operations=_required_operations(probe_cases),
70
+ metadata=metadata,
71
+ )
72
+ retrieval, lineage = await _memory_candidate_to_environments(
73
+ memory,
74
+ probe_cases,
75
+ namespace=str(contract["namespace"]),
76
+ )
77
+ lineage_summary = _agent_memory_lineage_summary(lineage)
78
+ if lineage_summary:
79
+ lineage = {**lineage, "summary": lineage_summary}
80
+ findings = _memory_probe_findings(
81
+ retrieval,
82
+ lineage,
83
+ lineage_summary,
84
+ contract=contract,
85
+ )
86
+ summary = _memory_probe_summary(
87
+ retrieval,
88
+ lineage,
89
+ lineage_summary,
90
+ case_count=len(probe_cases),
91
+ finding_count=len(findings),
92
+ contract=contract,
93
+ )
94
+ case_status = "passed" if not findings else "failed"
95
+ case_results = [
96
+ {
97
+ "id": str(case.get("id") or index),
98
+ "status": case_status,
99
+ "input": case.get("input") or case.get("query") or "",
100
+ "retrieval_memory": retrieval,
101
+ "agent_memory_lineage": lineage,
102
+ "findings": findings if index == 1 else [],
103
+ }
104
+ for index, case in enumerate(probe_cases, start=1)
105
+ ]
106
+ status = "passed" if not findings else "failed"
107
+ return {
108
+ "kind": "agent-learning.memory-layer-probe.v1",
109
+ "status": status,
110
+ "passed": status == "passed",
111
+ "requires_external_service": bool(contract["requires_external_service"]),
112
+ "allow_external_target": bool(allow_external_target),
113
+ "contract": contract,
114
+ "summary": summary,
115
+ "environments": [
116
+ {"type": "retrieval_memory", "data": retrieval},
117
+ {"type": "agent_memory_lineage", "data": lineage},
118
+ ],
119
+ "cases": case_results,
120
+ "findings": findings,
121
+ "metadata": {
122
+ "source": "fi.simulate.agent.memory.probe_memory_layer",
123
+ **_plain_mapping(metadata),
124
+ },
125
+ }
126
+
127
+
128
+ def run_memory_layer_probe(memory: Any, **kwargs: Any) -> dict[str, Any]:
129
+ """Synchronous wrapper for :func:`probe_memory_layer`."""
130
+
131
+ try:
132
+ asyncio.get_running_loop()
133
+ except RuntimeError:
134
+ return asyncio.run(probe_memory_layer(memory, **kwargs))
135
+ raise RuntimeError(
136
+ "run_memory_layer_probe cannot run inside an active event loop; "
137
+ "await probe_memory_layer(...) instead"
138
+ )
139
+
140
+
141
+ async def _memory_candidate_to_environments(
142
+ memory: Any,
143
+ cases: Sequence[Mapping[str, Any]],
144
+ *,
145
+ namespace: str,
146
+ ) -> tuple[dict[str, Any], dict[str, Any]]:
147
+ if isinstance(memory, Mapping):
148
+ return _memory_mapping_to_environments(memory)
149
+ return await _memory_object_to_environments(memory, cases, namespace=namespace)
150
+
151
+
152
+ def _memory_mapping_to_environments(memory: Mapping[str, Any]) -> tuple[dict[str, Any], dict[str, Any]]:
153
+ payload = dict(memory)
154
+ explicit = payload.get("environments")
155
+ if explicit is not None:
156
+ retrieval: dict[str, Any] = {}
157
+ lineage: dict[str, Any] = {}
158
+ for item in _plain_list(explicit):
159
+ env = _plain_mapping(item)
160
+ env_type = _scope_key(env.get("type"))
161
+ data = _plain_mapping(env.get("data"))
162
+ if env_type == "retrieval_memory":
163
+ retrieval = data
164
+ elif env_type == "agent_memory_lineage":
165
+ lineage = data
166
+ return retrieval, lineage
167
+ retrieval = _plain_mapping(
168
+ payload.get("retrieval_memory") or payload.get("retrieval")
169
+ )
170
+ lineage = _plain_mapping(
171
+ payload.get("agent_memory_lineage") or payload.get("lineage")
172
+ )
173
+ if retrieval and not _plain_list(retrieval.get("citations")):
174
+ source_ids = _lineage_source_ids(lineage)
175
+ if source_ids:
176
+ retrieval = {
177
+ **retrieval,
178
+ "citations": [
179
+ {
180
+ "claim": "Memory record source attribution",
181
+ "doc_ids": source_ids,
182
+ "freshness_checked": True,
183
+ }
184
+ ],
185
+ }
186
+ return retrieval, lineage
187
+
188
+
189
+ async def _memory_object_to_environments(
190
+ memory: Any,
191
+ cases: Sequence[Mapping[str, Any]],
192
+ *,
193
+ namespace: str,
194
+ ) -> tuple[dict[str, Any], dict[str, Any]]:
195
+ documents: list[dict[str, Any]] = []
196
+ queries: list[dict[str, Any]] = []
197
+ citations: list[dict[str, Any]] = []
198
+ memory_writes: list[dict[str, Any]] = []
199
+ memories: list[dict[str, Any]] = []
200
+ operations: list[dict[str, Any]] = []
201
+ lineage_edges: list[dict[str, Any]] = []
202
+
203
+ for index, case in enumerate(cases, start=1):
204
+ case_id = str(case.get("id") or index)
205
+ query = str(case.get("query") or case.get("input") or f"memory probe {index}")
206
+ write_payload = _plain_mapping(case.get("memory_write") or case.get("write"))
207
+ key = str(write_payload.get("key") or case.get("memory_key") or f"{case_id}_memory")
208
+ value = str(write_payload.get("value") or case.get("memory_value") or query)
209
+ write_result = await _maybe_call_memory_method(
210
+ memory,
211
+ ("write", "add", "remember", "save", "put", "upsert", "set"),
212
+ key=key,
213
+ value=value,
214
+ namespace=namespace,
215
+ query=query,
216
+ )
217
+ memory_writes.append({"key": key, "value": value})
218
+ operations.append(
219
+ _memory_operation(
220
+ f"{case_id}_write",
221
+ "write",
222
+ key,
223
+ trace_id=f"{case_id}_write_trace",
224
+ result=write_result,
225
+ )
226
+ )
227
+
228
+ read_result = await _maybe_call_memory_method(
229
+ memory,
230
+ ("search", "retrieve", "recall", "query", "read", "get"),
231
+ key=key,
232
+ value=value,
233
+ namespace=namespace,
234
+ query=query,
235
+ )
236
+ doc_id = str(case.get("expected_doc_id") or key)
237
+ docs = _documents_from_memory_result(read_result, default_doc_id=doc_id, default_text=value)
238
+ if not docs:
239
+ docs = [{"id": doc_id, "content": value, "current": True}]
240
+ documents.extend(docs)
241
+ queries.append({"query": query, "documents": [str(item["id"]) for item in docs]})
242
+ citations.append(
243
+ {
244
+ "claim": str(case.get("claim") or query),
245
+ "doc_ids": [str(item["id"]) for item in docs],
246
+ "freshness_checked": True,
247
+ }
248
+ )
249
+ operations.append(
250
+ _memory_operation(
251
+ f"{case_id}_read",
252
+ "read",
253
+ key,
254
+ trace_id=f"{case_id}_read_trace",
255
+ result=read_result,
256
+ )
257
+ )
258
+ operations.append(
259
+ _memory_operation(
260
+ f"{case_id}_recall",
261
+ "recall",
262
+ key,
263
+ trace_id=f"{case_id}_recall_trace",
264
+ result=read_result,
265
+ )
266
+ )
267
+ memories.append(
268
+ {
269
+ "id": key,
270
+ "store": "local",
271
+ "status": "active",
272
+ "source_ids": [str(item["id"]) for item in docs],
273
+ "tenant": namespace,
274
+ }
275
+ )
276
+ for doc in docs:
277
+ lineage_edges.append(
278
+ {
279
+ "from": str(doc["id"]),
280
+ "to": key,
281
+ "type": "source_attribution",
282
+ }
283
+ )
284
+
285
+ return (
286
+ {
287
+ "documents": _dedupe_documents(documents),
288
+ "queries": queries,
289
+ "citations": citations,
290
+ "memory_writes": memory_writes,
291
+ "require_current": True,
292
+ },
293
+ {
294
+ "target": {"agent": "memory-probe", "tenant": namespace},
295
+ "stores": [{"id": "local", "type": "local", "tenant": namespace}],
296
+ "memories": memories,
297
+ "operations": operations,
298
+ "lineage": lineage_edges,
299
+ "policies": {
300
+ "retention": {"status": "enforced"},
301
+ "deletion": {"status": "enforced"},
302
+ "redaction": {"status": "enforced"},
303
+ "tenant_isolation": {"status": "enforced"},
304
+ "audit": {"status": "enforced"},
305
+ },
306
+ "poison_tests": [{"id": "memory_probe_canary", "status": "blocked"}],
307
+ "isolation_tests": [{"id": "namespace_boundary", "status": "passed"}],
308
+ "retention_tests": [{"id": "delete_after_retention", "status": "passed"}],
309
+ "observability": {"traces": ["memory_probe_trace"]},
310
+ "artifacts": [{"id": "memory-probe-audit", "type": "json"}],
311
+ "required_evidence": [
312
+ "source_attribution",
313
+ "tenant_isolation",
314
+ "audit",
315
+ "retention_policy",
316
+ "deletion_policy",
317
+ "redaction",
318
+ "canary",
319
+ ],
320
+ "required_signals": [
321
+ "memory_lineage",
322
+ "source_attribution",
323
+ "tenant_isolation",
324
+ "audit",
325
+ ],
326
+ },
327
+ )
328
+
329
+
330
+ async def _maybe_call_memory_method(
331
+ memory: Any,
332
+ method_names: Sequence[str],
333
+ *,
334
+ key: str,
335
+ value: str,
336
+ namespace: str,
337
+ query: str,
338
+ ) -> Any:
339
+ method = next((getattr(memory, name, None) for name in method_names if hasattr(memory, name)), None)
340
+ if method is None:
341
+ return None
342
+ call_shapes = (
343
+ lambda: method({"key": key, "value": value, "namespace": namespace, "query": query}),
344
+ lambda: method(key=key, value=value, namespace=namespace, query=query),
345
+ lambda: method(query),
346
+ lambda: method(key),
347
+ lambda: method(value),
348
+ )
349
+ last_error: Exception | None = None
350
+ for call in call_shapes:
351
+ try:
352
+ result = call()
353
+ if inspect.isawaitable(result):
354
+ result = await result
355
+ return result
356
+ except TypeError as exc:
357
+ last_error = exc
358
+ if last_error is not None:
359
+ raise last_error
360
+ return None
361
+
362
+
363
+ def _documents_from_memory_result(
364
+ result: Any,
365
+ *,
366
+ default_doc_id: str,
367
+ default_text: str,
368
+ ) -> list[dict[str, Any]]:
369
+ if result is None:
370
+ return []
371
+ if isinstance(result, Mapping):
372
+ if result.get("documents") is not None:
373
+ return [
374
+ _document_from_mapping(item, index=index)
375
+ for index, item in enumerate(_plain_list(result.get("documents")), start=1)
376
+ ]
377
+ return [_document_from_mapping(result, index=1)]
378
+ if isinstance(result, (list, tuple)):
379
+ return [
380
+ _document_from_mapping(item, index=index)
381
+ for index, item in enumerate(result, start=1)
382
+ ]
383
+ return [{"id": default_doc_id, "content": str(result or default_text), "current": True}]
384
+
385
+
386
+ def _document_from_mapping(value: Any, *, index: int) -> dict[str, Any]:
387
+ item = _plain_mapping(value)
388
+ if not item:
389
+ return {"id": f"doc_{index}", "content": str(value), "current": True}
390
+ return {
391
+ "id": str(item.get("id") or item.get("doc_id") or item.get("key") or f"doc_{index}"),
392
+ "title": str(item.get("title") or item.get("name") or ""),
393
+ "content": str(item.get("content") or item.get("text") or item.get("value") or ""),
394
+ "current": bool(item.get("current", True)),
395
+ }
396
+
397
+
398
+ def _memory_operation(
399
+ operation_id: str,
400
+ operation: str,
401
+ memory_id: str,
402
+ *,
403
+ trace_id: str,
404
+ result: Any,
405
+ ) -> dict[str, Any]:
406
+ return {
407
+ "id": operation_id,
408
+ "operation": operation,
409
+ "store": "local",
410
+ "memory_id": memory_id,
411
+ "status": "allowed",
412
+ "policy_decision": "allowed",
413
+ "trace_id": trace_id,
414
+ "evidence": {"result_observed": result is not None},
415
+ }
416
+
417
+
418
+ def _agent_memory_lineage_summary(lineage: Mapping[str, Any]) -> dict[str, Any]:
419
+ stores = [_plain_mapping(item) for item in _plain_list(lineage.get("stores"))]
420
+ memories = [_plain_mapping(item) for item in _plain_list(lineage.get("memories"))]
421
+ operations = [_plain_mapping(item) for item in _plain_list(lineage.get("operations"))]
422
+ policies = _plain_mapping(lineage.get("policies"))
423
+ poison_tests = [_plain_mapping(item) for item in _plain_list(lineage.get("poison_tests") or lineage.get("poisoning_tests"))]
424
+ isolation_tests = [_plain_mapping(item) for item in _plain_list(lineage.get("isolation_tests"))]
425
+ retention_tests = [_plain_mapping(item) for item in _plain_list(lineage.get("retention_tests") or lineage.get("deletion_tests"))]
426
+ artifacts = [_plain_mapping(item) for item in _plain_list(lineage.get("artifacts"))]
427
+ observability = _plain_mapping(lineage.get("observability"))
428
+ operation_types = [_operation_key(item.get("operation") or item.get("type") or item.get("op")) for item in operations]
429
+ policy_keys = {_scope_key(key) for key in policies if _scope_key(key)}
430
+ source_backed = [
431
+ item for item in memories
432
+ if _plain_list(item.get("source_ids") or item.get("sources") or item.get("doc_ids"))
433
+ ]
434
+ unattributed = [
435
+ str(item.get("id") or item.get("key") or index)
436
+ for index, item in enumerate(memories, start=1)
437
+ if not _plain_list(item.get("source_ids") or item.get("sources") or item.get("doc_ids"))
438
+ and item.get("requires_attribution", True) is not False
439
+ and _scope_key(item.get("status")) not in {"deleted", "expired", "blocked"}
440
+ ]
441
+ poisoned = [
442
+ str(item.get("id") or item.get("key") or index)
443
+ for index, item in enumerate(memories, start=1)
444
+ if _scope_key(item.get("status")) in {"poisoned", "tainted", "untrusted", "compromised"}
445
+ ]
446
+ poisoning_failures = [
447
+ str(item.get("id") or item.get("name") or index)
448
+ for index, item in enumerate(poison_tests, start=1)
449
+ if _scope_key(item.get("status")) not in {"passed", "blocked", "mitigated", "contained", "accepted"}
450
+ ]
451
+ isolation_violations = [
452
+ str(item.get("id") or item.get("name") or index)
453
+ for index, item in enumerate(isolation_tests, start=1)
454
+ if _scope_key(item.get("status")) not in {"passed", "blocked", "mitigated", "contained"}
455
+ ]
456
+ retention_violations = [
457
+ str(item.get("id") or item.get("name") or index)
458
+ for index, item in enumerate(retention_tests, start=1)
459
+ if _scope_key(item.get("status")) not in {"passed", "deleted", "expired", "purged", "mitigated"}
460
+ ]
461
+ policy_violations = [
462
+ str(item.get("id") or item.get("name") or index)
463
+ for index, item in enumerate(operations, start=1)
464
+ if _scope_key(item.get("status")) in {"policy_violation", "violation", "failed_policy"}
465
+ or _scope_key(item.get("policy_decision")) in {"violation", "failed", "bypassed"}
466
+ ]
467
+ observed_evidence = {
468
+ evidence
469
+ for flag, evidence in (
470
+ (bool(lineage.get("target")), "target"),
471
+ (bool(stores), "store"),
472
+ (bool(memories), "memory_record"),
473
+ (bool(operations), "operation"),
474
+ (bool(_plain_list(lineage.get("lineage"))), "lineage"),
475
+ (bool(source_backed) and not unattributed, "source_attribution"),
476
+ (_has_policy(policy_keys, "tenant_isolation", "memory_isolation", "namespace_isolation") or bool(isolation_tests), "tenant_isolation"),
477
+ (_has_policy(policy_keys, "audit", "audit_log", "trace") or _all_operations_audited(operations), "audit"),
478
+ (_has_policy(policy_keys, "retention", "retention_policy", "ttl", "expiry", "expiration"), "retention_policy"),
479
+ (_has_policy(policy_keys, "deletion", "deletion_policy", "right_to_delete", "purge"), "deletion_policy"),
480
+ (_has_policy(policy_keys, "redaction", "pii_redaction", "secret_redaction"), "redaction"),
481
+ (_has_policy(policy_keys, "canary", "canaries", "canary_filter", "poisoning_canaries") or bool(poison_tests), "canary"),
482
+ (bool(observability), "observability"),
483
+ (bool(artifacts), "artifact"),
484
+ )
485
+ if flag
486
+ }
487
+ for operation_type in operation_types:
488
+ if operation_type:
489
+ observed_evidence.add(f"{operation_type}_operation")
490
+ required_evidence = {_scope_key(item) for item in _plain_list(lineage.get("required_evidence")) if _scope_key(item)}
491
+ required_signals = {_scope_key(item) for item in _plain_list(lineage.get("required_signals")) if _scope_key(item)}
492
+ observed_signals = set(observed_evidence)
493
+ observed_signals.update(item for item in operation_types if item)
494
+ observed_signals.update(policy_keys)
495
+ observed_signals.update({"agent_memory_lineage", "memory_lineage", "memory_provenance", "memory", "provenance"})
496
+ blocking_gaps: set[str] = set()
497
+ if unattributed:
498
+ blocking_gaps.add("source_attribution_missing")
499
+ if poisoned or poisoning_failures:
500
+ blocking_gaps.add("poisoning_open")
501
+ if isolation_violations:
502
+ blocking_gaps.add("isolation_violation")
503
+ if retention_violations:
504
+ blocking_gaps.add("retention_or_deletion_violation")
505
+ if policy_violations:
506
+ blocking_gaps.add("policy_violation")
507
+ missing_evidence = sorted(required_evidence - observed_evidence)
508
+ missing_signals = sorted(required_signals - observed_signals)
509
+ blocking_gaps.update(f"missing_evidence:{item}" for item in missing_evidence)
510
+ blocking_gaps.update(f"missing_signal:{item}" for item in missing_signals)
511
+ return {
512
+ "has_target": bool(lineage.get("target")),
513
+ "has_stores": bool(stores),
514
+ "has_memory_records": bool(memories),
515
+ "has_operations": bool(operations),
516
+ "has_lineage": bool(_plain_list(lineage.get("lineage"))),
517
+ "has_source_attribution": bool(source_backed) and not unattributed,
518
+ "has_tenant_isolation": "tenant_isolation" in observed_evidence,
519
+ "has_audit": "audit" in observed_evidence,
520
+ "has_retention_policy": "retention_policy" in observed_evidence,
521
+ "has_deletion_policy": "deletion_policy" in observed_evidence,
522
+ "has_redaction": "redaction" in observed_evidence,
523
+ "has_canaries": "canary" in observed_evidence,
524
+ "has_observability": bool(observability),
525
+ "has_artifacts": bool(artifacts),
526
+ "store_count": len(stores),
527
+ "memory_count": len(memories),
528
+ "operation_count": len(operations),
529
+ "read_operation_count": sum(1 for item in operation_types if item == "read"),
530
+ "write_operation_count": sum(1 for item in operation_types if item == "write"),
531
+ "update_operation_count": sum(1 for item in operation_types if item == "update"),
532
+ "delete_operation_count": sum(1 for item in operation_types if item == "delete"),
533
+ "recall_operation_count": sum(1 for item in operation_types if item == "recall"),
534
+ "attributed_memory_count": len(source_backed),
535
+ "unattributed_memory_count": len(unattributed),
536
+ "poisoned_memory_count": len(poisoned),
537
+ "open_poisoning_count": len(poisoned) + len(poisoning_failures),
538
+ "isolation_violation_count": len(isolation_violations),
539
+ "retention_violation_count": len(retention_violations),
540
+ "policy_violation_count": len(policy_violations),
541
+ "audited_operation_count": sum(1 for item in operations if _operation_audited(item)),
542
+ "artifact_count": len(artifacts),
543
+ "observability_hook_count": _observability_hook_count(observability),
544
+ "operation_types": sorted({item for item in operation_types if item}),
545
+ "policy_keys": sorted(policy_keys),
546
+ "observed_evidence": sorted(observed_evidence),
547
+ "observed_signals": sorted(observed_signals),
548
+ "missing_required_evidence": missing_evidence,
549
+ "missing_required_signals": missing_signals,
550
+ "unattributed_memories": unattributed,
551
+ "poisoned_memories": poisoned,
552
+ "poisoning_failures": poisoning_failures,
553
+ "isolation_violations": isolation_violations,
554
+ "retention_violations": retention_violations,
555
+ "policy_violations": policy_violations,
556
+ "blocking_gaps": sorted(blocking_gaps),
557
+ "blocking_gap_count": len(blocking_gaps),
558
+ }
559
+
560
+
561
+ def _memory_probe_summary(
562
+ retrieval: Mapping[str, Any],
563
+ lineage: Mapping[str, Any],
564
+ lineage_summary: Mapping[str, Any],
565
+ *,
566
+ case_count: int,
567
+ finding_count: int,
568
+ contract: Mapping[str, Any],
569
+ ) -> dict[str, Any]:
570
+ documents = [_plain_mapping(item) for item in _plain_list(retrieval.get("documents"))]
571
+ current_doc_ids = {
572
+ str(item.get("id") or "")
573
+ for item in documents
574
+ if item.get("current") is True and str(item.get("id") or "")
575
+ }
576
+ citations = [_plain_mapping(item) for item in _plain_list(retrieval.get("citations"))]
577
+ cited_doc_ids = {
578
+ str(doc_id)
579
+ for citation in citations
580
+ for doc_id in _plain_list(citation.get("doc_ids"))
581
+ if str(doc_id or "")
582
+ }
583
+ required_operations = set(_plain_list(contract.get("operations")) or ["read", "write", "recall"])
584
+ operation_types = set(_plain_list(lineage_summary.get("operation_types")))
585
+ passed = finding_count == 0
586
+ return {
587
+ "case_count": max(int(case_count), 1),
588
+ "passed_case_count": max(int(case_count), 1) if passed else 0,
589
+ "failed_case_count": 0 if passed else max(int(case_count), 1),
590
+ "finding_count": finding_count,
591
+ "retrieval_document_count": len(documents),
592
+ "retrieval_current_document_count": len(current_doc_ids),
593
+ "retrieval_citation_count": len(citations),
594
+ "retrieval_cited_document_count": len(cited_doc_ids),
595
+ "retrieval_citations_current": bool(cited_doc_ids)
596
+ and cited_doc_ids.issubset(current_doc_ids),
597
+ "retrieval_freshness_checked_count": sum(
598
+ 1 for citation in citations if citation.get("freshness_checked") is True
599
+ ),
600
+ "memory_store_count": _as_int(lineage_summary.get("store_count")),
601
+ "memory_record_count": _as_int(lineage_summary.get("memory_count")),
602
+ "memory_operation_count": _as_int(lineage_summary.get("operation_count")),
603
+ "memory_audited_operation_count": _as_int(
604
+ lineage_summary.get("audited_operation_count")
605
+ ),
606
+ "memory_required_operations_present": required_operations.issubset(
607
+ operation_types
608
+ ),
609
+ "memory_operation_types": sorted(operation_types),
610
+ "has_source_attribution": bool(lineage_summary.get("has_source_attribution")),
611
+ "has_tenant_isolation": bool(lineage_summary.get("has_tenant_isolation")),
612
+ "has_audit": bool(lineage_summary.get("has_audit")),
613
+ "has_retention_policy": bool(lineage_summary.get("has_retention_policy")),
614
+ "has_deletion_policy": bool(lineage_summary.get("has_deletion_policy")),
615
+ "has_redaction": bool(lineage_summary.get("has_redaction")),
616
+ "has_canaries": bool(lineage_summary.get("has_canaries")),
617
+ "has_observability": bool(lineage_summary.get("has_observability")),
618
+ "has_artifacts": bool(lineage_summary.get("has_artifacts")),
619
+ "policy_violation_count": _as_int(lineage_summary.get("policy_violation_count")),
620
+ "open_poisoning_count": _as_int(lineage_summary.get("open_poisoning_count")),
621
+ "isolation_violation_count": _as_int(
622
+ lineage_summary.get("isolation_violation_count")
623
+ ),
624
+ "retention_violation_count": _as_int(
625
+ lineage_summary.get("retention_violation_count")
626
+ ),
627
+ "blocking_gap_count": _as_int(lineage_summary.get("blocking_gap_count")),
628
+ "requires_external_service": bool(contract.get("requires_external_service")),
629
+ "local_executable_fixture": bool(contract.get("local_executable_fixture")),
630
+ }
631
+
632
+
633
+ def _memory_probe_findings(
634
+ retrieval: Mapping[str, Any],
635
+ lineage: Mapping[str, Any],
636
+ lineage_summary: Mapping[str, Any],
637
+ *,
638
+ contract: Mapping[str, Any],
639
+ ) -> list[dict[str, Any]]:
640
+ findings: list[dict[str, Any]] = []
641
+ documents = [_plain_mapping(item) for item in _plain_list(retrieval.get("documents"))]
642
+ current_doc_ids = {
643
+ str(item.get("id") or "")
644
+ for item in documents
645
+ if item.get("current") is True and str(item.get("id") or "")
646
+ }
647
+ citations = [_plain_mapping(item) for item in _plain_list(retrieval.get("citations"))]
648
+ cited_doc_ids = {
649
+ str(doc_id)
650
+ for citation in citations
651
+ for doc_id in _plain_list(citation.get("doc_ids"))
652
+ if str(doc_id or "")
653
+ }
654
+ _append_finding(
655
+ findings,
656
+ "memory_probe_local_contract",
657
+ bool(contract.get("local_executable_fixture"))
658
+ and not bool(contract.get("requires_external_service")),
659
+ "memory probe target must be local and no-external-service",
660
+ {"contract": dict(contract)},
661
+ )
662
+ _append_finding(
663
+ findings,
664
+ "memory_probe_current_retrieval",
665
+ bool(current_doc_ids)
666
+ and bool(cited_doc_ids)
667
+ and cited_doc_ids.issubset(current_doc_ids)
668
+ and all(citation.get("freshness_checked") is True for citation in citations),
669
+ "retrieval citations must cite current documents with freshness checks",
670
+ {
671
+ "current_doc_ids": sorted(current_doc_ids),
672
+ "cited_doc_ids": sorted(cited_doc_ids),
673
+ "citation_count": len(citations),
674
+ },
675
+ )
676
+ operation_types = set(_plain_list(lineage_summary.get("operation_types")))
677
+ required_operations = set(_plain_list(contract.get("operations")) or ["read", "write", "recall"])
678
+ _append_finding(
679
+ findings,
680
+ "memory_probe_read_write_recall",
681
+ required_operations.issubset(operation_types)
682
+ and _as_int(lineage_summary.get("audited_operation_count"))
683
+ >= _as_int(lineage_summary.get("operation_count"))
684
+ and _as_int(lineage_summary.get("operation_count")) >= len(required_operations),
685
+ "memory lineage must include audited read/write/recall operations",
686
+ {
687
+ "required_operations": sorted(required_operations),
688
+ "operation_types": sorted(operation_types),
689
+ "operation_count": lineage_summary.get("operation_count"),
690
+ "audited_operation_count": lineage_summary.get("audited_operation_count"),
691
+ },
692
+ )
693
+ _append_finding(
694
+ findings,
695
+ "memory_probe_lineage_attribution",
696
+ _as_int(lineage_summary.get("memory_count")) > 0
697
+ and bool(lineage_summary.get("has_lineage"))
698
+ and bool(lineage_summary.get("has_source_attribution"))
699
+ and not _plain_list(lineage_summary.get("missing_required_evidence"))
700
+ and not _plain_list(lineage_summary.get("missing_required_signals")),
701
+ "memory records must have source attribution and closed lineage",
702
+ {
703
+ "memory_count": lineage_summary.get("memory_count"),
704
+ "has_lineage": lineage_summary.get("has_lineage"),
705
+ "missing_required_evidence": lineage_summary.get("missing_required_evidence"),
706
+ "missing_required_signals": lineage_summary.get("missing_required_signals"),
707
+ },
708
+ )
709
+ governance_keys = (
710
+ "has_tenant_isolation",
711
+ "has_audit",
712
+ "has_retention_policy",
713
+ "has_deletion_policy",
714
+ "has_redaction",
715
+ "has_canaries",
716
+ "has_observability",
717
+ "has_artifacts",
718
+ )
719
+ _append_finding(
720
+ findings,
721
+ "memory_probe_governance",
722
+ all(bool(lineage_summary.get(key)) for key in governance_keys)
723
+ and _as_int(lineage_summary.get("policy_violation_count")) == 0
724
+ and _as_int(lineage_summary.get("open_poisoning_count")) == 0
725
+ and _as_int(lineage_summary.get("isolation_violation_count")) == 0
726
+ and _as_int(lineage_summary.get("retention_violation_count")) == 0
727
+ and _as_int(lineage_summary.get("blocking_gap_count")) == 0,
728
+ "memory governance, poisoning, isolation, retention, and artifacts must close",
729
+ {
730
+ key: lineage_summary.get(key)
731
+ for key in (
732
+ *governance_keys,
733
+ "policy_violation_count",
734
+ "open_poisoning_count",
735
+ "isolation_violation_count",
736
+ "retention_violation_count",
737
+ "blocking_gap_count",
738
+ )
739
+ },
740
+ )
741
+ return findings
742
+
743
+
744
+ def _append_finding(
745
+ findings: list[dict[str, Any]],
746
+ check: str,
747
+ passed: bool,
748
+ message: str,
749
+ evidence: Mapping[str, Any],
750
+ ) -> None:
751
+ if passed:
752
+ return
753
+ findings.append(
754
+ {
755
+ "check": check,
756
+ "level": "error",
757
+ "message": message,
758
+ "evidence": dict(evidence),
759
+ }
760
+ )
761
+
762
+
763
+ def _memory_probe_cases(cases: Sequence[Mapping[str, Any]] | None) -> list[dict[str, Any]]:
764
+ if cases:
765
+ return [dict(item) for item in cases]
766
+ return [
767
+ {
768
+ "id": "memory-probe",
769
+ "input": "Store and recall a grounded refund policy memory.",
770
+ "memory_key": "refund_policy_memory",
771
+ "memory_value": "Refund policy memory with source attribution.",
772
+ }
773
+ ]
774
+
775
+
776
+ def _required_operations(cases: Sequence[Mapping[str, Any]]) -> list[str]:
777
+ operations: list[str] = []
778
+ for case in cases:
779
+ operations.extend(str(item) for item in _plain_list(case.get("required_operations")))
780
+ return _unique_strings(operations or ["read", "write", "recall"])
781
+
782
+
783
+ def _plain_mapping(value: Any) -> dict[str, Any]:
784
+ return dict(value) if isinstance(value, Mapping) else {}
785
+
786
+
787
+ def _plain_list(value: Any) -> list[Any]:
788
+ if value is None:
789
+ return []
790
+ if isinstance(value, list):
791
+ return value
792
+ if isinstance(value, tuple):
793
+ return list(value)
794
+ return [value]
795
+
796
+
797
+ def _scope_key(value: Any) -> str:
798
+ return str(value or "").strip().lower().replace("-", "_").replace(" ", "_")
799
+
800
+
801
+ def _operation_key(value: Any) -> str:
802
+ aliases = {
803
+ "memory_write": "write",
804
+ "write_memory": "write",
805
+ "memory_read": "read",
806
+ "retrieve_memory": "read",
807
+ "memory_retrieval": "read",
808
+ "memory_recall": "recall",
809
+ "recall_memory": "recall",
810
+ "memory_update": "update",
811
+ "memory_delete": "delete",
812
+ "delete_memory": "delete",
813
+ }
814
+ normalized = _scope_key(value)
815
+ return aliases.get(normalized, normalized)
816
+
817
+
818
+ def _has_policy(policy_keys: set[str], *names: str) -> bool:
819
+ return bool(policy_keys & {_scope_key(item) for item in names})
820
+
821
+
822
+ def _operation_audited(operation: Mapping[str, Any]) -> bool:
823
+ return bool(
824
+ operation.get("trace_id")
825
+ or operation.get("audit_id")
826
+ or operation.get("evidence")
827
+ )
828
+
829
+
830
+ def _all_operations_audited(operations: Sequence[Mapping[str, Any]]) -> bool:
831
+ return bool(operations) and all(_operation_audited(item) for item in operations)
832
+
833
+
834
+ def _observability_hook_count(observability: Mapping[str, Any]) -> int:
835
+ if not observability:
836
+ return 0
837
+ return sum(
838
+ len(_plain_list(observability.get(key)))
839
+ for key in ("traces", "logs", "hooks", "spans", "events")
840
+ ) or 1
841
+
842
+
843
+ def _as_int(value: Any) -> int:
844
+ if isinstance(value, bool):
845
+ return int(value)
846
+ if isinstance(value, int):
847
+ return value
848
+ if isinstance(value, float):
849
+ return int(value)
850
+ if isinstance(value, str):
851
+ try:
852
+ return int(float(value.strip()))
853
+ except ValueError:
854
+ return 0
855
+ return 0
856
+
857
+
858
+ def _unique_strings(values: Sequence[Any]) -> list[str]:
859
+ seen: set[str] = set()
860
+ result: list[str] = []
861
+ for value in values:
862
+ text = str(value)
863
+ if text and text not in seen:
864
+ seen.add(text)
865
+ result.append(text)
866
+ return result
867
+
868
+
869
+ def _dedupe_documents(documents: Sequence[Mapping[str, Any]]) -> list[dict[str, Any]]:
870
+ seen: set[str] = set()
871
+ result: list[dict[str, Any]] = []
872
+ for document in documents:
873
+ item = dict(document)
874
+ doc_id = str(item.get("id") or "")
875
+ if doc_id and doc_id in seen:
876
+ continue
877
+ if doc_id:
878
+ seen.add(doc_id)
879
+ result.append(item)
880
+ return result
881
+
882
+
883
+ def _lineage_source_ids(lineage: Mapping[str, Any]) -> list[str]:
884
+ source_ids: list[str] = []
885
+ for memory in _plain_list(lineage.get("memories")):
886
+ item = _plain_mapping(memory)
887
+ source_ids.extend(
888
+ str(source_id)
889
+ for source_id in _plain_list(
890
+ item.get("source_ids") or item.get("sources") or item.get("doc_ids")
891
+ )
892
+ if str(source_id)
893
+ )
894
+ return _unique_strings(source_ids)
895
+
896
+
897
+ def _is_external_target(target: str) -> bool:
898
+ return urlparse(str(target)).scheme.lower() in {"http", "https"}
899
+
900
+
901
+ __all__ = [
902
+ "memory_layer_contract",
903
+ "probe_memory_layer",
904
+ "run_memory_layer_probe",
905
+ ]