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,2075 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import logging
5
+ import math
6
+ from dataclasses import dataclass, field, replace
7
+ from itertools import combinations
8
+ from typing import Any, Callable, Iterable, List, Mapping, Optional, Sequence
9
+
10
+ from ..base.base_optimizer import BaseOptimizer
11
+ from ..components import ComponentDiagnosis, relevant_search_paths
12
+ from ..targets import AgentCandidate, CandidateEvaluation, OptimizationTarget
13
+ from ..types import EvaluationResult, IterationHistory, OptimizationResult
14
+ from .agent import (
15
+ _dedupe_diagnoses,
16
+ _diagnose_candidate_evaluation,
17
+ _dump_model,
18
+ _history_from_candidate,
19
+ _normalize_candidate_evaluation,
20
+ _normalize_diagnoses,
21
+ )
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ DEFAULT_COUNCIL_ROLES = ("explorer", "critic", "synthesizer", "steward")
27
+ SOCIETY_ROLES = (
28
+ "explorer",
29
+ "critic",
30
+ "synthesizer",
31
+ "steward",
32
+ "specialist",
33
+ "adversary",
34
+ )
35
+
36
+ # Phase 4 society vocabulary (scholarly design devices used as deterministic
37
+ # engineering metadata — psychometric/philological grounding only, zero
38
+ # doctrinal claims).
39
+
40
+ GUNA_AXES = ("rajas", "sattva", "tamas")
41
+
42
+ GUNA_ARCHETYPE_DEFAULTS: dict[str, tuple[float, float, float]] = {
43
+ # (rajas, sattva, tamas) — dominant axis per the Phase-4 architecture
44
+ # archetype-default table (canon home; values stay byte-identical).
45
+ "focused_action": (0.8, 0.4, 0.2), # arjuna — explorer
46
+ "prudent_critic": (0.7, 0.5, 0.4), # vidura — adversary
47
+ "orchestrator": (0.5, 0.6, 0.4), # sutradhara
48
+ "working_memory": (0.4, 0.6, 0.5), # smriti
49
+ "bridge_builder": (0.6, 0.5, 0.3), # hanuman
50
+ "charioteer_counsel": (0.3, 0.8, 0.4), # krishna — critic
51
+ "collective_synthesis": (0.2, 0.9, 0.3), # sangha — synthesizer
52
+ "minimal_process_guardian": (0.1, 0.5, 0.9),# dharma_steward — steward
53
+ "": (0.5, 0.5, 0.5),
54
+ }
55
+
56
+ CHAMBER_TOKENS = ("samiti", "sabha")
57
+
58
+ # Chambers are ORTHOGONAL to phases/stages: within every phase samiti roles
59
+ # generate widely and sabha roles deliberate/promote — chamber derives from
60
+ # role kind, never from phase.
61
+ SAMITI_PROPOSAL_KINDS = frozenset({"specialist", "explorer", "adversary"})
62
+ SABHA_PROPOSAL_KINDS = frozenset(
63
+ {"critic", "synthesizer", "coverage_synthesis", "steward"}
64
+ )
65
+
66
+ PANCA_AVAYAVA_MEMBERS = (
67
+ # Five-member (panca-avayava) proposal justification — Nyaya-Sutra syllogism
68
+ # structure used as an auditable record schema (Pramana arXiv:2604.04937
69
+ # operationalization precedent). Scholarly design device, not a doctrinal
70
+ # claim.
71
+ "pratijna", # claim: what this patch asserts will improve
72
+ "hetu", # reason: the diagnosis/metric evidence relied on
73
+ "udaharana", # rule + example: the prior candidate/row exhibiting the rule
74
+ "upanaya", # application: why the rule covers THIS candidate
75
+ "nigamana", # conclusion: the expected admissible evidence delta
76
+ )
77
+
78
+ HETVABHASA_REJECTION_CLASSES = (
79
+ "savyabhichara", # inconclusive reason: evidence does not discriminate candidates
80
+ "viruddha", # contradictory reason: evidence contradicts the claim
81
+ "satpratipaksha", # counterbalanced: an equal counter-justification exists
82
+ "asiddha", # unestablished reason: cited evidence/row not found in lineage
83
+ "badhita", # defeated: claim contradicted by a stronger admissible check
84
+ )
85
+
86
+ CRITIQUE_OPERATOR_CLASSES = (
87
+ "vada", # truth-seeking review — critic
88
+ "jalpa", # adversarial stress — adversary; findings admissible only via evidence
89
+ "vitanda", # refutation-only veto pass — may reject, never proposes
90
+ )
91
+
92
+
93
+ @dataclass(frozen=True)
94
+ class AgentSearchProposal:
95
+ """One deterministic candidate patch proposed by an agent-search strategy."""
96
+
97
+ patch: dict[str, Any]
98
+ role: str
99
+ parent_ids: tuple[str, ...]
100
+ reason: str
101
+ metadata: Mapping[str, Any] = field(default_factory=dict)
102
+
103
+
104
+ @dataclass(frozen=True)
105
+ class AgentSearchState:
106
+ """Read-only state passed to a pluggable agent-search strategy."""
107
+
108
+ seed_candidate: AgentCandidate
109
+ evaluations: Sequence[CandidateEvaluation]
110
+ search_space: Mapping[str, List[Any]]
111
+ search_paths: Sequence[str]
112
+ diagnoses: Sequence[ComponentDiagnosis]
113
+ beam_width: int
114
+ max_proposals: int
115
+ round_number: int
116
+ # Phase 4: round-scoped pooled-diagnosis society ledger (GEA experience
117
+ # pooling). None = legacy behavior.
118
+ ledger: Optional[Mapping[str, Any]] = None
119
+
120
+
121
+ @dataclass(frozen=True)
122
+ class AgentSocietyRole:
123
+ """One role node in a deterministic society-search proposal graph."""
124
+
125
+ name: str
126
+ proposal_kind: str
127
+ phase: int = 1
128
+ depends_on: tuple[str, ...] = ()
129
+ path_prefixes: tuple[str, ...] = ()
130
+ archetype: str = ""
131
+ description: str = ""
132
+ # Phase 4 (placed after the legacy fields so positional construction stays
133
+ # byte-compatible): ONE nested optional guna mapping — {"rajas", "sattva",
134
+ # "tamas"} each in [0, 1]; None/absent = derive from archetype defaults —
135
+ # and an optional chamber ("samiti" | "sabha"); None = derive from role
136
+ # kind. No sentinel values.
137
+ guna: Optional[Mapping[str, float]] = None
138
+ chamber: Optional[str] = None
139
+
140
+ def to_metadata(self) -> dict[str, Any]:
141
+ return {
142
+ "name": self.name,
143
+ "proposal_kind": self.proposal_kind,
144
+ "phase": self.phase,
145
+ "depends_on": list(self.depends_on),
146
+ "path_prefixes": list(self.path_prefixes),
147
+ "archetype": self.archetype,
148
+ "description": self.description,
149
+ "guna": dict(self.guna) if self.guna is not None else None,
150
+ "chamber": self.chamber,
151
+ }
152
+
153
+
154
+ ROLE_GRAPH_PROPOSAL_KINDS = {
155
+ "adversary",
156
+ "coverage_synthesis",
157
+ "critic",
158
+ "explorer",
159
+ "specialist",
160
+ "steward",
161
+ "synthesizer",
162
+ }
163
+
164
+
165
+ DEFAULT_SOCIETY_ROLE_GRAPH = (
166
+ AgentSocietyRole(
167
+ name="sutradhara",
168
+ proposal_kind="specialist",
169
+ phase=1,
170
+ path_prefixes=("multi_agent", "orchestration", "router", "graph"),
171
+ archetype="orchestrator",
172
+ description="Bundle coordination, routing, handoff, and graph repairs.",
173
+ ),
174
+ AgentSocietyRole(
175
+ name="smriti",
176
+ proposal_kind="specialist",
177
+ phase=1,
178
+ path_prefixes=("memory", "retrieval", "retriever"),
179
+ archetype="working_memory",
180
+ description="Bundle memory, retrieval, and retained context repairs.",
181
+ ),
182
+ AgentSocietyRole(
183
+ name="arjuna",
184
+ proposal_kind="explorer",
185
+ phase=1,
186
+ archetype="focused_action",
187
+ description="Probe one controllable path at a time under metric feedback.",
188
+ ),
189
+ AgentSocietyRole(
190
+ name="hanuman",
191
+ proposal_kind="specialist",
192
+ phase=1,
193
+ path_prefixes=("tools", "framework", "voice", "browser", "cua", "implementation"),
194
+ archetype="bridge_builder",
195
+ description="Bundle tool, framework, world-interface, and runtime repairs.",
196
+ ),
197
+ AgentSocietyRole(
198
+ name="vidura",
199
+ proposal_kind="adversary",
200
+ phase=1,
201
+ path_prefixes=("security", "policy", "trust", "environment"),
202
+ archetype="prudent_critic",
203
+ description="Stress policy, security, trust-boundary, and environment choices.",
204
+ ),
205
+ AgentSocietyRole(
206
+ name="krishna",
207
+ proposal_kind="critic",
208
+ phase=2,
209
+ depends_on=("arjuna", "sutradhara", "smriti"),
210
+ archetype="charioteer_counsel",
211
+ description="Test one more change against current strong partial candidates.",
212
+ ),
213
+ AgentSocietyRole(
214
+ name="sangha",
215
+ proposal_kind="coverage_synthesis",
216
+ phase=2,
217
+ depends_on=("sutradhara", "smriti", "hanuman", "vidura", "arjuna"),
218
+ archetype="collective_synthesis",
219
+ description="Combine best path representatives across role evidence.",
220
+ ),
221
+ AgentSocietyRole(
222
+ name="dharma_steward",
223
+ proposal_kind="steward",
224
+ phase=3,
225
+ depends_on=("sangha", "krishna"),
226
+ archetype="minimal_process_guardian",
227
+ description="Remove one change at a time to keep only metric-proven repairs.",
228
+ ),
229
+ )
230
+
231
+
232
+ class AgentSearchStrategy:
233
+ """Proposal-generation strategy for framework-neutral agent optimization."""
234
+
235
+ name = "agent_search_strategy"
236
+ roles: Sequence[str] = ()
237
+
238
+ def propose(self, state: AgentSearchState) -> List[AgentSearchProposal]:
239
+ raise NotImplementedError
240
+
241
+
242
+ class DeterministicCouncilStrategy(AgentSearchStrategy):
243
+ """Current council search: explore, critique, synthesize, and steward."""
244
+
245
+ name = "deterministic_council_search"
246
+ roles = DEFAULT_COUNCIL_ROLES
247
+
248
+ def propose(self, state: AgentSearchState) -> List[AgentSearchProposal]:
249
+ return _build_round_proposals(
250
+ seed_candidate=state.seed_candidate,
251
+ evaluations=state.evaluations,
252
+ search_space=dict(state.search_space),
253
+ search_paths=state.search_paths,
254
+ beam_width=state.beam_width,
255
+ max_proposals=state.max_proposals,
256
+ round_number=state.round_number,
257
+ )
258
+
259
+
260
+ class SocietySearchStrategy(AgentSearchStrategy):
261
+ """
262
+ Deterministic role-diverse search for multi-interaction agent systems.
263
+
264
+ The strategy keeps metric-bound candidate evaluation, but allocates proposal
265
+ slots across social roles so search can test isolated mutations, component
266
+ bundles, stress combinations, synthesis, critique, and simplification.
267
+ """
268
+
269
+ name = "deterministic_society_search"
270
+ roles = SOCIETY_ROLES
271
+
272
+ def propose(self, state: AgentSearchState) -> List[AgentSearchProposal]:
273
+ return _build_society_proposals(
274
+ seed_candidate=state.seed_candidate,
275
+ evaluations=state.evaluations,
276
+ search_space=dict(state.search_space),
277
+ search_paths=state.search_paths,
278
+ diagnoses=state.diagnoses,
279
+ beam_width=state.beam_width,
280
+ max_proposals=state.max_proposals,
281
+ round_number=state.round_number,
282
+ ledger=state.ledger,
283
+ )
284
+
285
+
286
+ class SocietyRoleGraphSearchStrategy(AgentSearchStrategy):
287
+ """
288
+ Deterministic society search with explicit role graph metadata.
289
+
290
+ Role names and archetypes are inspiration labels only. Candidate acceptance
291
+ still depends entirely on the provided metric/evaluator contract.
292
+ """
293
+
294
+ name = "deterministic_role_graph_society_search"
295
+
296
+ def __init__(
297
+ self,
298
+ role_graph: Optional[Sequence[AgentSocietyRole | Mapping[str, Any]]] = None,
299
+ *,
300
+ max_paths_per_proposal: int = 1,
301
+ staged_conditioning: Optional[Mapping[str, Any]] = None,
302
+ ) -> None:
303
+ if max_paths_per_proposal < 1:
304
+ raise ValueError("max_paths_per_proposal must be at least 1.")
305
+ self.role_graph = _normalize_society_role_graph(role_graph)
306
+ self.roles = tuple(role.name for role in self.role_graph)
307
+ # Guna patch-radius base: explorer/adversary streams propose
308
+ # max(1, round(rajas * max_paths_per_proposal)) paths per patch. The
309
+ # default of 1 reproduces the legacy single-path radius for every
310
+ # default-archetype triple.
311
+ self.max_paths_per_proposal = max_paths_per_proposal
312
+ # 4C staged conditioning declaration (stage -> phase -> path-class);
313
+ # the strategy EXECUTES stages through role-graph phases — this is the
314
+ # declared map the optimizer trace proves the order from.
315
+ self.staged_conditioning = (
316
+ dict(staged_conditioning) if staged_conditioning is not None else None
317
+ )
318
+
319
+ def propose(self, state: AgentSearchState) -> List[AgentSearchProposal]:
320
+ return _build_role_graph_society_proposals(
321
+ seed_candidate=state.seed_candidate,
322
+ evaluations=state.evaluations,
323
+ search_space=dict(state.search_space),
324
+ search_paths=state.search_paths,
325
+ diagnoses=state.diagnoses,
326
+ beam_width=state.beam_width,
327
+ max_proposals=state.max_proposals,
328
+ round_number=state.round_number,
329
+ role_graph=self.role_graph,
330
+ ledger=state.ledger,
331
+ max_paths_per_proposal=self.max_paths_per_proposal,
332
+ )
333
+
334
+ def to_metadata(self) -> dict[str, Any]:
335
+ metadata = {
336
+ "role_graph": [role.to_metadata() for role in self.role_graph],
337
+ "role_graph_inspiration": (
338
+ "human social coordination, metacognition, and Hindu mythic "
339
+ "archetypes used only as deterministic proposal metadata"
340
+ ),
341
+ "guna_mix": _guna_mix(self.role_graph),
342
+ "chambers": {
343
+ chamber: [
344
+ role.name
345
+ for role in self.role_graph
346
+ if (role.chamber or _chamber_for_proposal_kind(role.proposal_kind))
347
+ == chamber
348
+ ]
349
+ for chamber in CHAMBER_TOKENS
350
+ },
351
+ "max_paths_per_proposal": self.max_paths_per_proposal,
352
+ }
353
+ if self.staged_conditioning is not None:
354
+ metadata["staged_conditioning"] = dict(self.staged_conditioning)
355
+ return metadata
356
+
357
+
358
+ class CouncilAgentOptimizer(BaseOptimizer):
359
+ """
360
+ Optimizes agent configs with deterministic multi-round social search.
361
+
362
+ `AgentOptimizer` is best when exhaustive candidate enumeration is acceptable.
363
+ This optimizer is intended for multi-interaction agents where useful fixes
364
+ are often combinations of partial changes: one role explores isolated
365
+ mutations, one critiques the current best candidate, one synthesizes strong
366
+ partial candidates, and one steward tests whether combined patches can be
367
+ simplified without losing score.
368
+ """
369
+
370
+ def __init__(
371
+ self,
372
+ target: Optional[OptimizationTarget] = None,
373
+ *,
374
+ evaluate_candidate: Optional[
375
+ Callable[[AgentCandidate], CandidateEvaluation | EvaluationResult | float]
376
+ ] = None,
377
+ simulation_evaluator: Any = None,
378
+ diagnoses: Optional[Iterable[ComponentDiagnosis | dict[str, Any]]] = None,
379
+ max_rounds: int = 3,
380
+ beam_width: int = 4,
381
+ max_proposals_per_round: int = 16,
382
+ target_score: float = 1.0,
383
+ include_seed: bool = True,
384
+ auto_diagnose: bool = True,
385
+ diagnostic_score_threshold: float = 0.85,
386
+ search_strategy: Optional[AgentSearchStrategy | str] = None,
387
+ samiti_budget: Optional[int] = None,
388
+ sabha_budget: Optional[int] = None,
389
+ society_ledger: bool = False,
390
+ social_memory: Optional[Any] = None,
391
+ ) -> None:
392
+ if max_rounds < 1:
393
+ raise ValueError("max_rounds must be at least 1.")
394
+ if beam_width < 1:
395
+ raise ValueError("beam_width must be at least 1.")
396
+ if max_proposals_per_round < 1:
397
+ raise ValueError("max_proposals_per_round must be at least 1.")
398
+ _validate_chamber_budgets(samiti_budget, sabha_budget)
399
+
400
+ self.target = target
401
+ self.evaluate_candidate = evaluate_candidate
402
+ self.simulation_evaluator = simulation_evaluator
403
+ self.diagnoses = _normalize_diagnoses(diagnoses)
404
+ self.max_rounds = max_rounds
405
+ self.beam_width = beam_width
406
+ self.max_proposals_per_round = max_proposals_per_round
407
+ self.target_score = target_score
408
+ self.include_seed = include_seed
409
+ self.auto_diagnose = auto_diagnose
410
+ self.diagnostic_score_threshold = diagnostic_score_threshold
411
+ self.search_strategy = _resolve_search_strategy(search_strategy)
412
+ self.samiti_budget = samiti_budget
413
+ self.sabha_budget = sabha_budget
414
+ self.society_ledger = society_ledger
415
+ self.social_memory = social_memory
416
+ super().__init__()
417
+
418
+ def optimize(
419
+ self,
420
+ evaluator: Any = None,
421
+ data_mapper: Any = None,
422
+ dataset: Optional[List[dict[str, Any]]] = None,
423
+ metric: Optional[Callable] = None,
424
+ *,
425
+ target: Optional[OptimizationTarget] = None,
426
+ evaluate_candidate: Optional[
427
+ Callable[[AgentCandidate], CandidateEvaluation | EvaluationResult | float]
428
+ ] = None,
429
+ simulation_evaluator: Any = None,
430
+ diagnoses: Optional[Iterable[ComponentDiagnosis | dict[str, Any]]] = None,
431
+ max_rounds: Optional[int] = None,
432
+ beam_width: Optional[int] = None,
433
+ max_proposals_per_round: Optional[int] = None,
434
+ target_score: Optional[float] = None,
435
+ include_seed: Optional[bool] = None,
436
+ auto_diagnose: Optional[bool] = None,
437
+ diagnostic_score_threshold: Optional[float] = None,
438
+ search_strategy: Optional[AgentSearchStrategy | str] = None,
439
+ samiti_budget: Optional[int] = None,
440
+ sabha_budget: Optional[int] = None,
441
+ society_ledger: Optional[bool] = None,
442
+ social_memory: Optional[Any] = None,
443
+ **kwargs: Any,
444
+ ) -> OptimizationResult:
445
+ active_target = target or self.target
446
+ if active_target is None:
447
+ raise ValueError("CouncilAgentOptimizer requires a target.")
448
+
449
+ active_evaluator = (
450
+ evaluate_candidate
451
+ or self.evaluate_candidate
452
+ or getattr(simulation_evaluator, "evaluate_candidate", None)
453
+ or getattr(self.simulation_evaluator, "evaluate_candidate", None)
454
+ )
455
+ if active_evaluator is None:
456
+ raise ValueError(
457
+ "CouncilAgentOptimizer requires evaluate_candidate or simulation_evaluator."
458
+ )
459
+
460
+ active_diagnoses = _normalize_diagnoses(diagnoses)
461
+ if diagnoses is None:
462
+ active_diagnoses = list(self.diagnoses)
463
+ active_max_rounds = self.max_rounds if max_rounds is None else max_rounds
464
+ active_beam_width = self.beam_width if beam_width is None else beam_width
465
+ active_max_proposals = (
466
+ self.max_proposals_per_round
467
+ if max_proposals_per_round is None
468
+ else max_proposals_per_round
469
+ )
470
+ if active_max_rounds < 1:
471
+ raise ValueError("max_rounds must be at least 1.")
472
+ if active_beam_width < 1:
473
+ raise ValueError("beam_width must be at least 1.")
474
+ if active_max_proposals < 1:
475
+ raise ValueError("max_proposals_per_round must be at least 1.")
476
+ active_target_score = (
477
+ self.target_score if target_score is None else target_score
478
+ )
479
+ use_include_seed = self.include_seed if include_seed is None else include_seed
480
+ use_auto_diagnose = self.auto_diagnose if auto_diagnose is None else auto_diagnose
481
+ active_diagnostic_threshold = (
482
+ self.diagnostic_score_threshold
483
+ if diagnostic_score_threshold is None
484
+ else diagnostic_score_threshold
485
+ )
486
+ active_search_strategy = (
487
+ self.search_strategy
488
+ if search_strategy is None
489
+ else _resolve_search_strategy(search_strategy)
490
+ )
491
+ active_samiti_budget = (
492
+ self.samiti_budget if samiti_budget is None else samiti_budget
493
+ )
494
+ active_sabha_budget = (
495
+ self.sabha_budget if sabha_budget is None else sabha_budget
496
+ )
497
+ _validate_chamber_budgets(active_samiti_budget, active_sabha_budget)
498
+ use_society_ledger = (
499
+ self.society_ledger if society_ledger is None else bool(society_ledger)
500
+ )
501
+ active_social_memory = (
502
+ self.social_memory if social_memory is None else social_memory
503
+ )
504
+
505
+ seed_candidate = active_target.seed_candidate()
506
+ evaluated: dict[str, CandidateEvaluation] = {}
507
+ history: List[IterationHistory] = []
508
+ role_counts: dict[str, int] = {}
509
+ round_summaries: List[dict[str, Any]] = []
510
+ best: CandidateEvaluation | None = None
511
+
512
+ role_chambers = _strategy_role_chambers(active_search_strategy)
513
+ chamber_budgets = {
514
+ "samiti": active_samiti_budget,
515
+ "sabha": active_sabha_budget,
516
+ }
517
+ chamber_used = {"samiti": 0, "sabha": 0}
518
+ chamber_skipped = {"samiti": 0, "sabha": 0}
519
+ rejections: List[dict[str, Any]] = []
520
+ ledger_rounds: List[dict[str, Any]] = []
521
+ current_ledger: Optional[dict[str, Any]] = None
522
+ persisted_via: Optional[str] = None
523
+ if use_society_ledger and active_social_memory is not None:
524
+ persisted_via = active_social_memory.__class__.__name__
525
+ prior_ledgers = list(
526
+ getattr(active_social_memory, "society_ledgers", None) or []
527
+ )
528
+ prior_diagnoses = [
529
+ dict(item)
530
+ for entry in prior_ledgers
531
+ if isinstance(entry, Mapping)
532
+ for item in entry.get("diagnoses", []) or []
533
+ if isinstance(item, Mapping)
534
+ ]
535
+ if prior_diagnoses:
536
+ # Cross-campaign preload: previously persisted ledgers seed
537
+ # round 1 of this campaign (GEA experience pooling).
538
+ current_ledger = {
539
+ "round": 0,
540
+ "diagnoses": prior_diagnoses,
541
+ "pooled_from_candidates": sum(
542
+ int(entry.get("pooled_from_candidates") or 0)
543
+ for entry in prior_ledgers
544
+ if isinstance(entry, Mapping)
545
+ ),
546
+ "preloaded": True,
547
+ }
548
+
549
+ if use_include_seed:
550
+ seed_evaluation = self._evaluate(
551
+ seed_candidate,
552
+ active_evaluator,
553
+ evaluated,
554
+ history,
555
+ role_counts,
556
+ role="seed",
557
+ round_number=0,
558
+ )
559
+ best = seed_evaluation
560
+ if use_auto_diagnose and not active_diagnoses:
561
+ active_diagnoses = _diagnose_candidate_evaluation(
562
+ seed_evaluation,
563
+ failing_threshold=active_diagnostic_threshold,
564
+ )
565
+
566
+ search_paths = _ordered_search_paths(active_target, active_diagnoses)
567
+ if not search_paths:
568
+ raise ValueError("CouncilAgentOptimizer target search space cannot be empty.")
569
+
570
+ for round_number in range(1, active_max_rounds + 1):
571
+ proposals = active_search_strategy.propose(
572
+ AgentSearchState(
573
+ seed_candidate=seed_candidate,
574
+ evaluations=list(evaluated.values()),
575
+ search_space=active_target.search_space,
576
+ search_paths=search_paths,
577
+ diagnoses=active_diagnoses,
578
+ beam_width=active_beam_width,
579
+ max_proposals=active_max_proposals,
580
+ round_number=round_number,
581
+ ledger=current_ledger,
582
+ )
583
+ )
584
+ admitted_proposals: List[AgentSearchProposal] = []
585
+ for proposal in proposals:
586
+ duplicate_id = _candidate_id_for_patch(seed_candidate, proposal.patch)
587
+ if duplicate_id in evaluated:
588
+ rejections.append(
589
+ {
590
+ "round": round_number,
591
+ "role": proposal.role,
592
+ "candidate_id": duplicate_id,
593
+ "rejected": True,
594
+ "hetvabhasa_class": "savyabhichara",
595
+ "detail": (
596
+ "duplicate patch: evidence does not discriminate "
597
+ "from an already-evaluated candidate"
598
+ ),
599
+ }
600
+ )
601
+ continue
602
+ admitted_proposals.append(proposal)
603
+ proposals = admitted_proposals
604
+ logger.info(
605
+ "Council round %s evaluating %s proposal(s)",
606
+ round_number,
607
+ len(proposals),
608
+ )
609
+
610
+ round_best = best
611
+ round_evaluated = 0
612
+ round_evaluations: List[CandidateEvaluation] = []
613
+ for proposal in proposals:
614
+ allowed_paths = set(search_paths)
615
+ if proposal.patch and not (set(proposal.patch) & allowed_paths):
616
+ # Locality breach is recorded (asiddha) but not enforced
617
+ # here — promotion-time enforcement is the replay veto's
618
+ # job; in-round evidence must stay visible.
619
+ rejections.append(
620
+ {
621
+ "round": round_number,
622
+ "role": proposal.role,
623
+ "candidate_id": _candidate_id_for_patch(
624
+ seed_candidate, proposal.patch
625
+ ),
626
+ "rejected": False,
627
+ "hetvabhasa_class": "asiddha",
628
+ "detail": (
629
+ "patch touches no path inside the diagnosed "
630
+ "search locality"
631
+ ),
632
+ }
633
+ )
634
+ chamber = _proposal_chamber(proposal, role_chambers)
635
+ candidate = seed_candidate.with_patch(
636
+ proposal.patch,
637
+ metadata={
638
+ "kind": "council_proposal",
639
+ "optimizer": self.__class__.__name__,
640
+ "proposal_role": proposal.role,
641
+ "proposal_reason": proposal.reason,
642
+ "proposal_round": round_number,
643
+ "proposal_parent_ids": list(proposal.parent_ids),
644
+ "proposal_metadata": dict(proposal.metadata),
645
+ },
646
+ )
647
+ is_new_candidate = candidate.id not in evaluated
648
+ declared_chamber_budget = chamber_budgets.get(chamber)
649
+ if (
650
+ is_new_candidate
651
+ and declared_chamber_budget is not None
652
+ and chamber_used[chamber] >= declared_chamber_budget
653
+ ):
654
+ chamber_skipped[chamber] += 1
655
+ continue
656
+ evaluation = self._evaluate(
657
+ candidate,
658
+ active_evaluator,
659
+ evaluated,
660
+ history,
661
+ role_counts,
662
+ role=proposal.role,
663
+ round_number=round_number,
664
+ )
665
+ if is_new_candidate:
666
+ chamber_used[chamber] += 1
667
+ round_evaluations.append(evaluation)
668
+ parent_scores = [
669
+ evaluated[parent_id].score
670
+ for parent_id in proposal.parent_ids
671
+ if parent_id in evaluated and parent_id != candidate.id
672
+ ]
673
+ role_kind = str(
674
+ proposal.metadata.get("role_kind") or proposal.role
675
+ )
676
+ if parent_scores and evaluation.score < max(parent_scores):
677
+ rejections.append(
678
+ {
679
+ "round": round_number,
680
+ "role": proposal.role,
681
+ "candidate_id": candidate.id,
682
+ "rejected": True,
683
+ "hetvabhasa_class": "viruddha",
684
+ "detail": (
685
+ f"score {evaluation.score:.4f} regresses parent "
686
+ f"best {max(parent_scores):.4f}"
687
+ ),
688
+ }
689
+ )
690
+ elif (
691
+ role_kind == "steward"
692
+ and parent_scores
693
+ and evaluation.score == max(parent_scores)
694
+ ):
695
+ rejections.append(
696
+ {
697
+ "round": round_number,
698
+ "role": proposal.role,
699
+ "candidate_id": proposal.parent_ids[0]
700
+ if proposal.parent_ids
701
+ else candidate.id,
702
+ "rejected": True,
703
+ "hetvabhasa_class": "satpratipaksha",
704
+ "detail": (
705
+ "steward removal kept the score unchanged: the "
706
+ "removed change carries an equal counter-"
707
+ "justification"
708
+ ),
709
+ }
710
+ )
711
+ if round_best is None or evaluation.score > round_best.score:
712
+ round_best = evaluation
713
+ round_evaluated += 1
714
+ if best is None or evaluation.score > best.score:
715
+ best = evaluation
716
+ logger.info(
717
+ "New best council candidate %s score=%.4f",
718
+ candidate.id,
719
+ evaluation.score,
720
+ )
721
+ if best.score >= active_target_score:
722
+ break
723
+
724
+ if use_society_ledger:
725
+ pooled_diagnoses: List[ComponentDiagnosis] = []
726
+ contributing = 0
727
+ for evaluation in round_evaluations:
728
+ candidate_diagnoses = _diagnose_candidate_evaluation(
729
+ evaluation,
730
+ failing_threshold=active_diagnostic_threshold,
731
+ )
732
+ if candidate_diagnoses:
733
+ contributing += 1
734
+ pooled_diagnoses.extend(candidate_diagnoses)
735
+ round_ledger = {
736
+ "round": round_number,
737
+ "diagnoses": [
738
+ _dump_model(item)
739
+ for item in _dedupe_diagnoses(pooled_diagnoses)
740
+ ],
741
+ "pooled_from_candidates": len(round_evaluations),
742
+ "contributing_candidates": contributing,
743
+ }
744
+ ledger_rounds.append(
745
+ {
746
+ "round": round_number,
747
+ "diagnoses_pooled": len(round_ledger["diagnoses"]),
748
+ "pooled_from_candidates": round_ledger[
749
+ "pooled_from_candidates"
750
+ ],
751
+ "persisted_via": persisted_via,
752
+ }
753
+ )
754
+ current_ledger = round_ledger
755
+ if active_social_memory is not None:
756
+ society_ledgers = getattr(
757
+ active_social_memory, "society_ledgers", None
758
+ )
759
+ if society_ledgers is None:
760
+ society_ledgers = []
761
+ active_social_memory.society_ledgers = society_ledgers
762
+ society_ledgers.append(dict(round_ledger))
763
+
764
+ if round_best is not None and use_auto_diagnose:
765
+ round_diagnoses = _diagnose_candidate_evaluation(
766
+ round_best,
767
+ failing_threshold=active_diagnostic_threshold,
768
+ )
769
+ if round_diagnoses:
770
+ active_diagnoses = _dedupe_diagnoses(
771
+ [*active_diagnoses, *round_diagnoses]
772
+ )
773
+ search_paths = _ordered_search_paths(active_target, active_diagnoses)
774
+
775
+ round_summaries.append(
776
+ {
777
+ "round": round_number,
778
+ "proposals": len(proposals),
779
+ "evaluated": round_evaluated,
780
+ "best_score": best.score if best is not None else None,
781
+ "search_paths": list(search_paths),
782
+ }
783
+ )
784
+ if best is not None and best.score >= active_target_score:
785
+ break
786
+
787
+ if best is None:
788
+ raise ValueError("CouncilAgentOptimizer did not evaluate any candidates.")
789
+
790
+ strategy_name = getattr(
791
+ active_search_strategy,
792
+ "name",
793
+ active_search_strategy.__class__.__name__,
794
+ )
795
+ strategy_roles = list(getattr(active_search_strategy, "roles", ()))
796
+ metadata = {
797
+ "optimizer": self.__class__.__name__,
798
+ "strategy": strategy_name,
799
+ "roles": strategy_roles,
800
+ "target_name": best.candidate.target_name,
801
+ "best_candidate_id": best.candidate.id,
802
+ "search_paths": list(search_paths),
803
+ "rounds": round_summaries,
804
+ "beam_width": active_beam_width,
805
+ "max_proposals_per_round": active_max_proposals,
806
+ "role_evaluations": role_counts,
807
+ }
808
+ strategy_metadata = _strategy_metadata(active_search_strategy)
809
+ if strategy_metadata:
810
+ metadata["strategy_metadata"] = strategy_metadata
811
+ if "role_graph" in strategy_metadata:
812
+ metadata["role_graph"] = strategy_metadata["role_graph"]
813
+ if "guna_mix" in strategy_metadata:
814
+ metadata["guna_mix"] = strategy_metadata["guna_mix"]
815
+ if active_diagnoses:
816
+ metadata["diagnostics"] = [_dump_model(item) for item in active_diagnoses]
817
+ metadata["auto_diagnosed"] = use_auto_diagnose
818
+
819
+ # Phase 4 society/governance surfaces (additive; ranking comes only
820
+ # from the evaluation suite — external-verification rule).
821
+ metadata["ranking_source"] = "evaluation_suite"
822
+ metadata["chambers"] = {
823
+ chamber: {
824
+ "roles": sorted(
825
+ name
826
+ for name, value in role_chambers.items()
827
+ if value == chamber
828
+ and name in set(strategy_roles) | set(role_counts)
829
+ ),
830
+ "declared_budget": chamber_budgets[chamber],
831
+ "evaluations_used": chamber_used[chamber],
832
+ "skipped_proposals": chamber_skipped[chamber],
833
+ }
834
+ for chamber in CHAMBER_TOKENS
835
+ }
836
+ if rejections:
837
+ metadata["rejections"] = rejections
838
+ if ledger_rounds:
839
+ metadata["ledger_rounds"] = ledger_rounds
840
+ metadata["society_ledger"] = True
841
+ metadata["nirnaya"] = [
842
+ {
843
+ "round": round_summaries[-1]["round"] if round_summaries else 1,
844
+ "decision": "promote",
845
+ "selected_candidate_id": best.candidate.id,
846
+ "justification": _justification(
847
+ pratijna=(
848
+ f"candidate {best.candidate.id} is the promotable winner"
849
+ ),
850
+ hetu=(
851
+ f"top admissible evaluation score {best.score:.4f} from "
852
+ "the evaluation suite"
853
+ ),
854
+ udaharana=(
855
+ "rule: selection is single-lineage — the steward promotes "
856
+ "the top-ranked evidence-backed candidate, never an average"
857
+ ),
858
+ upanaya=(
859
+ f"candidate {best.candidate.id} holds the top rank in this "
860
+ "run's lineage"
861
+ ),
862
+ nigamana=(
863
+ "promotion is expected to re-close every frozen evidence "
864
+ "row on replay"
865
+ ),
866
+ ),
867
+ "rejected_alternatives": [
868
+ {
869
+ "candidate_id": rejection.get("candidate_id"),
870
+ "hetvabhasa_class": rejection.get("hetvabhasa_class"),
871
+ }
872
+ for rejection in rejections
873
+ if rejection.get("rejected")
874
+ ],
875
+ "replay_verdict": None,
876
+ "admissible_evidence_refs": [best.candidate.id],
877
+ "frozen_rows_closed": None,
878
+ }
879
+ ]
880
+
881
+ return OptimizationResult(
882
+ best_generator=best.candidate,
883
+ best_candidate=best.candidate,
884
+ history=history,
885
+ final_score=best.score,
886
+ total_iterations=len(history),
887
+ total_evaluations=len(history),
888
+ metadata=metadata,
889
+ )
890
+
891
+ def _evaluate(
892
+ self,
893
+ candidate: AgentCandidate,
894
+ evaluator: Callable[[AgentCandidate], CandidateEvaluation | EvaluationResult | float],
895
+ evaluated: dict[str, CandidateEvaluation],
896
+ history: List[IterationHistory],
897
+ role_counts: dict[str, int],
898
+ *,
899
+ role: str,
900
+ round_number: int,
901
+ ) -> CandidateEvaluation:
902
+ if candidate.id in evaluated:
903
+ return evaluated[candidate.id]
904
+
905
+ value = evaluator(candidate)
906
+ evaluation = _normalize_candidate_evaluation(value, candidate)
907
+ evaluation.metadata = {
908
+ **candidate.metadata,
909
+ **evaluation.metadata,
910
+ "proposal_role": role,
911
+ "proposal_round": round_number,
912
+ }
913
+ evaluated[candidate.id] = evaluation
914
+ history.append(_history_from_candidate(evaluation))
915
+ role_counts[role] = role_counts.get(role, 0) + 1
916
+ return evaluation
917
+
918
+
919
+ class SocietyAgentOptimizer(CouncilAgentOptimizer):
920
+ """
921
+ Council optimizer preset using role-diverse society search.
922
+
923
+ It is deterministic by default and uses the same `OptimizationTarget` and
924
+ evaluator contracts as `AgentOptimizer`/`CouncilAgentOptimizer`.
925
+ """
926
+
927
+ def __init__(
928
+ self,
929
+ *args: Any,
930
+ search_strategy: Optional[AgentSearchStrategy | str] = None,
931
+ **kwargs: Any,
932
+ ) -> None:
933
+ super().__init__(
934
+ *args,
935
+ search_strategy=search_strategy or SocietySearchStrategy(),
936
+ **kwargs,
937
+ )
938
+
939
+
940
+ def _ordered_search_paths(
941
+ target: OptimizationTarget,
942
+ diagnoses: Sequence[ComponentDiagnosis],
943
+ ) -> List[str]:
944
+ allowed_paths = relevant_search_paths(target.search_space, diagnoses)
945
+ return [path for path in target.search_space if path in allowed_paths]
946
+
947
+
948
+ def _resolve_search_strategy(
949
+ strategy: Optional[AgentSearchStrategy | str | Mapping[str, Any]],
950
+ ) -> AgentSearchStrategy:
951
+ if strategy is None or strategy == "council":
952
+ return DeterministicCouncilStrategy()
953
+ if strategy == "society":
954
+ return SocietySearchStrategy()
955
+ if isinstance(strategy, str) and strategy in {"role_graph", "society_role_graph"}:
956
+ return SocietyRoleGraphSearchStrategy()
957
+ if isinstance(strategy, AgentSearchStrategy):
958
+ return strategy
959
+ if isinstance(strategy, Mapping):
960
+ # Phase 4 (extend-only): JSON-declarable strategy — lets optimization
961
+ # manifests declare a staged role-graph society search without
962
+ # constructing strategy objects.
963
+ token = str(
964
+ strategy.get("strategy")
965
+ or strategy.get("name")
966
+ or strategy.get("type")
967
+ or "role_graph"
968
+ )
969
+ if token not in {"role_graph", "society_role_graph"}:
970
+ return _resolve_search_strategy(token)
971
+ return SocietyRoleGraphSearchStrategy(
972
+ strategy.get("role_graph"),
973
+ max_paths_per_proposal=int(strategy.get("max_paths_per_proposal", 1)),
974
+ staged_conditioning=strategy.get("staged_conditioning"),
975
+ )
976
+ if hasattr(strategy, "propose"):
977
+ return strategy # type: ignore[return-value]
978
+ raise ValueError(
979
+ "search_strategy must be 'council', 'society', 'role_graph', "
980
+ "'society_role_graph', a strategy mapping, or an AgentSearchStrategy."
981
+ )
982
+
983
+
984
+ def _strategy_metadata(strategy: AgentSearchStrategy) -> dict[str, Any]:
985
+ to_metadata = getattr(strategy, "to_metadata", None)
986
+ if callable(to_metadata):
987
+ metadata = to_metadata()
988
+ if isinstance(metadata, Mapping):
989
+ return dict(metadata)
990
+ return {}
991
+
992
+
993
+ def _validate_chamber_budgets(
994
+ samiti_budget: Optional[int],
995
+ sabha_budget: Optional[int],
996
+ ) -> None:
997
+ if samiti_budget is not None and samiti_budget < 1:
998
+ raise ValueError("samiti_budget must be at least 1 when declared.")
999
+ if sabha_budget is not None and sabha_budget < 1:
1000
+ raise ValueError("sabha_budget must be at least 1 when declared.")
1001
+
1002
+
1003
+ def _strategy_role_chambers(strategy: AgentSearchStrategy) -> dict[str, str]:
1004
+ """Role name/kind -> chamber map for evaluation attribution."""
1005
+
1006
+ chambers: dict[str, str] = {}
1007
+ role_graph = getattr(strategy, "role_graph", None) or ()
1008
+ for role in role_graph:
1009
+ if isinstance(role, AgentSocietyRole):
1010
+ chambers[role.name] = role.chamber or _chamber_for_proposal_kind(
1011
+ role.proposal_kind
1012
+ )
1013
+ for kind in ROLE_GRAPH_PROPOSAL_KINDS:
1014
+ chambers.setdefault(kind, _chamber_for_proposal_kind(kind))
1015
+ return chambers
1016
+
1017
+
1018
+ def _proposal_chamber(
1019
+ proposal: AgentSearchProposal,
1020
+ role_chambers: Mapping[str, str],
1021
+ ) -> str:
1022
+ explicit = proposal.metadata.get("role_chamber")
1023
+ if explicit in CHAMBER_TOKENS:
1024
+ return str(explicit)
1025
+ if proposal.role in role_chambers:
1026
+ return role_chambers[proposal.role]
1027
+ role_kind = str(proposal.metadata.get("role_kind") or proposal.role)
1028
+ return _chamber_for_proposal_kind(role_kind)
1029
+
1030
+
1031
+ def _normalize_society_role_graph(
1032
+ role_graph: Optional[Sequence[AgentSocietyRole | Mapping[str, Any]]],
1033
+ ) -> tuple[AgentSocietyRole, ...]:
1034
+ roles: List[AgentSocietyRole] = []
1035
+ for item in role_graph or DEFAULT_SOCIETY_ROLE_GRAPH:
1036
+ if isinstance(item, AgentSocietyRole):
1037
+ role = item
1038
+ elif isinstance(item, Mapping):
1039
+ role = AgentSocietyRole(
1040
+ name=str(item["name"]),
1041
+ proposal_kind=str(item["proposal_kind"]),
1042
+ phase=int(item.get("phase", 1)),
1043
+ depends_on=tuple(str(value) for value in item.get("depends_on", ())),
1044
+ path_prefixes=tuple(
1045
+ str(value) for value in item.get("path_prefixes", ())
1046
+ ),
1047
+ archetype=str(item.get("archetype", "")),
1048
+ description=str(item.get("description", "")),
1049
+ guna=item.get("guna"),
1050
+ chamber=item.get("chamber"),
1051
+ )
1052
+ else:
1053
+ raise TypeError("role_graph entries must be AgentSocietyRole or mappings")
1054
+ if role.proposal_kind not in ROLE_GRAPH_PROPOSAL_KINDS:
1055
+ raise ValueError(
1056
+ f"Unsupported society role proposal_kind '{role.proposal_kind}'."
1057
+ )
1058
+ if role.phase < 1:
1059
+ raise ValueError("society role phase must be at least 1.")
1060
+ # Phase 4: resolve absent guna through the archetype-default table,
1061
+ # validate explicit triples, derive absent chamber from role kind.
1062
+ guna = _normalized_guna(role)
1063
+ chamber = role.chamber or _chamber_for_proposal_kind(role.proposal_kind)
1064
+ if chamber not in CHAMBER_TOKENS:
1065
+ raise ValueError(
1066
+ f"society role chamber must be one of {CHAMBER_TOKENS}, "
1067
+ f"got {chamber!r}."
1068
+ )
1069
+ roles.append(replace(role, guna=guna, chamber=chamber))
1070
+
1071
+ names = [role.name for role in roles]
1072
+ if len(names) != len(set(names)):
1073
+ raise ValueError("society role names must be unique.")
1074
+ return tuple(roles)
1075
+
1076
+
1077
+ def _normalized_guna(role: AgentSocietyRole) -> dict[str, float]:
1078
+ if role.guna is None:
1079
+ rajas, sattva, tamas = GUNA_ARCHETYPE_DEFAULTS.get(
1080
+ role.archetype, GUNA_ARCHETYPE_DEFAULTS[""]
1081
+ )
1082
+ return {"rajas": rajas, "sattva": sattva, "tamas": tamas}
1083
+ guna = dict(role.guna)
1084
+ if set(guna) != set(GUNA_AXES):
1085
+ raise ValueError(
1086
+ f"society role guna must declare exactly the axes {GUNA_AXES}, "
1087
+ f"got {sorted(guna)}."
1088
+ )
1089
+ for axis in GUNA_AXES:
1090
+ value = guna[axis]
1091
+ if not isinstance(value, (int, float)) or isinstance(value, bool):
1092
+ raise ValueError(f"society role guna {axis} must be a number in [0, 1].")
1093
+ if not 0.0 <= float(value) <= 1.0:
1094
+ raise ValueError(
1095
+ f"society role guna {axis} must be in [0, 1], got {value!r}."
1096
+ )
1097
+ return {axis: float(guna[axis]) for axis in GUNA_AXES}
1098
+
1099
+
1100
+ def _chamber_for_proposal_kind(proposal_kind: str) -> str:
1101
+ return "samiti" if proposal_kind in SAMITI_PROPOSAL_KINDS else "sabha"
1102
+
1103
+
1104
+ def _guna_mix(role_graph: Sequence[AgentSocietyRole]) -> dict[str, float]:
1105
+ """Society mean guna triple — the declared, tunable meta-parameter."""
1106
+
1107
+ resolved = [_normalized_guna(role) for role in role_graph]
1108
+ if not resolved:
1109
+ return {axis: 0.0 for axis in GUNA_AXES}
1110
+ return {
1111
+ axis: round(sum(item[axis] for item in resolved) / len(resolved), 4)
1112
+ for axis in GUNA_AXES
1113
+ }
1114
+
1115
+
1116
+ def _guna_radius(rajas: float, max_paths_per_proposal: int) -> int:
1117
+ """Mechanical rajas mapping: patch-radius units for generative streams."""
1118
+
1119
+ return max(1, round(rajas * max_paths_per_proposal))
1120
+
1121
+
1122
+ def _validate_justification(justification: Mapping[str, Any]) -> dict[str, str]:
1123
+ """Reject panca-avayava mappings missing any member or carrying empties."""
1124
+
1125
+ if not isinstance(justification, Mapping):
1126
+ raise ValueError("proposal justification must be a mapping.")
1127
+ record: dict[str, str] = {}
1128
+ for member in PANCA_AVAYAVA_MEMBERS:
1129
+ value = str(justification.get(member) or "").strip()
1130
+ if not value:
1131
+ raise ValueError(
1132
+ f"proposal justification is missing a non-empty '{member}' member."
1133
+ )
1134
+ record[member] = value
1135
+ return record
1136
+
1137
+
1138
+ def _justification(
1139
+ *,
1140
+ pratijna: str,
1141
+ hetu: str,
1142
+ udaharana: str,
1143
+ upanaya: str,
1144
+ nigamana: str,
1145
+ ) -> dict[str, str]:
1146
+ return {
1147
+ "pratijna": pratijna,
1148
+ "hetu": hetu,
1149
+ "udaharana": udaharana,
1150
+ "upanaya": upanaya,
1151
+ "nigamana": nigamana,
1152
+ }
1153
+
1154
+
1155
+ def _ledger_diagnoses(
1156
+ ledger: Optional[Mapping[str, Any]],
1157
+ ) -> List[ComponentDiagnosis]:
1158
+ if not ledger:
1159
+ return []
1160
+ return _normalize_diagnoses(
1161
+ item
1162
+ for item in ledger.get("diagnoses", []) or []
1163
+ if isinstance(item, (Mapping, ComponentDiagnosis))
1164
+ )
1165
+
1166
+
1167
+ def _build_round_proposals(
1168
+ *,
1169
+ seed_candidate: AgentCandidate,
1170
+ evaluations: Sequence[CandidateEvaluation],
1171
+ search_space: dict[str, List[Any]],
1172
+ search_paths: Sequence[str],
1173
+ beam_width: int,
1174
+ max_proposals: int,
1175
+ round_number: int,
1176
+ ) -> List[AgentSearchProposal]:
1177
+ proposals: List[AgentSearchProposal] = []
1178
+ seen: set[str] = set()
1179
+ ranked = sorted(
1180
+ evaluations,
1181
+ key=lambda item: (item.score, -len(item.candidate.patch), item.candidate.id),
1182
+ reverse=True,
1183
+ )
1184
+ changed_ranked = [item for item in ranked if item.candidate.patch]
1185
+ beam = ranked[:beam_width] or [
1186
+ CandidateEvaluation(candidate=seed_candidate, score=0.0)
1187
+ ]
1188
+
1189
+ if round_number > 1:
1190
+ for proposal in _synthesis_proposals(changed_ranked[:beam_width], search_paths):
1191
+ _append_proposal(proposals, seen, proposal, max_proposals)
1192
+
1193
+ if round_number > 1:
1194
+ for evaluation in beam:
1195
+ if not evaluation.candidate.patch:
1196
+ continue
1197
+ for proposal in _critic_proposals(
1198
+ evaluation.candidate,
1199
+ search_space,
1200
+ search_paths,
1201
+ ):
1202
+ _append_proposal(proposals, seen, proposal, max_proposals)
1203
+
1204
+ for proposal in _explorer_proposals(seed_candidate, search_space, search_paths):
1205
+ _append_proposal(proposals, seen, proposal, max_proposals)
1206
+
1207
+ if round_number > 1:
1208
+ for evaluation in changed_ranked[:beam_width]:
1209
+ for proposal in _steward_proposals(evaluation.candidate):
1210
+ _append_proposal(proposals, seen, proposal, max_proposals)
1211
+
1212
+ return proposals
1213
+
1214
+
1215
+ def _build_society_proposals(
1216
+ *,
1217
+ seed_candidate: AgentCandidate,
1218
+ evaluations: Sequence[CandidateEvaluation],
1219
+ search_space: dict[str, List[Any]],
1220
+ search_paths: Sequence[str],
1221
+ diagnoses: Sequence[ComponentDiagnosis],
1222
+ beam_width: int,
1223
+ max_proposals: int,
1224
+ round_number: int,
1225
+ ledger: Optional[Mapping[str, Any]] = None,
1226
+ ) -> List[AgentSearchProposal]:
1227
+ ranked = sorted(
1228
+ evaluations,
1229
+ key=lambda item: (item.score, -len(item.candidate.patch), item.candidate.id),
1230
+ reverse=True,
1231
+ )
1232
+ changed_ranked = [item for item in ranked if item.candidate.patch]
1233
+ beam = ranked[:beam_width] or [
1234
+ CandidateEvaluation(candidate=seed_candidate, score=0.0)
1235
+ ]
1236
+ pooled_diagnoses = list(diagnoses)
1237
+ ledger_diagnoses = _ledger_diagnoses(ledger)
1238
+ if ledger_diagnoses:
1239
+ # GEA experience pooling: no role reasons only from its own
1240
+ # candidate's diagnoses — the round-scoped society ledger joins in.
1241
+ pooled_diagnoses = _dedupe_diagnoses([*pooled_diagnoses, *ledger_diagnoses])
1242
+
1243
+ streams: List[Iterable[AgentSearchProposal]] = []
1244
+ if round_number > 1:
1245
+ streams.append(_coverage_synthesis_proposals(changed_ranked, search_paths))
1246
+ streams.append(_synthesis_proposals(changed_ranked[:beam_width], search_paths))
1247
+ streams.append(
1248
+ proposal
1249
+ for evaluation in beam
1250
+ if evaluation.candidate.patch
1251
+ for proposal in _critic_proposals(
1252
+ evaluation.candidate,
1253
+ search_space,
1254
+ search_paths,
1255
+ )
1256
+ )
1257
+
1258
+ streams.extend(
1259
+ [
1260
+ _specialist_proposals(
1261
+ seed_candidate,
1262
+ search_space,
1263
+ search_paths,
1264
+ pooled_diagnoses,
1265
+ ),
1266
+ _explorer_proposals(seed_candidate, search_space, search_paths),
1267
+ _adversary_proposals(
1268
+ seed_candidate,
1269
+ ranked[:beam_width],
1270
+ search_space,
1271
+ search_paths,
1272
+ ),
1273
+ ]
1274
+ )
1275
+
1276
+ if round_number > 1:
1277
+ streams.append(
1278
+ proposal
1279
+ for evaluation in changed_ranked[:beam_width]
1280
+ for proposal in _steward_proposals(evaluation.candidate)
1281
+ )
1282
+
1283
+ return _interleave_proposal_streams(streams, max_proposals)
1284
+
1285
+
1286
+ def _build_role_graph_society_proposals(
1287
+ *,
1288
+ seed_candidate: AgentCandidate,
1289
+ evaluations: Sequence[CandidateEvaluation],
1290
+ search_space: dict[str, List[Any]],
1291
+ search_paths: Sequence[str],
1292
+ diagnoses: Sequence[ComponentDiagnosis],
1293
+ beam_width: int,
1294
+ max_proposals: int,
1295
+ round_number: int,
1296
+ role_graph: Sequence[AgentSocietyRole],
1297
+ ledger: Optional[Mapping[str, Any]] = None,
1298
+ max_paths_per_proposal: int = 1,
1299
+ ) -> List[AgentSearchProposal]:
1300
+ ranked = sorted(
1301
+ evaluations,
1302
+ key=lambda item: (item.score, -len(item.candidate.patch), item.candidate.id),
1303
+ reverse=True,
1304
+ )
1305
+ changed_ranked = [item for item in ranked if item.candidate.patch]
1306
+ beam = ranked[:beam_width] or [
1307
+ CandidateEvaluation(candidate=seed_candidate, score=0.0)
1308
+ ]
1309
+ evaluated_roles = {
1310
+ str(evaluation.metadata.get("proposal_role"))
1311
+ for evaluation in evaluations
1312
+ if evaluation.metadata.get("proposal_role")
1313
+ }
1314
+ pooled_diagnoses = list(diagnoses)
1315
+ ledger_diagnoses = _ledger_diagnoses(ledger)
1316
+ if ledger_diagnoses:
1317
+ pooled_diagnoses = _dedupe_diagnoses([*pooled_diagnoses, *ledger_diagnoses])
1318
+
1319
+ streams: List[Iterable[AgentSearchProposal]] = []
1320
+ for role in _ordered_role_graph_roles(role_graph, round_number):
1321
+ if not _society_role_is_active(role, evaluated_roles, round_number):
1322
+ continue
1323
+ role_paths = _role_search_paths(role, search_paths)
1324
+ if role.proposal_kind != "steward" and not role_paths:
1325
+ continue
1326
+ stream = _role_graph_stream(
1327
+ role,
1328
+ seed_candidate=seed_candidate,
1329
+ ranked=ranked,
1330
+ changed_ranked=changed_ranked,
1331
+ beam=beam,
1332
+ search_space=search_space,
1333
+ search_paths=role_paths,
1334
+ diagnoses=pooled_diagnoses,
1335
+ beam_width=beam_width,
1336
+ round_number=round_number,
1337
+ max_paths_per_proposal=max_paths_per_proposal,
1338
+ )
1339
+ streams.append(stream)
1340
+
1341
+ return _interleave_proposal_streams(streams, max_proposals)
1342
+
1343
+
1344
+ def _ordered_role_graph_roles(
1345
+ role_graph: Sequence[AgentSocietyRole],
1346
+ round_number: int,
1347
+ ) -> List[AgentSocietyRole]:
1348
+ if round_number <= 1:
1349
+ return list(role_graph)
1350
+ priority = {
1351
+ "coverage_synthesis": 0,
1352
+ "synthesizer": 0,
1353
+ "critic": 1,
1354
+ "adversary": 2,
1355
+ "specialist": 2,
1356
+ "explorer": 2,
1357
+ "steward": 3,
1358
+ }
1359
+ return [
1360
+ role
1361
+ for _, role in sorted(
1362
+ enumerate(role_graph),
1363
+ key=lambda item: (priority.get(item[1].proposal_kind, 2), item[0]),
1364
+ )
1365
+ ]
1366
+
1367
+
1368
+ def _society_role_is_active(
1369
+ role: AgentSocietyRole,
1370
+ evaluated_roles: set[str],
1371
+ round_number: int,
1372
+ ) -> bool:
1373
+ if role.phase > round_number:
1374
+ return False
1375
+ if not role.depends_on:
1376
+ return True
1377
+ return bool(set(role.depends_on) & evaluated_roles)
1378
+
1379
+
1380
+ def _role_graph_stream(
1381
+ role: AgentSocietyRole,
1382
+ *,
1383
+ seed_candidate: AgentCandidate,
1384
+ ranked: Sequence[CandidateEvaluation],
1385
+ changed_ranked: Sequence[CandidateEvaluation],
1386
+ beam: Sequence[CandidateEvaluation],
1387
+ search_space: dict[str, List[Any]],
1388
+ search_paths: Sequence[str],
1389
+ diagnoses: Sequence[ComponentDiagnosis],
1390
+ beam_width: int,
1391
+ round_number: int,
1392
+ max_paths_per_proposal: int = 1,
1393
+ ) -> Iterable[AgentSearchProposal]:
1394
+ # Deterministic guna behavioral mappings (pure functions of the resolved
1395
+ # triple): rajas scales generative patch radius, sattva scales synthesis
1396
+ # breadth / reconciliation, tamas scales steward removal aggressiveness.
1397
+ guna = _normalized_guna(role)
1398
+ radius_units = _guna_radius(guna["rajas"], max_paths_per_proposal)
1399
+ if role.proposal_kind == "specialist":
1400
+ proposals = _specialist_proposals(
1401
+ seed_candidate,
1402
+ search_space,
1403
+ search_paths,
1404
+ diagnoses,
1405
+ )
1406
+ elif role.proposal_kind == "explorer":
1407
+ proposals = _explorer_proposals(
1408
+ seed_candidate,
1409
+ search_space,
1410
+ search_paths,
1411
+ max_paths=radius_units,
1412
+ )
1413
+ elif role.proposal_kind == "adversary":
1414
+ proposals = _adversary_proposals(
1415
+ seed_candidate,
1416
+ ranked[:beam_width],
1417
+ search_space,
1418
+ search_paths,
1419
+ max_boundary_paths=3 * radius_units,
1420
+ )
1421
+ elif role.proposal_kind == "critic" and round_number > 1:
1422
+ proposals = (
1423
+ proposal
1424
+ for evaluation in beam
1425
+ if evaluation.candidate.patch
1426
+ for proposal in _critic_proposals(
1427
+ evaluation.candidate,
1428
+ search_space,
1429
+ search_paths,
1430
+ )
1431
+ )
1432
+ elif role.proposal_kind == "coverage_synthesis" and round_number > 1:
1433
+ proposals = _coverage_synthesis_proposals(
1434
+ changed_ranked,
1435
+ search_paths,
1436
+ reconcile=guna["sattva"] >= 0.5,
1437
+ )
1438
+ elif role.proposal_kind == "synthesizer" and round_number > 1:
1439
+ breadth = max(1, round(guna["sattva"] * beam_width))
1440
+ proposals = _synthesis_proposals(changed_ranked[:breadth], search_paths)
1441
+ elif role.proposal_kind == "steward" and round_number > 1:
1442
+ allowed = set(search_paths)
1443
+ proposals = (
1444
+ proposal
1445
+ for evaluation in changed_ranked[:beam_width]
1446
+ for proposal in _steward_proposals(evaluation.candidate, tamas=guna["tamas"])
1447
+ if not allowed or allowed & set(proposal.patch)
1448
+ )
1449
+ else:
1450
+ proposals = ()
1451
+
1452
+ return _annotate_role_graph_proposals(role, proposals)
1453
+
1454
+
1455
+ def _annotate_role_graph_proposals(
1456
+ role: AgentSocietyRole,
1457
+ proposals: Iterable[AgentSearchProposal],
1458
+ ) -> Iterable[AgentSearchProposal]:
1459
+ role_guna = _normalized_guna(role)
1460
+ role_chamber = role.chamber or _chamber_for_proposal_kind(role.proposal_kind)
1461
+ for proposal in proposals:
1462
+ metadata = {
1463
+ **dict(proposal.metadata),
1464
+ "role_kind": role.proposal_kind,
1465
+ "role_phase": role.phase,
1466
+ "role_archetype": role.archetype,
1467
+ "role_description": role.description,
1468
+ "role_path_prefixes": list(role.path_prefixes),
1469
+ "role_depends_on": list(role.depends_on),
1470
+ "role_guna": dict(role_guna),
1471
+ "role_chamber": role_chamber,
1472
+ }
1473
+ yield AgentSearchProposal(
1474
+ patch=proposal.patch,
1475
+ role=role.name,
1476
+ parent_ids=proposal.parent_ids,
1477
+ reason=f"{role.proposal_kind}:{proposal.reason}",
1478
+ metadata=metadata,
1479
+ )
1480
+
1481
+
1482
+ def _role_search_paths(
1483
+ role: AgentSocietyRole,
1484
+ search_paths: Sequence[str],
1485
+ ) -> List[str]:
1486
+ if not role.path_prefixes:
1487
+ return list(search_paths)
1488
+ return [
1489
+ path
1490
+ for path in search_paths
1491
+ if any(path == prefix or path.startswith(f"{prefix}.") for prefix in role.path_prefixes)
1492
+ ]
1493
+
1494
+
1495
+ def _interleave_proposal_streams(
1496
+ streams: Sequence[Iterable[AgentSearchProposal]],
1497
+ max_proposals: int,
1498
+ ) -> List[AgentSearchProposal]:
1499
+ proposals: List[AgentSearchProposal] = []
1500
+ seen: set[str] = set()
1501
+ iterators = [iter(stream) for stream in streams]
1502
+ active = [True for _ in iterators]
1503
+
1504
+ while len(proposals) < max_proposals and any(active):
1505
+ for index, iterator in enumerate(iterators):
1506
+ if not active[index]:
1507
+ continue
1508
+ while True:
1509
+ try:
1510
+ proposal = next(iterator)
1511
+ except StopIteration:
1512
+ active[index] = False
1513
+ break
1514
+ before = len(proposals)
1515
+ _append_proposal(proposals, seen, proposal, max_proposals)
1516
+ if len(proposals) > before:
1517
+ break
1518
+ if len(proposals) >= max_proposals:
1519
+ break
1520
+ if len(proposals) >= max_proposals:
1521
+ break
1522
+ return proposals
1523
+
1524
+
1525
+ def _specialist_proposals(
1526
+ seed_candidate: AgentCandidate,
1527
+ search_space: dict[str, List[Any]],
1528
+ search_paths: Sequence[str],
1529
+ diagnoses: Sequence[ComponentDiagnosis],
1530
+ ) -> Iterable[AgentSearchProposal]:
1531
+ target_name = seed_candidate.target_name or "the optimization target"
1532
+ for group_key, paths in _path_groups(search_paths, diagnoses).items():
1533
+ patch: dict[str, Any] = {}
1534
+ for path in paths:
1535
+ value = _first_non_seed_value(seed_candidate, search_space, path)
1536
+ if value is not _NO_VALUE:
1537
+ patch[path] = value
1538
+ if not patch:
1539
+ continue
1540
+ evidence = "; ".join(
1541
+ diagnosis.evidence
1542
+ for diagnosis in diagnoses
1543
+ if diagnosis.evidence
1544
+ and _diagnostic_group_key(sorted(patch)[0], [diagnosis])
1545
+ ) or f"component grouping over declared search paths {sorted(patch)}"
1546
+ yield AgentSearchProposal(
1547
+ patch=patch,
1548
+ role="specialist",
1549
+ parent_ids=(seed_candidate.id,),
1550
+ reason=f"apply_component_bundle:{group_key}",
1551
+ metadata={
1552
+ "justification": _justification(
1553
+ pratijna=(
1554
+ f"bundling component '{group_key}' repairs improves {target_name}"
1555
+ ),
1556
+ hetu=evidence,
1557
+ udaharana=(
1558
+ f"seed candidate {seed_candidate.id} exhibits the diagnosed "
1559
+ f"component state; rule: diagnosed components are repaired as one bundle"
1560
+ ),
1561
+ upanaya=(
1562
+ f"this candidate patches exactly the '{group_key}' paths "
1563
+ f"{sorted(patch)}"
1564
+ ),
1565
+ nigamana=(
1566
+ "expect the diagnosed-component metrics to close on the "
1567
+ "next admissible evaluation"
1568
+ ),
1569
+ )
1570
+ },
1571
+ )
1572
+
1573
+
1574
+ def _adversary_proposals(
1575
+ seed_candidate: AgentCandidate,
1576
+ ranked: Sequence[CandidateEvaluation],
1577
+ search_space: dict[str, List[Any]],
1578
+ search_paths: Sequence[str],
1579
+ max_boundary_paths: int = 3,
1580
+ ) -> Iterable[AgentSearchProposal]:
1581
+ target_name = seed_candidate.target_name or "the optimization target"
1582
+ boundary_patch: dict[str, Any] = {}
1583
+ for path in search_paths:
1584
+ value = _last_non_seed_value(seed_candidate, search_space, path)
1585
+ if value is not _NO_VALUE:
1586
+ boundary_patch[path] = value
1587
+ if len(boundary_patch) >= max_boundary_paths:
1588
+ break
1589
+ if boundary_patch:
1590
+ yield AgentSearchProposal(
1591
+ patch=boundary_patch,
1592
+ role="adversary",
1593
+ parent_ids=(seed_candidate.id,),
1594
+ reason="stress_boundary_combination",
1595
+ metadata={
1596
+ "justification": _justification(
1597
+ pratijna=(
1598
+ f"a boundary-value combination stresses {target_name} "
1599
+ "into revealing brittle settings"
1600
+ ),
1601
+ hetu=(
1602
+ "search space declares boundary values on paths "
1603
+ f"{sorted(boundary_patch)}"
1604
+ ),
1605
+ udaharana=(
1606
+ f"seed candidate {seed_candidate.id} holds interior values; "
1607
+ "rule: adversarial probes test the declared extremes"
1608
+ ),
1609
+ upanaya=(
1610
+ "this candidate combines the last non-seed value of each "
1611
+ "boundary path in one patch"
1612
+ ),
1613
+ nigamana=(
1614
+ "expect either a robustness confirmation or an admissible "
1615
+ "failure signal at the boundary"
1616
+ ),
1617
+ )
1618
+ },
1619
+ )
1620
+
1621
+ for evaluation in ranked:
1622
+ source_patch = dict(evaluation.candidate.patch)
1623
+ if not source_patch:
1624
+ continue
1625
+ for path in search_paths:
1626
+ value = _last_non_seed_value(evaluation.candidate, search_space, path)
1627
+ if value is _NO_VALUE:
1628
+ continue
1629
+ patch = {**source_patch, path: value}
1630
+ yield AgentSearchProposal(
1631
+ patch=patch,
1632
+ role="adversary",
1633
+ parent_ids=(evaluation.candidate.id,),
1634
+ reason="stress_candidate_with_boundary_change",
1635
+ metadata={
1636
+ "justification": _justification(
1637
+ pratijna=(
1638
+ f"candidate {evaluation.candidate.id} should survive a "
1639
+ f"boundary change on {path}"
1640
+ ),
1641
+ hetu=(
1642
+ f"candidate {evaluation.candidate.id} scored "
1643
+ f"{evaluation.score:.4f} with patch {sorted(source_patch)}"
1644
+ ),
1645
+ udaharana=(
1646
+ "rule: strong candidates are stress-tested with one "
1647
+ "additional boundary value before promotion"
1648
+ ),
1649
+ upanaya=(
1650
+ f"this candidate keeps the parent patch and sets {path} "
1651
+ "to its boundary value"
1652
+ ),
1653
+ nigamana=(
1654
+ "expect a measurable score delta isolating the boundary "
1655
+ "sensitivity of the parent"
1656
+ ),
1657
+ )
1658
+ },
1659
+ )
1660
+
1661
+
1662
+ def _path_groups(
1663
+ search_paths: Sequence[str],
1664
+ diagnoses: Sequence[ComponentDiagnosis],
1665
+ ) -> dict[str, List[str]]:
1666
+ groups: dict[str, List[str]] = {}
1667
+ for path in search_paths:
1668
+ group_key = _diagnostic_group_key(path, diagnoses) or path.split(".", 1)[0]
1669
+ groups.setdefault(group_key, []).append(path)
1670
+ return groups
1671
+
1672
+
1673
+ def _diagnostic_group_key(
1674
+ path: str,
1675
+ diagnoses: Sequence[ComponentDiagnosis],
1676
+ ) -> Optional[str]:
1677
+ for diagnosis in diagnoses:
1678
+ for suggested_path in diagnosis.suggested_paths:
1679
+ if path == suggested_path or path.startswith(f"{suggested_path}."):
1680
+ return f"{diagnosis.component}:{suggested_path}"
1681
+ if path == diagnosis.component or path.startswith(f"{diagnosis.component}."):
1682
+ return diagnosis.component
1683
+ return None
1684
+
1685
+
1686
+ class _NoValue:
1687
+ pass
1688
+
1689
+
1690
+ _NO_VALUE = _NoValue()
1691
+
1692
+
1693
+ def _first_non_seed_value(
1694
+ seed_candidate: AgentCandidate,
1695
+ search_space: dict[str, List[Any]],
1696
+ path: str,
1697
+ ) -> Any:
1698
+ current = seed_candidate.get_path(path)
1699
+ for value in search_space.get(path, []):
1700
+ if value != current:
1701
+ return value
1702
+ return _NO_VALUE
1703
+
1704
+
1705
+ def _last_non_seed_value(
1706
+ candidate: AgentCandidate,
1707
+ search_space: dict[str, List[Any]],
1708
+ path: str,
1709
+ ) -> Any:
1710
+ current = candidate.get_path(path)
1711
+ for value in reversed(search_space.get(path, [])):
1712
+ if value != current:
1713
+ return value
1714
+ return _NO_VALUE
1715
+
1716
+
1717
+ def _synthesis_proposals(
1718
+ evaluations: Sequence[CandidateEvaluation],
1719
+ search_paths: Sequence[str],
1720
+ ) -> Iterable[AgentSearchProposal]:
1721
+ if len(evaluations) < 2:
1722
+ return
1723
+
1724
+ allowed = set(search_paths)
1725
+ all_sources = tuple(evaluations)
1726
+ yield AgentSearchProposal(
1727
+ patch=_merge_ranked_patches(all_sources, allowed),
1728
+ role="synthesizer",
1729
+ parent_ids=tuple(item.candidate.id for item in all_sources),
1730
+ reason="combine_best_partial_candidates",
1731
+ metadata={
1732
+ "justification": _justification(
1733
+ pratijna="merging the strongest partial candidates compounds their gains",
1734
+ hetu=(
1735
+ "evaluated parents "
1736
+ f"{[item.candidate.id for item in all_sources]} each improved "
1737
+ "disjoint or compatible paths"
1738
+ ),
1739
+ udaharana=(
1740
+ "rule: compatible partial repairs are merged rank-first so the "
1741
+ "strongest parent wins conflicting paths"
1742
+ ),
1743
+ upanaya="this candidate is the rank-first merge of every parent patch",
1744
+ nigamana=(
1745
+ "expect a combined score at or above the best parent on the "
1746
+ "next admissible evaluation"
1747
+ ),
1748
+ )
1749
+ },
1750
+ )
1751
+ for left, right in combinations(evaluations, 2):
1752
+ yield AgentSearchProposal(
1753
+ patch=_merge_ranked_patches((left, right), allowed),
1754
+ role="synthesizer",
1755
+ parent_ids=(left.candidate.id, right.candidate.id),
1756
+ reason="combine_pairwise_partial_candidates",
1757
+ metadata={
1758
+ "justification": _justification(
1759
+ pratijna=(
1760
+ f"the pair {left.candidate.id} + {right.candidate.id} "
1761
+ "combines compatible repairs"
1762
+ ),
1763
+ hetu=(
1764
+ f"parents scored {left.score:.4f} and {right.score:.4f} on "
1765
+ "admissible evaluations"
1766
+ ),
1767
+ udaharana=(
1768
+ "rule: pairwise merges isolate which parent combination "
1769
+ "carries the gain"
1770
+ ),
1771
+ upanaya="this candidate merges exactly the two parent patches",
1772
+ nigamana=(
1773
+ "expect the pairwise merge to attribute the combined gain "
1774
+ "on the next admissible evaluation"
1775
+ ),
1776
+ )
1777
+ },
1778
+ )
1779
+
1780
+
1781
+ def _coverage_synthesis_proposals(
1782
+ evaluations: Sequence[CandidateEvaluation],
1783
+ search_paths: Sequence[str],
1784
+ *,
1785
+ reconcile: bool = True,
1786
+ ) -> Iterable[AgentSearchProposal]:
1787
+ if not evaluations:
1788
+ return
1789
+
1790
+ allowed = set(search_paths)
1791
+ patch: dict[str, Any] = {}
1792
+ parent_ids: List[str] = []
1793
+ for path in search_paths:
1794
+ path_evaluations = [
1795
+ evaluation
1796
+ for evaluation in evaluations
1797
+ if path in evaluation.candidate.patch and path in allowed
1798
+ ]
1799
+ if not path_evaluations:
1800
+ continue
1801
+ if not reconcile:
1802
+ # Low-sattva synthesis skips conflicting paths instead of
1803
+ # reconciling them (deterministic guna mapping; default-archetype
1804
+ # sattva >= 0.5 keeps the legacy reconciliation).
1805
+ distinct_values = {
1806
+ json.dumps(
1807
+ evaluation.candidate.patch[path], sort_keys=True, default=str
1808
+ )
1809
+ for evaluation in path_evaluations
1810
+ }
1811
+ if len(distinct_values) > 1:
1812
+ continue
1813
+ selected = max(
1814
+ path_evaluations,
1815
+ key=lambda item: (
1816
+ item.score,
1817
+ -len(item.candidate.patch),
1818
+ item.candidate.id,
1819
+ ),
1820
+ )
1821
+ patch[path] = selected.candidate.patch[path]
1822
+ parent_ids.append(selected.candidate.id)
1823
+
1824
+ if patch:
1825
+ yield AgentSearchProposal(
1826
+ patch=patch,
1827
+ role="synthesizer",
1828
+ parent_ids=tuple(dict.fromkeys(parent_ids)),
1829
+ reason="combine_best_path_representatives",
1830
+ metadata={
1831
+ "justification": _justification(
1832
+ pratijna=(
1833
+ "selecting the best representative per path covers the "
1834
+ "whole repaired surface"
1835
+ ),
1836
+ hetu=(
1837
+ f"per-path winners {sorted(set(parent_ids))} carry the "
1838
+ "highest admissible score for their path"
1839
+ ),
1840
+ udaharana=(
1841
+ "rule: coverage synthesis promotes each path's best "
1842
+ "evidence-backed value"
1843
+ ),
1844
+ upanaya=(
1845
+ f"this candidate sets {sorted(patch)} to their per-path "
1846
+ "winning values"
1847
+ ),
1848
+ nigamana=(
1849
+ "expect coverage of every repaired path without losing "
1850
+ "any single-path gain"
1851
+ ),
1852
+ )
1853
+ },
1854
+ )
1855
+
1856
+
1857
+ def _critic_proposals(
1858
+ source_candidate: AgentCandidate,
1859
+ search_space: dict[str, List[Any]],
1860
+ search_paths: Sequence[str],
1861
+ ) -> Iterable[AgentSearchProposal]:
1862
+ source_patch = dict(source_candidate.patch)
1863
+ for path in search_paths:
1864
+ for value in search_space.get(path, []):
1865
+ if source_candidate.get_path(path) == value:
1866
+ continue
1867
+ patch = {**source_patch, path: value}
1868
+ yield AgentSearchProposal(
1869
+ patch=patch,
1870
+ role="critic",
1871
+ parent_ids=(source_candidate.id,),
1872
+ reason="test_next_change_against_current_candidate",
1873
+ metadata={
1874
+ "justification": _justification(
1875
+ pratijna=(
1876
+ f"candidate {source_candidate.id} improves further with "
1877
+ f"{path} changed"
1878
+ ),
1879
+ hetu=(
1880
+ f"parent patch {sorted(source_patch)} passed an "
1881
+ "admissible evaluation and the search space declares "
1882
+ f"another value for {path}"
1883
+ ),
1884
+ udaharana=(
1885
+ "rule: critics test exactly one more change against the "
1886
+ "current strong candidate"
1887
+ ),
1888
+ upanaya=(
1889
+ f"this candidate keeps the parent patch and sets {path} "
1890
+ "to the next declared value"
1891
+ ),
1892
+ nigamana=(
1893
+ f"expect the evaluation to confirm or refute {path} as "
1894
+ "the next improving change"
1895
+ ),
1896
+ )
1897
+ },
1898
+ )
1899
+
1900
+
1901
+ def _explorer_proposals(
1902
+ seed_candidate: AgentCandidate,
1903
+ search_space: dict[str, List[Any]],
1904
+ search_paths: Sequence[str],
1905
+ max_paths: int = 1,
1906
+ ) -> Iterable[AgentSearchProposal]:
1907
+ target_name = seed_candidate.target_name or "the optimization target"
1908
+ for path in search_paths:
1909
+ for value in search_space.get(path, []):
1910
+ if seed_candidate.get_path(path) == value:
1911
+ continue
1912
+ yield AgentSearchProposal(
1913
+ patch={path: value},
1914
+ role="explorer",
1915
+ parent_ids=(seed_candidate.id,),
1916
+ reason="isolate_single_path_effect",
1917
+ metadata={
1918
+ "justification": _justification(
1919
+ pratijna=f"setting {path} improves {target_name}",
1920
+ hetu=(
1921
+ "the declared search space lists an untested value "
1922
+ f"for {path}"
1923
+ ),
1924
+ udaharana=(
1925
+ f"seed candidate {seed_candidate.id} holds "
1926
+ f"{seed_candidate.get_path(path)!r} on {path}; rule: "
1927
+ "isolated single-path probes attribute metric deltas"
1928
+ ),
1929
+ upanaya=(
1930
+ f"this candidate patches only {path}, so any score "
1931
+ "delta is attributable to it"
1932
+ ),
1933
+ nigamana=(
1934
+ f"expect an admissible evaluation-score delta for {path}"
1935
+ ),
1936
+ )
1937
+ },
1938
+ )
1939
+ if max_paths > 1:
1940
+ # Rajas-widened exploration: deterministic sliding windows of adjacent
1941
+ # admissible paths, each set to its first non-seed value. max_paths == 1
1942
+ # (the default radius for every default-archetype triple) skips this
1943
+ # block entirely, preserving legacy proposals byte-for-byte.
1944
+ paths = list(search_paths)
1945
+ for start in range(len(paths)):
1946
+ window = paths[start : start + max_paths]
1947
+ if len(window) < 2:
1948
+ continue
1949
+ patch: dict[str, Any] = {}
1950
+ for path in window:
1951
+ value = _first_non_seed_value(seed_candidate, search_space, path)
1952
+ if value is not _NO_VALUE:
1953
+ patch[path] = value
1954
+ if len(patch) < 2:
1955
+ continue
1956
+ yield AgentSearchProposal(
1957
+ patch=patch,
1958
+ role="explorer",
1959
+ parent_ids=(seed_candidate.id,),
1960
+ reason="explore_adjacent_path_window",
1961
+ metadata={
1962
+ "justification": _justification(
1963
+ pratijna=(
1964
+ f"jointly setting {sorted(patch)} improves {target_name}"
1965
+ ),
1966
+ hetu=(
1967
+ "high-rajas exploration widens the mutation radius over "
1968
+ "adjacent admissible paths"
1969
+ ),
1970
+ udaharana=(
1971
+ f"seed candidate {seed_candidate.id} holds the seed "
1972
+ "values; rule: widened probes test interacting paths "
1973
+ "together"
1974
+ ),
1975
+ upanaya=(
1976
+ f"this candidate patches the adjacent window {sorted(patch)}"
1977
+ ),
1978
+ nigamana=(
1979
+ "expect an admissible evaluation delta attributable to "
1980
+ "the window"
1981
+ ),
1982
+ )
1983
+ },
1984
+ )
1985
+
1986
+
1987
+ def _steward_proposals(
1988
+ source_candidate: AgentCandidate,
1989
+ *,
1990
+ tamas: Optional[float] = None,
1991
+ ) -> Iterable[AgentSearchProposal]:
1992
+ if len(source_candidate.patch) < 2:
1993
+ return
1994
+ patch_paths = list(source_candidate.patch)
1995
+ if tamas is None:
1996
+ removal_limit = len(patch_paths)
1997
+ else:
1998
+ # Tamas mapping: removal attempts per round scale with the steward's
1999
+ # tamas (ceil keeps every default-archetype triple at full coverage
2000
+ # for the patch sizes the deterministic fixtures use).
2001
+ removal_limit = max(1, math.ceil(float(tamas) * len(patch_paths)))
2002
+ for path in patch_paths[:removal_limit]:
2003
+ patch = {
2004
+ key: value
2005
+ for key, value in source_candidate.patch.items()
2006
+ if key != path
2007
+ }
2008
+ yield AgentSearchProposal(
2009
+ patch=patch,
2010
+ role="steward",
2011
+ parent_ids=(source_candidate.id,),
2012
+ reason="remove_one_change_to_check_minimality",
2013
+ metadata={
2014
+ "justification": _justification(
2015
+ pratijna=(
2016
+ f"candidate {source_candidate.id} keeps its score without "
2017
+ f"the change on {path}"
2018
+ ),
2019
+ hetu=(
2020
+ f"parent patch {sorted(source_candidate.patch)} passed an "
2021
+ "admissible evaluation with multiple combined changes"
2022
+ ),
2023
+ udaharana=(
2024
+ "rule: stewards remove one change at a time so only "
2025
+ "metric-proven repairs survive"
2026
+ ),
2027
+ upanaya=(
2028
+ f"this candidate is the parent patch minus {path} and "
2029
+ "nothing else"
2030
+ ),
2031
+ nigamana=(
2032
+ f"expect an equal score if {path} was unnecessary, or a "
2033
+ "regression proving it was load-bearing"
2034
+ ),
2035
+ )
2036
+ },
2037
+ )
2038
+
2039
+
2040
+ def _merge_ranked_patches(
2041
+ evaluations: Sequence[CandidateEvaluation],
2042
+ allowed_paths: set[str],
2043
+ ) -> dict[str, Any]:
2044
+ patch: dict[str, Any] = {}
2045
+ for evaluation in evaluations:
2046
+ for path, value in evaluation.candidate.patch.items():
2047
+ if path in allowed_paths and path not in patch:
2048
+ patch[path] = value
2049
+ return patch
2050
+
2051
+
2052
+ def _append_proposal(
2053
+ proposals: List[AgentSearchProposal],
2054
+ seen: set[str],
2055
+ proposal: AgentSearchProposal,
2056
+ max_proposals: int,
2057
+ ) -> None:
2058
+ if len(proposals) >= max_proposals or not proposal.patch:
2059
+ return
2060
+ key = _canonical_patch(proposal.patch)
2061
+ if key in seen:
2062
+ return
2063
+ seen.add(key)
2064
+ proposals.append(proposal)
2065
+
2066
+
2067
+ def _candidate_id_for_patch(
2068
+ seed_candidate: AgentCandidate,
2069
+ patch: dict[str, Any],
2070
+ ) -> str:
2071
+ return seed_candidate.with_patch(patch).id
2072
+
2073
+
2074
+ def _canonical_patch(patch: dict[str, Any]) -> str:
2075
+ return json.dumps(patch, sort_keys=True, default=str)