foxclaw 0.1.0 → 0.1.3

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 (514) hide show
  1. package/README.md +54 -29
  2. package/bin/foxclaw.cjs +51 -0
  3. package/index.js +4 -10
  4. package/package.json +46 -38
  5. package/python/.dockerignore +35 -0
  6. package/python/.env.example +173 -0
  7. package/python/.gitattributes +46 -0
  8. package/python/.github/workflows/ci.yml +167 -0
  9. package/python/.github/workflows/release.yml +183 -0
  10. package/python/CHANGELOG.md +57 -0
  11. package/python/CHANGELOG_CN.md +57 -0
  12. package/python/Dockerfile +64 -0
  13. package/python/LICENSE +21 -0
  14. package/python/MANIFEST.in +14 -0
  15. package/python/README.md +201 -0
  16. package/python/README_CN.md +336 -0
  17. package/python/bench/bm25_audit.py +186 -0
  18. package/python/bench/bm25_regression.py +177 -0
  19. package/python/bench/bm25_stress.py +155 -0
  20. package/python/bench/cd_audit.py +215 -0
  21. package/python/bench/chaos.py +140 -0
  22. package/python/bench/concurrency.py +103 -0
  23. package/python/bench/e2e_real_models.py +94 -0
  24. package/python/bench/llm_retry_test.py +241 -0
  25. package/python/bench/load_test.py +193 -0
  26. package/python/bench/vector_ivf_upgrade.py +128 -0
  27. package/python/docs/CHANNELS.md +195 -0
  28. package/python/docs/GOVERNANCE.md +164 -0
  29. package/python/docs/RELEASE.md +110 -0
  30. package/python/docs/RUN.md +75 -0
  31. package/python/docs/RUN_CN.md +121 -0
  32. package/python/docs//351/205/215/347/275/256/346/214/207/345/215/227.md +152 -0
  33. package/python/evals/__init__.py +0 -0
  34. package/python/evals/governance/README.md +61 -0
  35. package/python/evals/governance/__init__.py +0 -0
  36. package/python/evals/governance/cases.py +115 -0
  37. package/python/evals/governance/compare_embeddings.py +94 -0
  38. package/python/evals/governance/convergence.svg +11 -0
  39. package/python/evals/governance/report.json +371 -0
  40. package/python/evals/governance/run_evals.py +368 -0
  41. package/python/examples/governance_middleware_demo.py +72 -0
  42. package/python/examples/langgraph_governance_demo.py +104 -0
  43. package/python/examples/sample_itinerary.csv +6 -0
  44. package/python/examples/sample_knowledge.csv +9 -0
  45. package/python/examples/sample_transactions.csv +14 -0
  46. package/python/foxclaw/__init__.py +408 -0
  47. package/python/foxclaw/agents/__init__.py +12 -0
  48. package/python/foxclaw/agents/agents.py +35 -0
  49. package/python/foxclaw/agents/base.py +794 -0
  50. package/python/foxclaw/agents/impl/__init__.py +1 -0
  51. package/python/foxclaw/agents/impl/_helpers.py +70 -0
  52. package/python/foxclaw/agents/impl/finance.py +231 -0
  53. package/python/foxclaw/agents/impl/office.py +350 -0
  54. package/python/foxclaw/agents/impl/supervisor.py +592 -0
  55. package/python/foxclaw/agents/mixins/_context.py +128 -0
  56. package/python/foxclaw/agents/mixins/augment.py +143 -0
  57. package/python/foxclaw/agents/mixins/llm.py +67 -0
  58. package/python/foxclaw/agents/mixins/memory.py +41 -0
  59. package/python/foxclaw/agents/mixins/preference.py +131 -0
  60. package/python/foxclaw/agents/mixins/prompt.py +71 -0
  61. package/python/foxclaw/agents/mixins/skillbox.py +58 -0
  62. package/python/foxclaw/agents/mixins/tool_exec.py +417 -0
  63. package/python/foxclaw/api/__init__.py +224 -0
  64. package/python/foxclaw/api/_state.py +43 -0
  65. package/python/foxclaw/api/bootstrap.py +96 -0
  66. package/python/foxclaw/api/middleware.py +294 -0
  67. package/python/foxclaw/api/routes/ahs.py +404 -0
  68. package/python/foxclaw/api/routes/attachments.py +116 -0
  69. package/python/foxclaw/api/routes/browser.py +35 -0
  70. package/python/foxclaw/api/routes/domain.py +306 -0
  71. package/python/foxclaw/api/routes/knowledge.py +145 -0
  72. package/python/foxclaw/api/routes/llm_admin.py +449 -0
  73. package/python/foxclaw/api/routes/memory.py +105 -0
  74. package/python/foxclaw/api/routes/orchestration.py +39 -0
  75. package/python/foxclaw/api/routes/recovery.py +44 -0
  76. package/python/foxclaw/api/routes/review.py +266 -0
  77. package/python/foxclaw/api/routes/search.py +33 -0
  78. package/python/foxclaw/api/routes/system.py +311 -0
  79. package/python/foxclaw/api/routes/task.py +263 -0
  80. package/python/foxclaw/api/routes/ws.py +64 -0
  81. package/python/foxclaw/api/spine.py +52 -0
  82. package/python/foxclaw/browser/__init__.py +13 -0
  83. package/python/foxclaw/browser/action/__init__.py +1 -0
  84. package/python/foxclaw/browser/action/action_executor.py +50 -0
  85. package/python/foxclaw/browser/action/action_types.py +44 -0
  86. package/python/foxclaw/browser/action/action_verifier.py +68 -0
  87. package/python/foxclaw/browser/action/url_policy.py +143 -0
  88. package/python/foxclaw/browser/adapter/__init__.py +1 -0
  89. package/python/foxclaw/browser/adapter/base_adapter.py +23 -0
  90. package/python/foxclaw/browser/adapter/web_adapter.py +46 -0
  91. package/python/foxclaw/browser/driver/__init__.py +1 -0
  92. package/python/foxclaw/browser/driver/base_driver.py +61 -0
  93. package/python/foxclaw/browser/driver/playwright_driver.py +170 -0
  94. package/python/foxclaw/browser/perception/__init__.py +1 -0
  95. package/python/foxclaw/browser/perception/dom_extractor.py +42 -0
  96. package/python/foxclaw/browser/perception/element_indexer.py +56 -0
  97. package/python/foxclaw/browser/perception/semantic_reducer.py +33 -0
  98. package/python/foxclaw/browser/perception/snapshot.py +29 -0
  99. package/python/foxclaw/browser/service.py +144 -0
  100. package/python/foxclaw/browser/session/__init__.py +1 -0
  101. package/python/foxclaw/browser/session/session_manager.py +57 -0
  102. package/python/foxclaw/capability/__init__.py +12 -0
  103. package/python/foxclaw/capability/embedding_service.py +65 -0
  104. package/python/foxclaw/channels/__init__.py +138 -0
  105. package/python/foxclaw/channels/discord.py +175 -0
  106. package/python/foxclaw/channels/dispatcher.py +179 -0
  107. package/python/foxclaw/channels/feishu.py +215 -0
  108. package/python/foxclaw/channels/openclaw_bridge.py +151 -0
  109. package/python/foxclaw/channels/signing.py +187 -0
  110. package/python/foxclaw/channels/telegram.py +120 -0
  111. package/python/foxclaw/channels/weixin.py +215 -0
  112. package/python/foxclaw/cli.py +183 -0
  113. package/python/foxclaw/cognitive/__init__.py +4 -0
  114. package/python/foxclaw/cognitive/reasoner.py +81 -0
  115. package/python/foxclaw/cognitive/state.py +141 -0
  116. package/python/foxclaw/core/__init__.py +9 -0
  117. package/python/foxclaw/core/audit_chain.py +131 -0
  118. package/python/foxclaw/core/audit_sink.py +100 -0
  119. package/python/foxclaw/core/compaction.py +265 -0
  120. package/python/foxclaw/core/config.py +65 -0
  121. package/python/foxclaw/core/confirm_nonce.py +151 -0
  122. package/python/foxclaw/core/dispatch.py +27 -0
  123. package/python/foxclaw/core/envfile.py +48 -0
  124. package/python/foxclaw/core/failure_policy.py +142 -0
  125. package/python/foxclaw/core/gateway.py +81 -0
  126. package/python/foxclaw/core/governance_audit.py +171 -0
  127. package/python/foxclaw/core/idempotency.py +282 -0
  128. package/python/foxclaw/core/injection_guard.py +202 -0
  129. package/python/foxclaw/core/memory_adapter.py +489 -0
  130. package/python/foxclaw/core/metrics.py +88 -0
  131. package/python/foxclaw/core/model_router.py +985 -0
  132. package/python/foxclaw/core/paths.py +103 -0
  133. package/python/foxclaw/core/recovery.py +349 -0
  134. package/python/foxclaw/core/scheduler.py +141 -0
  135. package/python/foxclaw/core/secrets.py +131 -0
  136. package/python/foxclaw/core/sessions.py +155 -0
  137. package/python/foxclaw/core/settings.py +414 -0
  138. package/python/foxclaw/core/shared_write.py +186 -0
  139. package/python/foxclaw/core/steering_queue.py +154 -0
  140. package/python/foxclaw/core/system_bus.py +84 -0
  141. package/python/foxclaw/core/thought_chain.py +316 -0
  142. package/python/foxclaw/core/token_meter.py +231 -0
  143. package/python/foxclaw/core/tools.py +164 -0
  144. package/python/foxclaw/core/tracing.py +375 -0
  145. package/python/foxclaw/core/truth_pipeline.py +110 -0
  146. package/python/foxclaw/core/update_check.py +149 -0
  147. package/python/foxclaw/core/vec_recall_adapter.py +477 -0
  148. package/python/foxclaw/core/verify.py +173 -0
  149. package/python/foxclaw/core/web_search.py +218 -0
  150. package/python/foxclaw/finance/__init__.py +15 -0
  151. package/python/foxclaw/finance/evidence.py +149 -0
  152. package/python/foxclaw/finance/fx.py +98 -0
  153. package/python/foxclaw/finance/graph.py +179 -0
  154. package/python/foxclaw/finance/models.py +146 -0
  155. package/python/foxclaw/finance/nlparse.py +515 -0
  156. package/python/foxclaw/finance/policy.py +301 -0
  157. package/python/foxclaw/finance/reasoner.py +121 -0
  158. package/python/foxclaw/finance/reasoning.py +319 -0
  159. package/python/foxclaw/finance/reconcile.py +175 -0
  160. package/python/foxclaw/finance/runtime.py +500 -0
  161. package/python/foxclaw/governance/__init__.py +24 -0
  162. package/python/foxclaw/governance/middleware.py +176 -0
  163. package/python/foxclaw/harness/__init__.py +17 -0
  164. package/python/foxclaw/harness/_base.py +66 -0
  165. package/python/foxclaw/harness/_policy.py +340 -0
  166. package/python/foxclaw/harness/drift.py +77 -0
  167. package/python/foxclaw/harness/event_bus.py +28 -0
  168. package/python/foxclaw/harness/harness.py +358 -0
  169. package/python/foxclaw/harness/loop_detection.py +145 -0
  170. package/python/foxclaw/itinerary/__init__.py +10 -0
  171. package/python/foxclaw/itinerary/amap_client.py +114 -0
  172. package/python/foxclaw/itinerary/models.py +80 -0
  173. package/python/foxclaw/itinerary/reasoner.py +96 -0
  174. package/python/foxclaw/itinerary/reasoning.py +168 -0
  175. package/python/foxclaw/itinerary/runtime.py +144 -0
  176. package/python/foxclaw/kernel/__init__.py +13 -0
  177. package/python/foxclaw/kernel/capabilities.py +72 -0
  178. package/python/foxclaw/kernel/policy_gate.py +108 -0
  179. package/python/foxclaw/kernel/process_manager.py +186 -0
  180. package/python/foxclaw/kernel/scheduler.py +125 -0
  181. package/python/foxclaw/kernel/sentinel.py +443 -0
  182. package/python/foxclaw/kernel/sentinel_client.py +243 -0
  183. package/python/foxclaw/kernel/spine.py +97 -0
  184. package/python/foxclaw/knowledge/__init__.py +360 -0
  185. package/python/foxclaw/knowledge/truth_crystallizer.py +89 -0
  186. package/python/foxclaw/learning/__init__.py +12 -0
  187. package/python/foxclaw/learning/knowledge_graph.py +144 -0
  188. package/python/foxclaw/learning/models.py +93 -0
  189. package/python/foxclaw/learning/reasoner.py +67 -0
  190. package/python/foxclaw/learning/runtime.py +351 -0
  191. package/python/foxclaw/learning/spaced_repetition.py +91 -0
  192. package/python/foxclaw/mcp_server.py +188 -0
  193. package/python/foxclaw/orchestration/__init__.py +167 -0
  194. package/python/foxclaw/pipeline/__init__.py +23 -0
  195. package/python/foxclaw/pipeline/cognitive_pipeline.py +429 -0
  196. package/python/foxclaw/pipeline/stage_meta.py +21 -0
  197. package/python/foxclaw/pipeline/supervisor_stages.py +201 -0
  198. package/python/foxclaw/planning/__init__.py +17 -0
  199. package/python/foxclaw/planning/llm_planner.py +232 -0
  200. package/python/foxclaw/planning/planner.py +94 -0
  201. package/python/foxclaw/search/__init__.py +24 -0
  202. package/python/foxclaw/search/agent_port.py +190 -0
  203. package/python/foxclaw/search/evidence.py +109 -0
  204. package/python/foxclaw/search/external_search.py +50 -0
  205. package/python/foxclaw/search/hkc_read_port.py +60 -0
  206. package/python/foxclaw/search/internal_search.py +71 -0
  207. package/python/foxclaw/search/models.py +127 -0
  208. package/python/foxclaw/search/planner.py +44 -0
  209. package/python/foxclaw/search/retriever.py +39 -0
  210. package/python/foxclaw/search/runtime.py +127 -0
  211. package/python/foxclaw/search/strategy.py +44 -0
  212. package/python/foxclaw/skills/__init__.py +559 -0
  213. package/python/foxclaw/skills/archive.py +111 -0
  214. package/python/foxclaw/skills/fetch.py +226 -0
  215. package/python/foxclaw/skills/hooks_spec.py +69 -0
  216. package/python/foxclaw/skills/install_cmd.py +216 -0
  217. package/python/foxclaw/skills/lifecycle.py +252 -0
  218. package/python/foxclaw/skills/markdown_skill.py +175 -0
  219. package/python/foxclaw/skills/markdown_store.py +39 -0
  220. package/python/foxclaw/skills/mcp_client.py +184 -0
  221. package/python/foxclaw/skills/mcp_store.py +40 -0
  222. package/python/foxclaw/skills/permission.py +145 -0
  223. package/python/foxclaw/skills/pipeline.py +354 -0
  224. package/python/foxclaw/skills/policy.py +74 -0
  225. package/python/foxclaw/skills/review.py +72 -0
  226. package/python/foxclaw/skills/store.py +57 -0
  227. package/python/frontend/assets/core_bg.jpg +0 -0
  228. package/python/frontend/deck/auth.js +25 -0
  229. package/python/frontend/deck/chat_model_picker.js +483 -0
  230. package/python/frontend/deck/domain.js +276 -0
  231. package/python/frontend/deck/main.js +1806 -0
  232. package/python/frontend/deck/markdown.js +44 -0
  233. package/python/frontend/deck/settings.js +307 -0
  234. package/python/frontend/deck/state.js +5 -0
  235. package/python/frontend/deck/util.js +4 -0
  236. package/python/frontend/deck/visuals.js +190 -0
  237. package/python/frontend/deck.css +855 -0
  238. package/python/frontend/deck.html +686 -0
  239. package/python/frontend/deck_v2.html +341 -0
  240. package/python/frontend/planet/index.html +266 -0
  241. package/python/frontend/planet/planet.css +579 -0
  242. package/python/frontend/planet/planet.js +1436 -0
  243. package/python/frontend/planet/vendor/three.core.min.js +6 -0
  244. package/python/frontend/planet/vendor/three.module.min.js +6 -0
  245. package/python/frontend/v3/assets/index-CQSBdxk4.js +34 -0
  246. package/python/frontend/v3/assets/index-D5nIsjms.css +1 -0
  247. package/python/frontend/v3/index.html +15 -0
  248. package/python/frontend/widget_runtime.js +252 -0
  249. package/python/frontend/widgets.js +639 -0
  250. package/python/frontend_src/index.html +14 -0
  251. package/python/frontend_src/package-lock.json +1376 -0
  252. package/python/frontend_src/package.json +19 -0
  253. package/python/frontend_src/src/App.vue +58 -0
  254. package/python/frontend_src/src/api.js +82 -0
  255. package/python/frontend_src/src/components/ChatDock.vue +365 -0
  256. package/python/frontend_src/src/components/ComposerMenu.vue +75 -0
  257. package/python/frontend_src/src/components/DocViewer.vue +162 -0
  258. package/python/frontend_src/src/components/Inspector.vue +235 -0
  259. package/python/frontend_src/src/components/LeftRail.vue +415 -0
  260. package/python/frontend_src/src/components/MenuPopover.vue +128 -0
  261. package/python/frontend_src/src/components/MessageStream.vue +389 -0
  262. package/python/frontend_src/src/components/ModelPicker.vue +350 -0
  263. package/python/frontend_src/src/components/QuickIngest.vue +112 -0
  264. package/python/frontend_src/src/components/Stage.vue +272 -0
  265. package/python/frontend_src/src/components/ToolMenu.vue +141 -0
  266. package/python/frontend_src/src/components/TopBar.vue +118 -0
  267. package/python/frontend_src/src/components/panels/ActivityPanel.vue +82 -0
  268. package/python/frontend_src/src/components/panels/AgentsPanel.vue +83 -0
  269. package/python/frontend_src/src/components/panels/KnowledgePanel.vue +425 -0
  270. package/python/frontend_src/src/components/panels/MemoryPanel.vue +117 -0
  271. package/python/frontend_src/src/components/panels/PanelBox.vue +50 -0
  272. package/python/frontend_src/src/components/panels/ReviewPanel.vue +292 -0
  273. package/python/frontend_src/src/components/panels/SettingsPanel.vue +358 -0
  274. package/python/frontend_src/src/components/panels/TasksPanel.vue +73 -0
  275. package/python/frontend_src/src/components/panels/ToolsPanel.vue +746 -0
  276. package/python/frontend_src/src/components/panels/UsagePanel.vue +105 -0
  277. package/python/frontend_src/src/lib/fmt.js +44 -0
  278. package/python/frontend_src/src/lib/i18n.js +53 -0
  279. package/python/frontend_src/src/lib/locales/en.js +173 -0
  280. package/python/frontend_src/src/lib/locales/zh.js +174 -0
  281. package/python/frontend_src/src/lib/md.js +190 -0
  282. package/python/frontend_src/src/main.js +9 -0
  283. package/python/frontend_src/src/stores/app.js +91 -0
  284. package/python/frontend_src/src/stores/chat.js +450 -0
  285. package/python/frontend_src/src/stores/model.js +96 -0
  286. package/python/frontend_src/src/stores/panels.js +289 -0
  287. package/python/frontend_src/src/stores/profile.js +31 -0
  288. package/python/frontend_src/src/stores/system.js +25 -0
  289. package/python/frontend_src/src/styles/base.css +138 -0
  290. package/python/frontend_src/src/styles/panel.css +117 -0
  291. package/python/frontend_src/src/styles/tokens.css +78 -0
  292. package/python/frontend_src/vite.config.js +39 -0
  293. package/python/pyproject.toml +268 -0
  294. package/python/requirements.txt +311 -0
  295. package/python/skills/README.md +123 -0
  296. package/python/skills/execution-contract/SKILL.md +30 -0
  297. package/python/skills/general-assistant/SKILL.md +34 -0
  298. package/python/skills/planner/SKILL.md +33 -0
  299. package/python/skills/schedule-itinerary/SKILL.md +35 -0
  300. package/python/skills/skill-composition/SKILL.md +31 -0
  301. package/python/skills/skill-guard/SKILL.md +67 -0
  302. package/python/skills/skill-memory/SKILL.md +50 -0
  303. package/python/skills/skill-router/SKILL.md +35 -0
  304. package/python/tests/conftest.py +54 -0
  305. package/python/tests/run_all.py +200 -0
  306. package/python/tests/test_agent_lifecycle.py +326 -0
  307. package/python/tests/test_ahs_p2.py +393 -0
  308. package/python/tests/test_anthropic_tools.py +192 -0
  309. package/python/tests/test_api.py +97 -0
  310. package/python/tests/test_api_auth.py +79 -0
  311. package/python/tests/test_api_host_guard.py +69 -0
  312. package/python/tests/test_attachments.py +151 -0
  313. package/python/tests/test_audit_chain.py +116 -0
  314. package/python/tests/test_audit_sink.py +130 -0
  315. package/python/tests/test_browser.py +149 -0
  316. package/python/tests/test_browser_request_guard.py +180 -0
  317. package/python/tests/test_capabilities.py +152 -0
  318. package/python/tests/test_channel_signing.py +185 -0
  319. package/python/tests/test_channel_trust.py +109 -0
  320. package/python/tests/test_channels.py +174 -0
  321. package/python/tests/test_cli.py +46 -0
  322. package/python/tests/test_compaction_toolpair.py +119 -0
  323. package/python/tests/test_config_layering.py +61 -0
  324. package/python/tests/test_confirm_nonce.py +186 -0
  325. package/python/tests/test_context_compaction_wiring.py +124 -0
  326. package/python/tests/test_dispatch.py +89 -0
  327. package/python/tests/test_domain_evals.py +334 -0
  328. package/python/tests/test_domain_import.py +72 -0
  329. package/python/tests/test_domain_reasoners.py +134 -0
  330. package/python/tests/test_e2e.py +161 -0
  331. package/python/tests/test_emb_cache.py +97 -0
  332. package/python/tests/test_embedding_singleton.py +87 -0
  333. package/python/tests/test_envfile.py +83 -0
  334. package/python/tests/test_evals_smoke.py +90 -0
  335. package/python/tests/test_event_spine.py +174 -0
  336. package/python/tests/test_evidence_semantic.py +88 -0
  337. package/python/tests/test_evidence_sse.py +94 -0
  338. package/python/tests/test_file_ingest.py +135 -0
  339. package/python/tests/test_finance.py +144 -0
  340. package/python/tests/test_finance_ledger.py +245 -0
  341. package/python/tests/test_finance_persist.py +174 -0
  342. package/python/tests/test_gateway.py +125 -0
  343. package/python/tests/test_governance.py +421 -0
  344. package/python/tests/test_governance_audit.py +324 -0
  345. package/python/tests/test_governance_middleware.py +137 -0
  346. package/python/tests/test_hkc.py +268 -0
  347. package/python/tests/test_hkc_index_build_manager.py +306 -0
  348. package/python/tests/test_hkc_manifest_reuse.py +294 -0
  349. package/python/tests/test_hkc_search_event_sync.py +482 -0
  350. package/python/tests/test_import_path_guard.py +155 -0
  351. package/python/tests/test_injection_guard.py +193 -0
  352. package/python/tests/test_integration.py +117 -0
  353. package/python/tests/test_itinerary.py +100 -0
  354. package/python/tests/test_kee_semantic.py +84 -0
  355. package/python/tests/test_kernel.py +212 -0
  356. package/python/tests/test_learning.py +101 -0
  357. package/python/tests/test_llm_admin.py +145 -0
  358. package/python/tests/test_llm_planner.py +344 -0
  359. package/python/tests/test_markdown_skills.py +62 -0
  360. package/python/tests/test_markdown_skills_openclaw.py +66 -0
  361. package/python/tests/test_mcp_connectors.py +97 -0
  362. package/python/tests/test_mcp_guardian_discovery.py +87 -0
  363. package/python/tests/test_mcp_no_memory.py +43 -0
  364. package/python/tests/test_mcp_policy_gate.py +76 -0
  365. package/python/tests/test_mcp_server.py +92 -0
  366. package/python/tests/test_memory_recall.py +92 -0
  367. package/python/tests/test_metrics.py +58 -0
  368. package/python/tests/test_multi_chain.py +90 -0
  369. package/python/tests/test_multimodal.py +122 -0
  370. package/python/tests/test_negotiation.py +136 -0
  371. package/python/tests/test_orchestration.py +109 -0
  372. package/python/tests/test_orchestration_decoupled.py +103 -0
  373. package/python/tests/test_p3_p5_hardening.py +324 -0
  374. package/python/tests/test_parallel_dispatch.py +207 -0
  375. package/python/tests/test_pipeline.py +492 -0
  376. package/python/tests/test_plan_visible.py +86 -0
  377. package/python/tests/test_planner.py +88 -0
  378. package/python/tests/test_policy_gate.py +108 -0
  379. package/python/tests/test_profile.py +67 -0
  380. package/python/tests/test_readme_claims.py +163 -0
  381. package/python/tests/test_reasoner.py +170 -0
  382. package/python/tests/test_release_check.py +252 -0
  383. package/python/tests/test_resume_checkpoint.py +148 -0
  384. package/python/tests/test_routing.py +85 -0
  385. package/python/tests/test_scheduler.py +138 -0
  386. package/python/tests/test_search.py +194 -0
  387. package/python/tests/test_secrets.py +153 -0
  388. package/python/tests/test_semantic.py +69 -0
  389. package/python/tests/test_sessions.py +90 -0
  390. package/python/tests/test_settings.py +265 -0
  391. package/python/tests/test_shadow_learning.py +290 -0
  392. package/python/tests/test_shared_write.py +200 -0
  393. package/python/tests/test_skill_taint_gate.py +145 -0
  394. package/python/tests/test_skills.py +117 -0
  395. package/python/tests/test_skills_fetch.py +282 -0
  396. package/python/tests/test_stats_and_events.py +162 -0
  397. package/python/tests/test_streaming.py +55 -0
  398. package/python/tests/test_supervisor_pipeline.py +176 -0
  399. package/python/tests/test_supervisor_triage.py +102 -0
  400. package/python/tests/test_taint.py +112 -0
  401. package/python/tests/test_task_stream.py +74 -0
  402. package/python/tests/test_thinking_sse.py +66 -0
  403. package/python/tests/test_thinking_stream.py +76 -0
  404. package/python/tests/test_tool_center.py +113 -0
  405. package/python/tests/test_tool_parallel.py +113 -0
  406. package/python/tests/test_tool_params.py +99 -0
  407. package/python/tests/test_tool_use.py +229 -0
  408. package/python/tests/test_toplevel_concurrency.py +116 -0
  409. package/python/tests/test_tracing.py +256 -0
  410. package/python/tests/test_truth_crystallizer.py +125 -0
  411. package/python/tests/test_truth_flow.py +88 -0
  412. package/python/tests/test_update_check.py +95 -0
  413. package/python/tests/test_url_policy.py +53 -0
  414. package/python/tests/test_verify_evidence.py +212 -0
  415. package/python/tests/test_version_sync.py +100 -0
  416. package/python/tests/test_web_trigger.py +82 -0
  417. package/python/tools/check_faiss_type.py +30 -0
  418. package/python/tools/check_log.ps1 +38 -0
  419. package/python/tools/ivf_status.py +72 -0
  420. package/python/tools/mypy_baseline.txt +13 -0
  421. package/python/tools/mypy_ratchet.py +143 -0
  422. package/python/tools/pack.py +173 -0
  423. package/python/tools/release_check.py +349 -0
  424. package/python/tools/restart_foxclaw.ps1 +71 -0
  425. package/python/tools/shared_write_audit.py +190 -0
  426. package/python/tools/size_baseline.txt +28 -0
  427. package/python/tools/size_check.py +102 -0
  428. package/python/tools/strip_comments.py +228 -0
  429. package/python/tools/verify_env.bat +2 -0
  430. package/python/tools/verify_env.py +25 -0
  431. package/python/vendor/HMR_LICENSE +37 -0
  432. package/python/vendor/HMR_README.md +65 -0
  433. package/python/vendor/hkc/LICENSE +37 -0
  434. package/python/vendor/hkc/hkc_ace/__init__.py +0 -0
  435. package/python/vendor/hkc/hkc_ace/ace.py +457 -0
  436. package/python/vendor/hkc/hkc_ace/skill_taxonomy.json +465 -0
  437. package/python/vendor/hkc/hkc_api/__init__.py +0 -0
  438. package/python/vendor/hkc/hkc_api/container.py +416 -0
  439. package/python/vendor/hkc/hkc_api/ingest_jobs.py +103 -0
  440. package/python/vendor/hkc/hkc_api/main.py +157 -0
  441. package/python/vendor/hkc/hkc_api/routes/__init__.py +0 -0
  442. package/python/vendor/hkc/hkc_api/routes/ability.py +151 -0
  443. package/python/vendor/hkc/hkc_api/routes/config.py +51 -0
  444. package/python/vendor/hkc/hkc_api/routes/knowledge.py +630 -0
  445. package/python/vendor/hkc/hkc_api/routes/search.py +88 -0
  446. package/python/vendor/hkc/hkc_api/schemas.py +153 -0
  447. package/python/vendor/hkc/hkc_api/synthesis.py +179 -0
  448. package/python/vendor/hkc/hkc_core/__init__.py +0 -0
  449. package/python/vendor/hkc/hkc_core/graph/__init__.py +0 -0
  450. package/python/vendor/hkc/hkc_core/graph/base.py +188 -0
  451. package/python/vendor/hkc/hkc_core/graph/sqlite_store.py +789 -0
  452. package/python/vendor/hkc/hkc_core/models/__init__.py +0 -0
  453. package/python/vendor/hkc/hkc_core/models/enums.py +65 -0
  454. package/python/vendor/hkc/hkc_core/models/ku.py +307 -0
  455. package/python/vendor/hkc/hkc_core/utils/__init__.py +0 -0
  456. package/python/vendor/hkc/hkc_core/utils/id_gen.py +113 -0
  457. package/python/vendor/hkc/hkc_crystallizer/__init__.py +44 -0
  458. package/python/vendor/hkc/hkc_crystallizer/adapters.py +94 -0
  459. package/python/vendor/hkc/hkc_crystallizer/candidate.py +145 -0
  460. package/python/vendor/hkc/hkc_crystallizer/crystallizer.py +179 -0
  461. package/python/vendor/hkc/hkc_crystallizer/ingress.py +90 -0
  462. package/python/vendor/hkc/hkc_kde/__init__.py +0 -0
  463. package/python/vendor/hkc/hkc_kde/chunker.py +197 -0
  464. package/python/vendor/hkc/hkc_kde/extractor.py +623 -0
  465. package/python/vendor/hkc/hkc_kde/kde.py +298 -0
  466. package/python/vendor/hkc/hkc_kde/loaders/__init__.py +0 -0
  467. package/python/vendor/hkc/hkc_kde/loaders/ebook_loader.py +153 -0
  468. package/python/vendor/hkc/hkc_kde/loaders/office_loader.py +194 -0
  469. package/python/vendor/hkc/hkc_kde/loaders/pdf_loader.py +162 -0
  470. package/python/vendor/hkc/hkc_kde/loaders/text_loader.py +157 -0
  471. package/python/vendor/hkc/hkc_kde/loaders/web_loader.py +91 -0
  472. package/python/vendor/hkc/hkc_kde/models.py +74 -0
  473. package/python/vendor/hkc/hkc_kde/packager.py +329 -0
  474. package/python/vendor/hkc/hkc_kde/relation_disc.py +151 -0
  475. package/python/vendor/hkc/hkc_kee/__init__.py +0 -0
  476. package/python/vendor/hkc/hkc_kee/dedup.py +227 -0
  477. package/python/vendor/hkc/hkc_kee/fingerprint.py +106 -0
  478. package/python/vendor/hkc/hkc_kee/kee.py +541 -0
  479. package/python/vendor/hkc/hkc_kep/__init__.py +0 -0
  480. package/python/vendor/hkc/hkc_kep/event_bus.py +205 -0
  481. package/python/vendor/hkc/hkc_sdk/__init__.py +63 -0
  482. package/python/vendor/hkc/hkc_sdk/base.py +151 -0
  483. package/python/vendor/hkc/hkc_sdk/direct_client.py +153 -0
  484. package/python/vendor/hkc/hkc_sdk/exceptions.py +39 -0
  485. package/python/vendor/hkc/hkc_sdk/http_client.py +166 -0
  486. package/python/vendor/hkc/hkc_sdk/models.py +171 -0
  487. package/python/vendor/hkc/hkc_search/__init__.py +0 -0
  488. package/python/vendor/hkc/hkc_search/bm25.py +325 -0
  489. package/python/vendor/hkc/hkc_search/embedding_backends.py +374 -0
  490. package/python/vendor/hkc/hkc_search/graph_search.py +132 -0
  491. package/python/vendor/hkc/hkc_search/hybrid.py +331 -0
  492. package/python/vendor/hkc/hkc_search/index_build_manager.py +378 -0
  493. package/python/vendor/hkc/hkc_search/vector_search.py +484 -0
  494. package/python/vendor/hmr/__init__.py +0 -0
  495. package/python/vendor/hmr/core/__init__.py +0 -0
  496. package/python/vendor/hmr/core/hmr.py +684 -0
  497. package/python/vendor/hmr/core/models.py +310 -0
  498. package/python/vendor/hmr/engines/__init__.py +0 -0
  499. package/python/vendor/hmr/engines/evolution.py +710 -0
  500. package/python/vendor/hmr/engines/jit_compiler.py +472 -0
  501. package/python/vendor/hmr/engines/lifecycle.py +264 -0
  502. package/python/vendor/hmr/engines/policy.py +625 -0
  503. package/python/vendor/hmr/engines/recall.py +260 -0
  504. package/python/vendor/hmr/engines/runtime_state.py +158 -0
  505. package/python/vendor/hmr/engines/scheduler.py +323 -0
  506. package/python/vendor/hmr/engines/semantic.py +150 -0
  507. package/python/vendor/hmr/engines/temporal.py +261 -0
  508. package/python/vendor/hmr/engines/thought_chain.py +635 -0
  509. package/python/vendor/hmr/graph/__init__.py +0 -0
  510. package/python/vendor/hmr/graph/cwg.py +338 -0
  511. package/python/vendor/hmr/graph/memory_graph.py +530 -0
  512. package/python/vendor/hmr/storage/__init__.py +0 -0
  513. package/python/vendor/hmr/storage/memory_fs.py +300 -0
  514. package/python/vendor/hmr/storage/vector_store.py +520 -0
@@ -0,0 +1,177 @@
1
+ """
2
+ bench/bm25_regression.py
3
+ 新 BM25(自实现倒排) vs rank_bm25.BM25Okapi top-10 一致率回归测试。
4
+
5
+ 目标:1万 corpus + 100 query,新实现 top-10 一致率 >= 99%。
6
+ 跑法:python bench/bm25_regression.py
7
+ """
8
+ import os
9
+ import random
10
+ import sys
11
+ import time
12
+ from dataclasses import dataclass
13
+
14
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'vendor'))
15
+
16
+ from hkc.hkc_search.bm25 import BM25Index as NewBM25, _tokenize
17
+
18
+
19
+ @dataclass
20
+ class MockKU:
21
+ ku_id: str
22
+ name: str
23
+ summary: str
24
+ tags: list[str]
25
+
26
+
27
+ TOPICS_EN = [
28
+ "machine learning", "neural network", "deep learning", "data science",
29
+ "natural language", "computer vision", "reinforcement learning",
30
+ "transformer model", "graph neural network", "knowledge graph",
31
+ "vector database", "embedding model", "semantic search", "retrieval augmented",
32
+ "transformer architecture", "attention mechanism", "language model",
33
+ "encoder decoder", "self supervised", "contrastive learning",
34
+ ]
35
+ TOPICS_CN = [
36
+ "机器学习", "神经网络", "深度学习", "数据科学",
37
+ "自然语言处理", "计算机视觉", "强化学习",
38
+ "图神经网络", "知识图谱", "向量数据库",
39
+ "嵌入模型", "语义搜索", "检索增强",
40
+ "注意力机制", "语言模型", "自监督学习",
41
+ ]
42
+ ACTIONS = ["tutorial", "review", "introduction", "advanced", "case study", "comparison"]
43
+ LANGS = ["python", "rust", "cpp", "java", "go", "typescript"]
44
+
45
+
46
+ def gen_corpus(n=10000, seed=42):
47
+ rng = random.Random(seed)
48
+ corpus = []
49
+ for i in range(n):
50
+ k = rng.randint(1, 3)
51
+ topics = [rng.choice(TOPICS_EN) for _ in range(k)]
52
+ if rng.random() < 0.5:
53
+ topics.append(rng.choice(TOPICS_CN))
54
+ action = rng.choice(ACTIONS)
55
+ lang = rng.choice(LANGS)
56
+ name = f"{' '.join(topics)} {action} ({lang}) #{i}"
57
+ summary = f"This is a {action} about {' '.join(topics)} in {lang}. " \
58
+ f"Topics: {', '.join(topics)}."
59
+ tags = topics + [action, lang]
60
+ corpus.append(MockKU(
61
+ ku_id=f"ku_{i:06d}", name=name, summary=summary, tags=tags,
62
+ ))
63
+ return corpus
64
+
65
+
66
+ def build_rank_bm25(corpus):
67
+ try:
68
+ from rank_bm25 import BM25Okapi
69
+ except ImportError:
70
+ return None
71
+ tokenized = [_tokenize(f"{ku.name} {ku.summary} {' '.join(ku.tags)}") for ku in corpus]
72
+ return BM25Okapi(tokenized)
73
+
74
+
75
+ def rank_bm25_search(bm25, query, top_k, corpus):
76
+ if bm25 is None:
77
+ return []
78
+ tokens = _tokenize(query)
79
+ if not tokens:
80
+ return []
81
+ scores = bm25.get_scores(tokens)
82
+ indexed = sorted(enumerate(scores), key=lambda x: x[1], reverse=True)[:top_k]
83
+ return [(corpus[i].ku_id, float(s)) for i, s in indexed if s > 0]
84
+
85
+
86
+ def gen_queries(n=100, seed=99):
87
+ rng = random.Random(seed)
88
+ queries = []
89
+ for _ in range(n):
90
+ k = rng.randint(1, 3)
91
+ terms = [rng.choice(TOPICS_EN) for _ in range(k)]
92
+ if rng.random() < 0.3:
93
+ terms.append(rng.choice(TOPICS_CN))
94
+ if rng.random() < 0.4:
95
+ terms.append(rng.choice(ACTIONS))
96
+ queries.append(" ".join(terms))
97
+ return queries
98
+
99
+
100
+ def top10_overlap(rank_results, new_results):
101
+ rank_ids = [ku_id for ku_id, _ in rank_results[:10]]
102
+ new_ids = {ku_id for ku_id, _ in new_results[:10]}
103
+ if not rank_ids:
104
+ return 1.0
105
+ return sum(1 for x in rank_ids if x in new_ids) / len(rank_ids)
106
+
107
+
108
+ def main():
109
+ print("=" * 60)
110
+ print("BM25 回归测试: 自实现倒排 vs rank_bm25.BM25Okapi")
111
+ print("=" * 60)
112
+
113
+ print("\n[1/4] 生成 10000 mock corpus ...")
114
+ corpus = gen_corpus(10000, seed=42)
115
+ print(f" OK, {len(corpus)} docs")
116
+
117
+ print("\n[2/4] 构建索引 ...")
118
+ t0 = time.time()
119
+ new_idx = NewBM25()
120
+ new_idx.build(corpus)
121
+ t_new = time.time() - t0
122
+ print(f" 新实现 build: {t_new*1000:.1f} ms")
123
+
124
+ rank_bm25 = build_rank_bm25(corpus)
125
+ if rank_bm25 is None:
126
+ print(" rank_bm25 未安装,跳过一致率比对(只测性能)")
127
+
128
+ print("\n[3/4] 100 query 跑搜索 ...")
129
+ queries = gen_queries(100, seed=99)
130
+ new_topk = 0
131
+ rank_topk = 0
132
+ overlap_sum = 0.0
133
+ valid = 0
134
+ t0 = time.time()
135
+ for q in queries:
136
+ new_r = [(r.ku_id, r.score) for r in new_idx.search(q, top_k=10)]
137
+ rank_r = rank_bm25_search(rank_bm25, q, 10, corpus)
138
+ new_topk += len(new_r)
139
+ rank_topk += len(rank_r)
140
+ if rank_r and new_r:
141
+ overlap_sum += top10_overlap(rank_r, new_r)
142
+ valid += 1
143
+ t_search = time.time() - t0
144
+ print(f" 耗时: {t_search*1000:.1f} ms ({(t_search/100)*1000:.2f} ms/query)")
145
+ print(f" 新实现召回: {new_topk}, rank_bm25 召回: {rank_topk}")
146
+
147
+ if rank_bm25 is not None and valid > 0:
148
+ avg = overlap_sum / valid
149
+ print(f"\n[4/4] top-10 一致率(对 rank_bm25 ground truth): {avg*100:.1f}%")
150
+ if avg >= 0.99:
151
+ print(" PASS (>= 99%)")
152
+ else:
153
+ print(" FAIL (< 99%)")
154
+ sys.exit(1)
155
+ else:
156
+ print("\n[4/4] 跳过一致率(无 rank_bm25 对照)")
157
+
158
+ print("\n[bonus] save/load roundtrip ...")
159
+ tmp = "_tmp_bm25.pkl"
160
+ new_idx.save(tmp)
161
+ new_idx2 = NewBM25()
162
+ assert new_idx2.load(tmp), "load 失败"
163
+ assert new_idx2.size() == new_idx.size()
164
+ for q in queries[:5]:
165
+ r1 = sorted([(r.ku_id, r.score) for r in new_idx.search(q, 10)])
166
+ r2 = sorted([(r.ku_id, r.score) for r in new_idx2.search(q, 10)])
167
+ assert r1 == r2, f"save/load 不一致: {q}"
168
+ os.remove(tmp)
169
+ print(f" OK, roundtrip 一致 ({new_idx.size()} docs)")
170
+
171
+ print("\n" + "=" * 60)
172
+ print("全部通过")
173
+ print("=" * 60)
174
+
175
+
176
+ if __name__ == "__main__":
177
+ main()
@@ -0,0 +1,155 @@
1
+ """
2
+ bench/bm25_stress.py
3
+ 5万/10万 corpus 压测 + remove_ku 增量测试。
4
+ """
5
+ import os, sys, random, time
6
+ from dataclasses import dataclass
7
+
8
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'vendor'))
9
+ from hkc.hkc_search.bm25 import BM25Index as NewBM25, _tokenize
10
+
11
+
12
+ @dataclass
13
+ class MockKU:
14
+ ku_id: str
15
+ name: str
16
+ summary: str
17
+ tags: list[str]
18
+
19
+
20
+ TOPICS_EN = ["machine learning","neural network","deep learning","data science",
21
+ "natural language","computer vision","reinforcement learning",
22
+ "transformer model","graph neural network","knowledge graph",
23
+ "vector database","embedding model","semantic search","retrieval augmented",
24
+ "transformer architecture","attention mechanism","language model",
25
+ "encoder decoder","self supervised","contrastive learning"] * 5
26
+ ACTIONS = ["tutorial","review","introduction","advanced","case study","comparison"] * 10
27
+ LANGS = ["python","rust","cpp","java","go","typescript"] * 10
28
+ TOPICS_CN = ["机器学习","神经网络","深度学习","数据科学","自然语言处理",
29
+ "计算机视觉","强化学习","图神经网络","知识图谱","向量数据库",
30
+ "嵌入模型","语义搜索","检索增强","注意力机制","语言模型","自监督学习"] * 5
31
+
32
+
33
+ def gen_corpus(n, seed=42):
34
+ rng = random.Random(seed)
35
+ corpus = []
36
+ for i in range(n):
37
+ k = rng.randint(1, 3)
38
+ topics = [rng.choice(TOPICS_EN) for _ in range(k)]
39
+ if rng.random() < 0.5:
40
+ topics.append(rng.choice(TOPICS_CN))
41
+ action = rng.choice(ACTIONS)
42
+ lang = rng.choice(LANGS)
43
+ name = f"{' '.join(topics)} {action} ({lang}) #{i}"
44
+ summary = f"This is a {action} about {' '.join(topics)} in {lang}. " \
45
+ f"Topics: {', '.join(topics)}."
46
+ tags = topics + [action, lang]
47
+ corpus.append(MockKU(ku_id=f"ku_{i:08d}", name=name, summary=summary, tags=tags))
48
+ return corpus
49
+
50
+
51
+ def gen_queries(n, seed=99):
52
+ rng = random.Random(seed)
53
+ queries = []
54
+ for _ in range(n):
55
+ k = rng.randint(1, 3)
56
+ terms = [rng.choice(TOPICS_EN) for _ in range(k)]
57
+ if rng.random() < 0.3:
58
+ terms.append(rng.choice(TOPICS_CN))
59
+ if rng.random() < 0.4:
60
+ terms.append(rng.choice(ACTIONS))
61
+ queries.append(" ".join(terms))
62
+ return queries
63
+
64
+
65
+ def bench_build(n):
66
+ print(f"\n[build] {n} corpus ...")
67
+ corpus = gen_corpus(n)
68
+ t0 = time.time()
69
+ idx = NewBM25()
70
+ idx.build(corpus)
71
+ dt = time.time() - t0
72
+ print(f" build: {dt*1000:.0f} ms ({dt*1000/n:.3f} ms/doc)")
73
+ return idx, corpus
74
+
75
+
76
+ def bench_search(idx, queries):
77
+ t0 = time.time()
78
+ total_hits = 0
79
+ for q in queries:
80
+ r = idx.search(q, top_k=10)
81
+ total_hits += len(r)
82
+ dt = time.time() - t0
83
+ print(f" search {len(queries)} query: {dt*1000:.0f} ms ({dt*1000/len(queries):.2f} ms/query, {total_hits} hits)")
84
+ return dt
85
+
86
+
87
+ def bench_remove(idx, n_remove=1000):
88
+ print(f"\n[remove] 删 {n_remove} 条 (random) ...")
89
+ import random
90
+ rng = random.Random(123)
91
+ to_remove = rng.sample(idx._doc_ids, n_remove) # noqa
92
+ t0 = time.time()
93
+ for ku_id in to_remove:
94
+ idx.remove_ku(ku_id)
95
+ dt = time.time() - t0
96
+ print(f" remove: {dt*1000:.0f} ms ({dt*1000/n_remove:.3f} ms/op)")
97
+ print(f" size after: {idx.size()}")
98
+
99
+
100
+ def bench_save_load(idx, path):
101
+ print(f"\n[save/load] {path} ...")
102
+ t0 = time.time()
103
+ idx.save(path)
104
+ t_save = time.time() - t0
105
+ sz = os.path.getsize(path)
106
+ t0 = time.time()
107
+ idx2 = NewBM25()
108
+ ok = idx2.load(path)
109
+ t_load = time.time() - t0
110
+ print(f" save : {t_save*1000:.0f} ms ({sz/1024/1024:.1f} MB)")
111
+ print(f" load : {t_load*1000:.0f} ms (ok={ok}, size={idx2.size()})")
112
+
113
+
114
+ def main():
115
+ print("=" * 60)
116
+ print("BM25 压测: 5万/10万 corpus build + search + remove + save/load")
117
+ print("=" * 60)
118
+
119
+ idx, corpus = bench_build(50_000)
120
+ queries = gen_queries(200)
121
+ bench_search(idx, queries)
122
+ bench_remove(idx, 1000)
123
+ tmp = "_tmp_bm25_50k.pkl"
124
+ bench_save_load(idx, tmp)
125
+ os.remove(tmp)
126
+
127
+ print(f"\n[build] +5万 增量 (5万 -> 10万) ...")
128
+ extra_corpus = gen_corpus(50_000, seed=43)
129
+ extra_corpus = []
130
+ rng = random.Random(43)
131
+ for i in range(50_000, 100_000):
132
+ k = rng.randint(1, 3)
133
+ topics = [rng.choice(TOPICS_EN) for _ in range(k)]
134
+ if rng.random() < 0.5:
135
+ topics.append(rng.choice(TOPICS_CN))
136
+ action = rng.choice(ACTIONS)
137
+ lang = rng.choice(LANGS)
138
+ name = f"{' '.join(topics)} {action} ({lang}) #{i}"
139
+ summary = f"This is a {action} about {' '.join(topics)} in {lang}. Topics: {', '.join(topics)}."
140
+ tags = topics + [action, lang]
141
+ extra_corpus.append(MockKU(ku_id=f"ku_{i:08d}", name=name, summary=summary, tags=tags))
142
+ t0 = time.time()
143
+ idx.append_batch(extra_corpus)
144
+ dt = time.time() - t0
145
+ print(f" append 5万 增量: {dt*1000:.0f} ms ({dt*1000/50_000:.3f} ms/doc)")
146
+ print(f" size after: {idx.size()}")
147
+ bench_search(idx, queries)
148
+
149
+ print("\n" + "=" * 60)
150
+ print("压测完成")
151
+ print("=" * 60)
152
+
153
+
154
+ if __name__ == "__main__":
155
+ main()
@@ -0,0 +1,215 @@
1
+ """
2
+ bench/cd_audit.py
3
+ C+D 阶段 5 项 Risk 审核(commit 0bf4349):
4
+ D1. IngestJobRegistry(max_workers=4) 真起 4 thread 并发
5
+ D2. Extractor 16 chunk × 4 worker + 50% 429 重试 (限流雪崩测试)
6
+ D3. progress_cb 线程安全:done 计数无重复无漏
7
+ D4. _retry_llm max_retries 可调 (验证配置生效)
8
+ D5. retry 不破坏 progress_cb 顺序
9
+ """
10
+ import os, sys, time, threading
11
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'vendor'))
12
+
13
+ from hkc.hkc_kde.extractor import Extractor
14
+ from hkc.hkc_api.ingest_jobs import IngestJobRegistry
15
+
16
+
17
+ def d1_ingest_4_workers():
18
+ """D1. 4 worker 真并发 peak=4,4 任务 ~0.3s 完成"""
19
+ reg = IngestJobRegistry(max_workers=4)
20
+ running = [0]
21
+ peak = [0]
22
+ lock = threading.Lock()
23
+
24
+ def work(progress):
25
+ with lock:
26
+ running[0] += 1
27
+ peak[0] = max(peak[0], running[0])
28
+ time.sleep(0.3)
29
+ with lock:
30
+ running[0] -= 1
31
+ return []
32
+
33
+ jobs = [reg.create(f"f{i}") for i in range(4)]
34
+ t0 = time.time()
35
+ for j in jobs:
36
+ reg.submit(j, work)
37
+ while True:
38
+ statuses = [reg.get(j.job_id)["status"] for j in jobs]
39
+ if all(s == "done" for s in statuses):
40
+ break
41
+ time.sleep(0.02)
42
+ elapsed = time.time() - t0
43
+ reg.shutdown()
44
+ assert peak[0] == 4, f"expected peak=4, got {peak[0]}"
45
+ assert elapsed < 0.5, f"expected < 0.5s (并行), got {elapsed:.2f}s"
46
+ print(f" D1 PASS: peak={peak[0]}, 4 jobs in {elapsed*1000:.0f}ms (串行需 1.2s)")
47
+
48
+
49
+ def d2_429_snowball():
50
+ """D2. 16 chunk × 4 worker × 100% 429 永不成功,验证不死循环 + 总调用有上界"""
51
+ state = {"calls": 0}
52
+
53
+ def always_429(*args, **kwargs):
54
+ state["calls"] += 1
55
+ err = Exception("rate limited")
56
+ err.status_code = 429
57
+ raise err
58
+
59
+ ext = Extractor(provider="anthropic", model="m")
60
+ import hkc.hkc_kde.extractor as ex_mod
61
+ orig_sleep = ex_mod.time.sleep
62
+ sleeps = []
63
+ ex_mod.time.sleep = lambda s: sleeps.append(s)
64
+ try:
65
+ def mock_chunk(chunk, timeout=60.0):
66
+ try:
67
+ return ext._retry_llm(always_429, base_delay=0.001, max_delay=0.01)
68
+ except Exception:
69
+ return None
70
+ ext._extract_chunk = mock_chunk
71
+
72
+ class MockChunk:
73
+ def __init__(self, i):
74
+ self.chunk_id = f"c{i}"
75
+ self.doc_id = "d0"
76
+ self.content = "x"
77
+ self.source_hint = ""
78
+
79
+ chunks = [MockChunk(i) for i in range(16)]
80
+ t0 = time.time()
81
+ result = ext.extract(chunks)
82
+ elapsed = time.time() - t0
83
+
84
+ success = sum(1 for item in result.items if item is not None)
85
+ assert success == 0, f"expected 0 success, got {success}"
86
+ assert state["calls"] == 64, f"snowball: expected 64 calls, got {state['calls']}"
87
+ assert len(sleeps) == 48, f"expected 48 sleeps (16 chunk * 3 retry), got {len(sleeps)}"
88
+ assert elapsed < 3.0, f"deadlock: {elapsed:.1f}s"
89
+ print(f" D2 PASS: 16 chunk x 4 worker x 100% 429 -> {state['calls']} calls, {len(sleeps)} sleeps, {elapsed*1000:.0f}ms (no deadlock)")
90
+ finally:
91
+ ex_mod.time.sleep = orig_sleep
92
+
93
+
94
+ def d3_progress_cb_thread_safe():
95
+ """D3. progress_cb 在 as_completed 循环调,主线程串行;16 chunk 计数无重复无漏"""
96
+ ext = Extractor(provider="anthropic", model="m")
97
+
98
+ import random
99
+ random.seed(42)
100
+ def mock_extract(chunk, timeout=60.0):
101
+ from hkc.hkc_kde.models import RawExtraction
102
+ return RawExtraction()
103
+ ext._extract_chunk = mock_extract
104
+
105
+ class MockChunk:
106
+ def __init__(self, i):
107
+ self.chunk_id = f"c{i}"
108
+ self.doc_id = "d0"
109
+ self.content = "x"
110
+ self.source_hint = ""
111
+
112
+ done_calls = []
113
+ lock = threading.Lock()
114
+
115
+ def cb(processed, total):
116
+ with lock:
117
+ done_calls.append(processed)
118
+
119
+ chunks = [MockChunk(i) for i in range(16)]
120
+ result = ext.extract(chunks, progress_cb=cb)
121
+
122
+ assert len(done_calls) == 16, f"expected 16 callbacks, got {len(done_calls)}"
123
+ assert sorted(done_calls) == list(range(1, 17)), \
124
+ f"expected processed 1..16, got {sorted(done_calls)}"
125
+ print(f" D3 PASS: {len(done_calls)} callbacks, all 1..16 unique")
126
+
127
+
128
+ def d4_max_retries_configurable():
129
+ """D4. _retry_llm 的 max_retries 真的生效"""
130
+ ext = Extractor(provider="mock", model="m")
131
+
132
+ call_count = [0]
133
+ def always_fail():
134
+ call_count[0] += 1
135
+ err = Exception("rate")
136
+ err.status_code = 429
137
+ raise err
138
+
139
+ import hkc.hkc_kde.extractor as ex_mod
140
+ orig_sleep = ex_mod.time.sleep
141
+ sleeps = []
142
+ ex_mod.time.sleep = lambda s: sleeps.append(s)
143
+ try:
144
+ try:
145
+ ext._retry_llm(always_fail, max_retries=2, base_delay=0.001)
146
+ assert False, "expected raise"
147
+ except Exception:
148
+ pass
149
+ assert call_count[0] == 3, f"max_retries=2 expected 3 calls, got {call_count[0]}"
150
+ assert len(sleeps) == 2, f"max_retries=2 expected 2 sleeps, got {len(sleeps)}"
151
+ print(f" D4 PASS: max_retries=2 -> 3 calls, 2 sleeps")
152
+ finally:
153
+ ex_mod.time.sleep = orig_sleep
154
+
155
+
156
+ def d5_retry_no_progress_leak():
157
+ """D5. retry 期间 progress_cb 仍只调 1 次/chunk(不管 retry 几次)"""
158
+ import time as time_mod
159
+ orig_sleep = time_mod.sleep
160
+ time_mod.sleep = lambda s: None
161
+
162
+ state = {"calls": 0}
163
+ def always_429(*args, **kwargs):
164
+ state["calls"] += 1
165
+ err = Exception("rate"); err.status_code = 429
166
+ raise err
167
+
168
+ class MockResp:
169
+ def __init__(self, t): self.content = [MockText(t)]
170
+ class MockText:
171
+ def __init__(self, t): self.text = t
172
+
173
+ class MockClient:
174
+ @property
175
+ def messages(self): return self
176
+ def create(self, *args, **kwargs): return always_429(*args, **kwargs)
177
+
178
+ ext = Extractor(provider="anthropic", model="m")
179
+ ext._client = MockClient()
180
+ try:
181
+ class MockChunk:
182
+ def __init__(self, i):
183
+ self.chunk_id=f"c{i}"; self.doc_id="d0"; self.content="x"; self.source_hint=""
184
+ cb_count = [0]
185
+ cb_lock = threading.Lock()
186
+ def cb(p, t):
187
+ with cb_lock:
188
+ cb_count[0] += 1
189
+ chunks = [MockChunk(i) for i in range(8)]
190
+ result = ext.extract(chunks, progress_cb=cb)
191
+ assert cb_count[0] == 8, f"expected 8 progress, got {cb_count[0]}"
192
+ assert state["calls"] == 32, f"expected 32 calls, got {state['calls']}"
193
+ success = sum(1 for item in result.items if item is not None)
194
+ assert success == 0
195
+ print(f" D5 PASS: 8 chunks x 4 attempts, progress_cb={cb_count[0]} (1 per chunk), calls={state['calls']}")
196
+ finally:
197
+ time_mod.sleep = orig_sleep
198
+
199
+
200
+ def main():
201
+ print("=" * 60)
202
+ print("C+D 阶段 5 项 Risk 审核 (commit 0bf4349)")
203
+ print("=" * 60)
204
+ d1_ingest_4_workers()
205
+ d2_429_snowball()
206
+ d3_progress_cb_thread_safe()
207
+ d4_max_retries_configurable()
208
+ d5_retry_no_progress_leak()
209
+ print("=" * 60)
210
+ print("5/5 PASS")
211
+ print("=" * 60)
212
+
213
+
214
+ if __name__ == "__main__":
215
+ main()
@@ -0,0 +1,140 @@
1
+ """
2
+ 混沌故障注入(Chaos Engineering)· 数字产物
3
+
4
+ 补足 bench 三件套的「故障维度」:
5
+ · concurrency.py —— 共享库并发写(无故障)
6
+ · load_test.py —— HTTP 高并发负载(无故障)
7
+ · chaos.py(本文件) —— 故障注入:LLM 空响应/异常/fallback/断路器,验证降级语义 + 恢复
8
+
9
+ 验证 ModelRouter(所有 Agent 的 LLM 唯一咽喉)在故障下的三件事:
10
+ 1. 降级语义正确:空响应/全失败**不伪装成功**(对齐 base.py §二「真相优先」);
11
+ 2. fallback 链:主 provider 故障 → 自动回退到已配的次 provider;
12
+ 3. 断路器:故障 provider 进冷却、成功解冻(惊群防护),系统可恢复。
13
+
14
+ 离线:不联网、不碰真实 Key,纯 monkeypatch 注入故障。
15
+ 用法:HF_HUB_OFFLINE=1 python bench/chaos.py
16
+ """
17
+ import os
18
+ import sys
19
+ import time
20
+ from pathlib import Path
21
+
22
+ sys.path.insert(0, str(Path(__file__).parent.parent))
23
+
24
+ import foxclaw.core.web_search as ws # noqa: E402
25
+ ws.needs_search = lambda q: False
26
+
27
+ from foxclaw.core.model_router import ModelRouter, PROVIDER_ENV # noqa: E402
28
+
29
+ _SYS = "你是 Foxclaw 助手。"
30
+ _MSG = [{"role": "user", "content": "用一句话总结量子计算"}]
31
+
32
+
33
+ def _make_router(primary="deepseek", fallback=None):
34
+ """构造一个走真实路径(非 stub)的 router,并把 _client 设成非 None,
35
+ 以便 monkeypatch _call_provider 注入故障。fallback 指定次 provider 并伪造其 key。"""
36
+ if fallback:
37
+ os.environ[PROVIDER_ENV[fallback]] = "chaos-test-key"
38
+ r = ModelRouter(provider=primary)
39
+ r._client = object()
40
+ r._clients[primary] = object()
41
+ if fallback:
42
+ r._clients[fallback] = object()
43
+ return r
44
+
45
+
46
+ def _boom(provider, client, model, system, messages, max_tokens, images=None):
47
+ raise RuntimeError("chaos: upstream timeout")
48
+
49
+
50
+ def case1_empty_response():
51
+ """[1] 空响应故障:provider 返回空串 → degraded + 显式失败文案,不伪装成功。"""
52
+ r = _make_router("deepseek")
53
+ r._call_provider = lambda *a, **k: ("", None)
54
+ out = r.complete(_SYS, _MSG)
55
+ ok = ("【模型返回为空】" in out) and r.is_degraded()
56
+ return ok, ("degraded=" + str(r.is_degraded())), out[:40]
57
+
58
+
59
+ def case2_call_failure():
60
+ """[2] 调用异常故障:LLM 抛异常 → 全部失败文案 + degraded,绝不拼接 stub 成功文案。"""
61
+ r = _make_router("deepseek")
62
+ r._call_provider = _boom
63
+ out = r.complete(_SYS, _MSG)
64
+ ok = ("【全部模型失败】" in out) and r.is_degraded() and ("已完成" not in out)
65
+ return ok, ("degraded=" + str(r.is_degraded()) + " 伪装成功=" + str("已完成" in out)), out[:40]
66
+
67
+
68
+ def case3_fallback_chain():
69
+ """[3] fallback 链:主 provider 抛异常 → 自动回退到已配次 provider 并记录原因。"""
70
+ r = _make_router("deepseek", fallback="openai")
71
+ calls = []
72
+
73
+ def _flaky(provider, client, model, system, messages, max_tokens, images=None):
74
+ calls.append(provider)
75
+ if provider == "deepseek":
76
+ raise RuntimeError("chaos: deepseek down")
77
+ return "openai 兜底回复", None
78
+
79
+ r._call_provider = _flaky
80
+ out = r.complete(_SYS, _MSG)
81
+ ok = (out == "openai 兜底回复") and (calls[-1] == "openai") and bool(r.fallback_reason)
82
+ return ok, ("调用顺序=" + str(calls) + " 回退原因=" + r.fallback_reason), out[:40]
83
+
84
+
85
+ def case4_circuit_breaker():
86
+ """[4] 断路器:provider 失败后进入冷却,立即重试被跳过(不再紧密重试)。"""
87
+ r = _make_router("deepseek")
88
+ r._call_provider = _boom
89
+ r.complete(_SYS, _MSG)
90
+ cooled = "deepseek" in r._cooldown
91
+ t0 = time.perf_counter()
92
+ r.complete(_SYS, _MSG)
93
+ fast = (time.perf_counter() - t0) < 0.5
94
+ ok = cooled and fast and r.is_degraded()
95
+ return ok, (f"冷却中={cooled} 跳过耗时={((time.perf_counter()-t0)*1000):.0f}ms"), "断路器生效"
96
+
97
+
98
+ def case5_recovery():
99
+ """[5] 恢复:故障清除(cooldown 过期/成功)后,同一 provider 恢复正常服务。"""
100
+ r = _make_router("deepseek")
101
+ r._call_provider = _boom
102
+ r.complete(_SYS, _MSG)
103
+ assert "deepseek" in r._cooldown
104
+ r._cooldown["deepseek"] = 0
105
+ r._call_provider = lambda *a, **k: ("恢复后的正常回复", None)
106
+ out = r.complete(_SYS, _MSG)
107
+ ok = (out == "恢复后的正常回复") and ("deepseek" not in r._cooldown)
108
+ return ok, ("解冻=" + str("deepseek" not in r._cooldown)), out[:40]
109
+
110
+
111
+ def main():
112
+ cases = [
113
+ ("空响应 → degraded(不伪装成功)", case1_empty_response),
114
+ ("调用异常 → 全部失败(不拼接 stub)", case2_call_failure),
115
+ ("fallback 链(主失败→次兜底)", case3_fallback_chain),
116
+ ("断路器(失败进冷却·重试跳过)", case4_circuit_breaker),
117
+ ("恢复(冷却过期→正常服务)", case5_recovery),
118
+ ]
119
+ print("=" * 64)
120
+ print("Foxclaw · 混沌故障注入(ModelRouter 咽喉)")
121
+ print("=" * 64)
122
+ n_ok = 0
123
+ for name, fn in cases:
124
+ try:
125
+ ok, detail, sample = fn()
126
+ n_ok += 1 if ok else 0
127
+ print(f"\n[{name}]")
128
+ print(f" {'✓ 通过' if ok else '✗ 失败'} {detail}")
129
+ print(f" 输出样例: {sample}…")
130
+ except Exception as e:
131
+ print(f"\n[{name}]\n ✗ 异常: {type(e).__name__}: {e}")
132
+ print("\n" + "=" * 64)
133
+ print(f"结论: {n_ok}/{len(cases)} 混沌用例通过 — "
134
+ f"{'✓ 故障下降级语义正确、fallback/断路器/恢复均生效' if n_ok == len(cases) else '✗ 存在故障处理缺口'}")
135
+ print("=" * 64)
136
+ return 0 if n_ok == len(cases) else 1
137
+
138
+
139
+ if __name__ == "__main__":
140
+ sys.exit(main())