foxclaw 0.1.0 → 0.1.2

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 (495) hide show
  1. package/README.md +54 -29
  2. package/bin/foxclaw.cjs +51 -0
  3. package/index.js +11 -10
  4. package/package.json +46 -38
  5. package/python/.dockerignore +35 -0
  6. package/python/.env.example +165 -0
  7. package/python/.gitattributes +46 -0
  8. package/python/.github/workflows/ci.yml +122 -0
  9. package/python/.github/workflows/release.yml +183 -0
  10. package/python/Dockerfile +64 -0
  11. package/python/LICENSE +21 -0
  12. package/python/MANIFEST.in +14 -0
  13. package/python/README.md +192 -0
  14. package/python/README_CN.md +324 -0
  15. package/python/bench/bm25_audit.py +199 -0
  16. package/python/bench/bm25_regression.py +178 -0
  17. package/python/bench/bm25_stress.py +159 -0
  18. package/python/bench/cd_audit.py +238 -0
  19. package/python/bench/chaos.py +142 -0
  20. package/python/bench/concurrency.py +103 -0
  21. package/python/bench/e2e_real_models.py +95 -0
  22. package/python/bench/llm_retry_test.py +248 -0
  23. package/python/bench/load_test.py +196 -0
  24. package/python/bench/vector_ivf_upgrade.py +134 -0
  25. package/python/docs/CHANNELS.md +195 -0
  26. package/python/docs/GOVERNANCE.md +164 -0
  27. package/python/docs/RELEASE.md +109 -0
  28. package/python/docs/RUN.md +75 -0
  29. package/python/docs/RUN_CN.md +121 -0
  30. package/python/docs//351/205/215/347/275/256/346/214/207/345/215/227.md +152 -0
  31. package/python/evals/__init__.py +0 -0
  32. package/python/evals/governance/README.md +61 -0
  33. package/python/evals/governance/__init__.py +0 -0
  34. package/python/evals/governance/cases.py +152 -0
  35. package/python/evals/governance/compare_embeddings.py +94 -0
  36. package/python/evals/governance/convergence.svg +11 -0
  37. package/python/evals/governance/report.json +371 -0
  38. package/python/evals/governance/run_evals.py +400 -0
  39. package/python/examples/governance_middleware_demo.py +77 -0
  40. package/python/examples/langgraph_governance_demo.py +108 -0
  41. package/python/examples/sample_itinerary.csv +6 -0
  42. package/python/examples/sample_knowledge.csv +9 -0
  43. package/python/examples/sample_transactions.csv +14 -0
  44. package/python/foxclaw/__init__.py +526 -0
  45. package/python/foxclaw/agents/__init__.py +12 -0
  46. package/python/foxclaw/agents/agents.py +35 -0
  47. package/python/foxclaw/agents/base.py +865 -0
  48. package/python/foxclaw/agents/impl/__init__.py +1 -0
  49. package/python/foxclaw/agents/impl/_helpers.py +78 -0
  50. package/python/foxclaw/agents/impl/finance.py +248 -0
  51. package/python/foxclaw/agents/impl/office.py +367 -0
  52. package/python/foxclaw/agents/impl/supervisor.py +659 -0
  53. package/python/foxclaw/agents/mixins/_context.py +151 -0
  54. package/python/foxclaw/agents/mixins/augment.py +149 -0
  55. package/python/foxclaw/agents/mixins/llm.py +66 -0
  56. package/python/foxclaw/agents/mixins/memory.py +35 -0
  57. package/python/foxclaw/agents/mixins/preference.py +81 -0
  58. package/python/foxclaw/agents/mixins/prompt.py +77 -0
  59. package/python/foxclaw/agents/mixins/skillbox.py +58 -0
  60. package/python/foxclaw/agents/mixins/tool_exec.py +443 -0
  61. package/python/foxclaw/api/__init__.py +283 -0
  62. package/python/foxclaw/api/app.py +123 -0
  63. package/python/foxclaw/api/middleware.py +317 -0
  64. package/python/foxclaw/api/routes/ahs.py +417 -0
  65. package/python/foxclaw/api/routes/attachments.py +117 -0
  66. package/python/foxclaw/api/routes/browser.py +39 -0
  67. package/python/foxclaw/api/routes/domain.py +314 -0
  68. package/python/foxclaw/api/routes/knowledge.py +163 -0
  69. package/python/foxclaw/api/routes/llm_admin.py +461 -0
  70. package/python/foxclaw/api/routes/memory.py +89 -0
  71. package/python/foxclaw/api/routes/orchestration.py +41 -0
  72. package/python/foxclaw/api/routes/recovery.py +43 -0
  73. package/python/foxclaw/api/routes/review.py +275 -0
  74. package/python/foxclaw/api/routes/search.py +37 -0
  75. package/python/foxclaw/api/routes/system.py +327 -0
  76. package/python/foxclaw/api/routes/task.py +281 -0
  77. package/python/foxclaw/api/routes/ws.py +68 -0
  78. package/python/foxclaw/api/spine.py +55 -0
  79. package/python/foxclaw/browser/__init__.py +13 -0
  80. package/python/foxclaw/browser/action/__init__.py +1 -0
  81. package/python/foxclaw/browser/action/action_executor.py +54 -0
  82. package/python/foxclaw/browser/action/action_types.py +45 -0
  83. package/python/foxclaw/browser/action/action_verifier.py +68 -0
  84. package/python/foxclaw/browser/action/url_policy.py +147 -0
  85. package/python/foxclaw/browser/adapter/__init__.py +1 -0
  86. package/python/foxclaw/browser/adapter/base_adapter.py +23 -0
  87. package/python/foxclaw/browser/adapter/web_adapter.py +46 -0
  88. package/python/foxclaw/browser/driver/__init__.py +1 -0
  89. package/python/foxclaw/browser/driver/base_driver.py +61 -0
  90. package/python/foxclaw/browser/driver/playwright_driver.py +166 -0
  91. package/python/foxclaw/browser/perception/__init__.py +1 -0
  92. package/python/foxclaw/browser/perception/dom_extractor.py +43 -0
  93. package/python/foxclaw/browser/perception/element_indexer.py +57 -0
  94. package/python/foxclaw/browser/perception/semantic_reducer.py +33 -0
  95. package/python/foxclaw/browser/perception/snapshot.py +29 -0
  96. package/python/foxclaw/browser/service.py +148 -0
  97. package/python/foxclaw/browser/session/__init__.py +1 -0
  98. package/python/foxclaw/browser/session/session_manager.py +57 -0
  99. package/python/foxclaw/capability/__init__.py +12 -0
  100. package/python/foxclaw/capability/embedding_service.py +67 -0
  101. package/python/foxclaw/channels/__init__.py +140 -0
  102. package/python/foxclaw/channels/discord.py +184 -0
  103. package/python/foxclaw/channels/dispatcher.py +204 -0
  104. package/python/foxclaw/channels/feishu.py +225 -0
  105. package/python/foxclaw/channels/openclaw_bridge.py +156 -0
  106. package/python/foxclaw/channels/signing.py +187 -0
  107. package/python/foxclaw/channels/telegram.py +121 -0
  108. package/python/foxclaw/channels/weixin.py +225 -0
  109. package/python/foxclaw/cli.py +154 -0
  110. package/python/foxclaw/cognitive/__init__.py +4 -0
  111. package/python/foxclaw/cognitive/reasoner.py +82 -0
  112. package/python/foxclaw/cognitive/state.py +147 -0
  113. package/python/foxclaw/core/__init__.py +9 -0
  114. package/python/foxclaw/core/audit_chain.py +133 -0
  115. package/python/foxclaw/core/audit_sink.py +100 -0
  116. package/python/foxclaw/core/compaction.py +283 -0
  117. package/python/foxclaw/core/config.py +65 -0
  118. package/python/foxclaw/core/confirm_nonce.py +157 -0
  119. package/python/foxclaw/core/dispatch.py +27 -0
  120. package/python/foxclaw/core/envfile.py +48 -0
  121. package/python/foxclaw/core/failure_policy.py +151 -0
  122. package/python/foxclaw/core/gateway.py +85 -0
  123. package/python/foxclaw/core/governance_audit.py +175 -0
  124. package/python/foxclaw/core/idempotency.py +289 -0
  125. package/python/foxclaw/core/injection_guard.py +221 -0
  126. package/python/foxclaw/core/memory_adapter.py +516 -0
  127. package/python/foxclaw/core/metrics.py +89 -0
  128. package/python/foxclaw/core/model_router.py +1097 -0
  129. package/python/foxclaw/core/paths.py +103 -0
  130. package/python/foxclaw/core/recovery.py +362 -0
  131. package/python/foxclaw/core/scheduler.py +144 -0
  132. package/python/foxclaw/core/secrets.py +134 -0
  133. package/python/foxclaw/core/sessions.py +157 -0
  134. package/python/foxclaw/core/settings.py +430 -0
  135. package/python/foxclaw/core/shared_write.py +198 -0
  136. package/python/foxclaw/core/steering_queue.py +153 -0
  137. package/python/foxclaw/core/system_bus.py +84 -0
  138. package/python/foxclaw/core/thought_chain.py +318 -0
  139. package/python/foxclaw/core/token_meter.py +235 -0
  140. package/python/foxclaw/core/tools.py +172 -0
  141. package/python/foxclaw/core/tracing.py +390 -0
  142. package/python/foxclaw/core/truth_pipeline.py +114 -0
  143. package/python/foxclaw/core/vec_recall_adapter.py +503 -0
  144. package/python/foxclaw/core/verify.py +181 -0
  145. package/python/foxclaw/core/web_search.py +242 -0
  146. package/python/foxclaw/finance/__init__.py +15 -0
  147. package/python/foxclaw/finance/evidence.py +153 -0
  148. package/python/foxclaw/finance/fx.py +100 -0
  149. package/python/foxclaw/finance/graph.py +188 -0
  150. package/python/foxclaw/finance/models.py +150 -0
  151. package/python/foxclaw/finance/nlparse.py +575 -0
  152. package/python/foxclaw/finance/policy.py +329 -0
  153. package/python/foxclaw/finance/reasoner.py +129 -0
  154. package/python/foxclaw/finance/reasoning.py +340 -0
  155. package/python/foxclaw/finance/reconcile.py +180 -0
  156. package/python/foxclaw/finance/runtime.py +527 -0
  157. package/python/foxclaw/governance/__init__.py +24 -0
  158. package/python/foxclaw/governance/middleware.py +184 -0
  159. package/python/foxclaw/harness/__init__.py +17 -0
  160. package/python/foxclaw/harness/_base.py +72 -0
  161. package/python/foxclaw/harness/_policy.py +359 -0
  162. package/python/foxclaw/harness/drift.py +89 -0
  163. package/python/foxclaw/harness/event_bus.py +31 -0
  164. package/python/foxclaw/harness/harness.py +446 -0
  165. package/python/foxclaw/harness/loop_detection.py +152 -0
  166. package/python/foxclaw/itinerary/__init__.py +10 -0
  167. package/python/foxclaw/itinerary/amap_client.py +117 -0
  168. package/python/foxclaw/itinerary/models.py +80 -0
  169. package/python/foxclaw/itinerary/reasoner.py +101 -0
  170. package/python/foxclaw/itinerary/reasoning.py +174 -0
  171. package/python/foxclaw/itinerary/runtime.py +148 -0
  172. package/python/foxclaw/kernel/__init__.py +13 -0
  173. package/python/foxclaw/kernel/capabilities.py +73 -0
  174. package/python/foxclaw/kernel/policy_gate.py +114 -0
  175. package/python/foxclaw/kernel/process_manager.py +205 -0
  176. package/python/foxclaw/kernel/scheduler.py +131 -0
  177. package/python/foxclaw/kernel/sentinel.py +453 -0
  178. package/python/foxclaw/kernel/sentinel_client.py +246 -0
  179. package/python/foxclaw/kernel/spine.py +97 -0
  180. package/python/foxclaw/knowledge/__init__.py +376 -0
  181. package/python/foxclaw/knowledge/truth_crystallizer.py +92 -0
  182. package/python/foxclaw/learning/__init__.py +12 -0
  183. package/python/foxclaw/learning/knowledge_graph.py +149 -0
  184. package/python/foxclaw/learning/models.py +93 -0
  185. package/python/foxclaw/learning/reasoner.py +70 -0
  186. package/python/foxclaw/learning/runtime.py +363 -0
  187. package/python/foxclaw/learning/spaced_repetition.py +96 -0
  188. package/python/foxclaw/mcp_server.py +189 -0
  189. package/python/foxclaw/orchestration/__init__.py +184 -0
  190. package/python/foxclaw/pipeline/__init__.py +23 -0
  191. package/python/foxclaw/pipeline/cognitive_pipeline.py +481 -0
  192. package/python/foxclaw/pipeline/supervisor_stages.py +230 -0
  193. package/python/foxclaw/planning/__init__.py +17 -0
  194. package/python/foxclaw/planning/llm_planner.py +232 -0
  195. package/python/foxclaw/planning/planner.py +99 -0
  196. package/python/foxclaw/search/__init__.py +24 -0
  197. package/python/foxclaw/search/agent_port.py +194 -0
  198. package/python/foxclaw/search/evidence.py +113 -0
  199. package/python/foxclaw/search/external_search.py +50 -0
  200. package/python/foxclaw/search/hkc_read_port.py +60 -0
  201. package/python/foxclaw/search/internal_search.py +73 -0
  202. package/python/foxclaw/search/models.py +129 -0
  203. package/python/foxclaw/search/planner.py +44 -0
  204. package/python/foxclaw/search/retriever.py +39 -0
  205. package/python/foxclaw/search/runtime.py +134 -0
  206. package/python/foxclaw/search/strategy.py +44 -0
  207. package/python/foxclaw/skills/__init__.py +613 -0
  208. package/python/foxclaw/skills/hooks_spec.py +73 -0
  209. package/python/foxclaw/skills/lifecycle.py +278 -0
  210. package/python/foxclaw/skills/markdown_skill.py +174 -0
  211. package/python/foxclaw/skills/markdown_store.py +39 -0
  212. package/python/foxclaw/skills/mcp_client.py +190 -0
  213. package/python/foxclaw/skills/mcp_store.py +43 -0
  214. package/python/foxclaw/skills/permission.py +180 -0
  215. package/python/foxclaw/skills/pipeline.py +386 -0
  216. package/python/foxclaw/skills/policy.py +80 -0
  217. package/python/foxclaw/skills/review.py +72 -0
  218. package/python/foxclaw/skills/store.py +57 -0
  219. package/python/frontend/assets/core_bg.jpg +0 -0
  220. package/python/frontend/deck/auth.js +32 -0
  221. package/python/frontend/deck/chat_model_picker.js +537 -0
  222. package/python/frontend/deck/domain.js +296 -0
  223. package/python/frontend/deck/main.js +1950 -0
  224. package/python/frontend/deck/markdown.js +48 -0
  225. package/python/frontend/deck/settings.js +324 -0
  226. package/python/frontend/deck/state.js +6 -0
  227. package/python/frontend/deck/util.js +6 -0
  228. package/python/frontend/deck/visuals.js +212 -0
  229. package/python/frontend/deck.css +855 -0
  230. package/python/frontend/deck.html +686 -0
  231. package/python/frontend/deck_v2.html +341 -0
  232. package/python/frontend/planet/index.html +266 -0
  233. package/python/frontend/planet/planet.css +579 -0
  234. package/python/frontend/planet/planet.js +1637 -0
  235. package/python/frontend/planet/vendor/three.core.min.js +6 -0
  236. package/python/frontend/planet/vendor/three.module.min.js +6 -0
  237. package/python/frontend/v3/assets/index-BD7VPbMV.js +33 -0
  238. package/python/frontend/v3/assets/index-CAvnYWGH.css +1 -0
  239. package/python/frontend/v3/index.html +15 -0
  240. package/python/frontend/widget_runtime.js +295 -0
  241. package/python/frontend/widgets.js +696 -0
  242. package/python/frontend_src/index.html +14 -0
  243. package/python/frontend_src/package-lock.json +1376 -0
  244. package/python/frontend_src/package.json +19 -0
  245. package/python/frontend_src/src/App.vue +62 -0
  246. package/python/frontend_src/src/api.js +89 -0
  247. package/python/frontend_src/src/components/ChatDock.vue +377 -0
  248. package/python/frontend_src/src/components/ComposerMenu.vue +75 -0
  249. package/python/frontend_src/src/components/DocViewer.vue +171 -0
  250. package/python/frontend_src/src/components/Inspector.vue +241 -0
  251. package/python/frontend_src/src/components/LeftRail.vue +394 -0
  252. package/python/frontend_src/src/components/MenuPopover.vue +130 -0
  253. package/python/frontend_src/src/components/MessageStream.vue +401 -0
  254. package/python/frontend_src/src/components/ModelPicker.vue +355 -0
  255. package/python/frontend_src/src/components/QuickIngest.vue +118 -0
  256. package/python/frontend_src/src/components/Stage.vue +226 -0
  257. package/python/frontend_src/src/components/ToolMenu.vue +144 -0
  258. package/python/frontend_src/src/components/TopBar.vue +90 -0
  259. package/python/frontend_src/src/components/panels/ActivityPanel.vue +84 -0
  260. package/python/frontend_src/src/components/panels/AgentsPanel.vue +86 -0
  261. package/python/frontend_src/src/components/panels/KnowledgePanel.vue +441 -0
  262. package/python/frontend_src/src/components/panels/MemoryPanel.vue +118 -0
  263. package/python/frontend_src/src/components/panels/PanelBox.vue +51 -0
  264. package/python/frontend_src/src/components/panels/ReviewPanel.vue +308 -0
  265. package/python/frontend_src/src/components/panels/SettingsPanel.vue +254 -0
  266. package/python/frontend_src/src/components/panels/TasksPanel.vue +74 -0
  267. package/python/frontend_src/src/components/panels/ToolsPanel.vue +640 -0
  268. package/python/frontend_src/src/components/panels/UsagePanel.vue +105 -0
  269. package/python/frontend_src/src/lib/fmt.js +48 -0
  270. package/python/frontend_src/src/lib/md.js +204 -0
  271. package/python/frontend_src/src/main.js +7 -0
  272. package/python/frontend_src/src/stores/app.js +106 -0
  273. package/python/frontend_src/src/stores/chat.js +484 -0
  274. package/python/frontend_src/src/stores/model.js +104 -0
  275. package/python/frontend_src/src/stores/panels.js +330 -0
  276. package/python/frontend_src/src/styles/base.css +138 -0
  277. package/python/frontend_src/src/styles/panel.css +117 -0
  278. package/python/frontend_src/src/styles/tokens.css +78 -0
  279. package/python/frontend_src/vite.config.js +46 -0
  280. package/python/pyproject.toml +265 -0
  281. package/python/skills/README.md +123 -0
  282. package/python/skills/execution-contract/SKILL.md +30 -0
  283. package/python/skills/general-assistant/SKILL.md +34 -0
  284. package/python/skills/planner/SKILL.md +33 -0
  285. package/python/skills/schedule-itinerary/SKILL.md +35 -0
  286. package/python/skills/skill-composition/SKILL.md +31 -0
  287. package/python/skills/skill-guard/SKILL.md +67 -0
  288. package/python/skills/skill-memory/SKILL.md +50 -0
  289. package/python/skills/skill-router/SKILL.md +35 -0
  290. package/python/tests/conftest.py +76 -0
  291. package/python/tests/run_all.py +263 -0
  292. package/python/tests/test_agent_lifecycle.py +360 -0
  293. package/python/tests/test_ahs_p2.py +414 -0
  294. package/python/tests/test_anthropic_tools.py +206 -0
  295. package/python/tests/test_api.py +87 -0
  296. package/python/tests/test_api_auth.py +83 -0
  297. package/python/tests/test_api_host_guard.py +69 -0
  298. package/python/tests/test_attachments.py +167 -0
  299. package/python/tests/test_audit_chain.py +121 -0
  300. package/python/tests/test_audit_sink.py +136 -0
  301. package/python/tests/test_browser.py +153 -0
  302. package/python/tests/test_browser_request_guard.py +184 -0
  303. package/python/tests/test_capabilities.py +158 -0
  304. package/python/tests/test_channel_signing.py +192 -0
  305. package/python/tests/test_channel_trust.py +109 -0
  306. package/python/tests/test_channels.py +184 -0
  307. package/python/tests/test_cli.py +46 -0
  308. package/python/tests/test_compaction_toolpair.py +119 -0
  309. package/python/tests/test_config_layering.py +65 -0
  310. package/python/tests/test_confirm_nonce.py +197 -0
  311. package/python/tests/test_context_compaction_wiring.py +130 -0
  312. package/python/tests/test_dispatch.py +95 -0
  313. package/python/tests/test_domain_evals.py +403 -0
  314. package/python/tests/test_domain_import.py +78 -0
  315. package/python/tests/test_domain_reasoners.py +148 -0
  316. package/python/tests/test_e2e.py +178 -0
  317. package/python/tests/test_emb_cache.py +107 -0
  318. package/python/tests/test_embedding_singleton.py +89 -0
  319. package/python/tests/test_envfile.py +83 -0
  320. package/python/tests/test_evals_smoke.py +114 -0
  321. package/python/tests/test_event_spine.py +182 -0
  322. package/python/tests/test_evidence_semantic.py +101 -0
  323. package/python/tests/test_evidence_sse.py +97 -0
  324. package/python/tests/test_file_ingest.py +146 -0
  325. package/python/tests/test_finance.py +153 -0
  326. package/python/tests/test_finance_ledger.py +252 -0
  327. package/python/tests/test_finance_persist.py +178 -0
  328. package/python/tests/test_gateway.py +144 -0
  329. package/python/tests/test_governance.py +468 -0
  330. package/python/tests/test_governance_audit.py +359 -0
  331. package/python/tests/test_governance_middleware.py +143 -0
  332. package/python/tests/test_hkc.py +286 -0
  333. package/python/tests/test_hkc_index_build_manager.py +339 -0
  334. package/python/tests/test_hkc_manifest_reuse.py +317 -0
  335. package/python/tests/test_hkc_search_event_sync.py +533 -0
  336. package/python/tests/test_import_path_guard.py +161 -0
  337. package/python/tests/test_injection_guard.py +211 -0
  338. package/python/tests/test_integration.py +141 -0
  339. package/python/tests/test_itinerary.py +103 -0
  340. package/python/tests/test_kee_semantic.py +89 -0
  341. package/python/tests/test_kernel.py +222 -0
  342. package/python/tests/test_learning.py +110 -0
  343. package/python/tests/test_llm_admin.py +160 -0
  344. package/python/tests/test_llm_planner.py +366 -0
  345. package/python/tests/test_markdown_skills.py +73 -0
  346. package/python/tests/test_markdown_skills_openclaw.py +71 -0
  347. package/python/tests/test_mcp_connectors.py +109 -0
  348. package/python/tests/test_mcp_guardian_discovery.py +91 -0
  349. package/python/tests/test_mcp_no_memory.py +43 -0
  350. package/python/tests/test_mcp_policy_gate.py +77 -0
  351. package/python/tests/test_mcp_server.py +95 -0
  352. package/python/tests/test_memory_recall.py +95 -0
  353. package/python/tests/test_metrics.py +66 -0
  354. package/python/tests/test_multi_chain.py +102 -0
  355. package/python/tests/test_multimodal.py +135 -0
  356. package/python/tests/test_negotiation.py +146 -0
  357. package/python/tests/test_orchestration.py +119 -0
  358. package/python/tests/test_orchestration_decoupled.py +109 -0
  359. package/python/tests/test_p3_p5_hardening.py +338 -0
  360. package/python/tests/test_parallel_dispatch.py +215 -0
  361. package/python/tests/test_pipeline.py +533 -0
  362. package/python/tests/test_plan_visible.py +86 -0
  363. package/python/tests/test_planner.py +95 -0
  364. package/python/tests/test_policy_gate.py +109 -0
  365. package/python/tests/test_reasoner.py +185 -0
  366. package/python/tests/test_release_check.py +263 -0
  367. package/python/tests/test_resume_checkpoint.py +156 -0
  368. package/python/tests/test_routing.py +95 -0
  369. package/python/tests/test_scheduler.py +141 -0
  370. package/python/tests/test_search.py +204 -0
  371. package/python/tests/test_secrets.py +159 -0
  372. package/python/tests/test_semantic.py +78 -0
  373. package/python/tests/test_sessions.py +91 -0
  374. package/python/tests/test_settings.py +281 -0
  375. package/python/tests/test_shadow_learning.py +330 -0
  376. package/python/tests/test_shared_write.py +207 -0
  377. package/python/tests/test_skill_taint_gate.py +154 -0
  378. package/python/tests/test_skills.py +133 -0
  379. package/python/tests/test_stats_and_events.py +165 -0
  380. package/python/tests/test_streaming.py +60 -0
  381. package/python/tests/test_supervisor_pipeline.py +177 -0
  382. package/python/tests/test_supervisor_triage.py +108 -0
  383. package/python/tests/test_taint.py +117 -0
  384. package/python/tests/test_task_stream.py +85 -0
  385. package/python/tests/test_thinking_sse.py +67 -0
  386. package/python/tests/test_thinking_stream.py +76 -0
  387. package/python/tests/test_tool_center.py +129 -0
  388. package/python/tests/test_tool_parallel.py +113 -0
  389. package/python/tests/test_tool_params.py +103 -0
  390. package/python/tests/test_tool_use.py +242 -0
  391. package/python/tests/test_toplevel_concurrency.py +117 -0
  392. package/python/tests/test_tracing.py +290 -0
  393. package/python/tests/test_truth_crystallizer.py +127 -0
  394. package/python/tests/test_truth_flow.py +93 -0
  395. package/python/tests/test_url_policy.py +53 -0
  396. package/python/tests/test_verify_evidence.py +225 -0
  397. package/python/tests/test_version_sync.py +71 -0
  398. package/python/tests/test_web_trigger.py +99 -0
  399. package/python/tools/check_faiss_type.py +31 -0
  400. package/python/tools/check_log.ps1 +46 -0
  401. package/python/tools/ivf_status.py +72 -0
  402. package/python/tools/mypy_baseline.txt +5 -0
  403. package/python/tools/mypy_ratchet.py +97 -0
  404. package/python/tools/pack.py +178 -0
  405. package/python/tools/release_check.py +349 -0
  406. package/python/tools/restart_foxclaw.ps1 +74 -0
  407. package/python/tools/shared_write_audit.py +204 -0
  408. package/python/tools/size_baseline.txt +28 -0
  409. package/python/tools/size_check.py +102 -0
  410. package/python/tools/verify_env.bat +2 -0
  411. package/python/tools/verify_env.py +25 -0
  412. package/python/vendor/HMR_LICENSE +37 -0
  413. package/python/vendor/HMR_README.md +65 -0
  414. package/python/vendor/hkc/LICENSE +37 -0
  415. package/python/vendor/hkc/hkc_ace/__init__.py +0 -0
  416. package/python/vendor/hkc/hkc_ace/ace.py +457 -0
  417. package/python/vendor/hkc/hkc_ace/skill_taxonomy.json +465 -0
  418. package/python/vendor/hkc/hkc_api/__init__.py +0 -0
  419. package/python/vendor/hkc/hkc_api/container.py +416 -0
  420. package/python/vendor/hkc/hkc_api/ingest_jobs.py +103 -0
  421. package/python/vendor/hkc/hkc_api/main.py +157 -0
  422. package/python/vendor/hkc/hkc_api/routes/__init__.py +0 -0
  423. package/python/vendor/hkc/hkc_api/routes/ability.py +151 -0
  424. package/python/vendor/hkc/hkc_api/routes/config.py +51 -0
  425. package/python/vendor/hkc/hkc_api/routes/knowledge.py +630 -0
  426. package/python/vendor/hkc/hkc_api/routes/search.py +88 -0
  427. package/python/vendor/hkc/hkc_api/schemas.py +153 -0
  428. package/python/vendor/hkc/hkc_api/synthesis.py +179 -0
  429. package/python/vendor/hkc/hkc_core/__init__.py +0 -0
  430. package/python/vendor/hkc/hkc_core/graph/__init__.py +0 -0
  431. package/python/vendor/hkc/hkc_core/graph/base.py +188 -0
  432. package/python/vendor/hkc/hkc_core/graph/sqlite_store.py +789 -0
  433. package/python/vendor/hkc/hkc_core/models/__init__.py +0 -0
  434. package/python/vendor/hkc/hkc_core/models/enums.py +65 -0
  435. package/python/vendor/hkc/hkc_core/models/ku.py +307 -0
  436. package/python/vendor/hkc/hkc_core/utils/__init__.py +0 -0
  437. package/python/vendor/hkc/hkc_core/utils/id_gen.py +113 -0
  438. package/python/vendor/hkc/hkc_crystallizer/__init__.py +44 -0
  439. package/python/vendor/hkc/hkc_crystallizer/adapters.py +94 -0
  440. package/python/vendor/hkc/hkc_crystallizer/candidate.py +145 -0
  441. package/python/vendor/hkc/hkc_crystallizer/crystallizer.py +179 -0
  442. package/python/vendor/hkc/hkc_crystallizer/ingress.py +90 -0
  443. package/python/vendor/hkc/hkc_kde/__init__.py +0 -0
  444. package/python/vendor/hkc/hkc_kde/chunker.py +197 -0
  445. package/python/vendor/hkc/hkc_kde/extractor.py +623 -0
  446. package/python/vendor/hkc/hkc_kde/kde.py +298 -0
  447. package/python/vendor/hkc/hkc_kde/loaders/__init__.py +0 -0
  448. package/python/vendor/hkc/hkc_kde/loaders/ebook_loader.py +153 -0
  449. package/python/vendor/hkc/hkc_kde/loaders/office_loader.py +194 -0
  450. package/python/vendor/hkc/hkc_kde/loaders/pdf_loader.py +162 -0
  451. package/python/vendor/hkc/hkc_kde/loaders/text_loader.py +157 -0
  452. package/python/vendor/hkc/hkc_kde/loaders/web_loader.py +91 -0
  453. package/python/vendor/hkc/hkc_kde/models.py +74 -0
  454. package/python/vendor/hkc/hkc_kde/packager.py +329 -0
  455. package/python/vendor/hkc/hkc_kde/relation_disc.py +151 -0
  456. package/python/vendor/hkc/hkc_kee/__init__.py +0 -0
  457. package/python/vendor/hkc/hkc_kee/dedup.py +227 -0
  458. package/python/vendor/hkc/hkc_kee/fingerprint.py +106 -0
  459. package/python/vendor/hkc/hkc_kee/kee.py +541 -0
  460. package/python/vendor/hkc/hkc_kep/__init__.py +0 -0
  461. package/python/vendor/hkc/hkc_kep/event_bus.py +205 -0
  462. package/python/vendor/hkc/hkc_sdk/__init__.py +63 -0
  463. package/python/vendor/hkc/hkc_sdk/base.py +151 -0
  464. package/python/vendor/hkc/hkc_sdk/direct_client.py +153 -0
  465. package/python/vendor/hkc/hkc_sdk/exceptions.py +39 -0
  466. package/python/vendor/hkc/hkc_sdk/http_client.py +166 -0
  467. package/python/vendor/hkc/hkc_sdk/models.py +171 -0
  468. package/python/vendor/hkc/hkc_search/__init__.py +0 -0
  469. package/python/vendor/hkc/hkc_search/bm25.py +325 -0
  470. package/python/vendor/hkc/hkc_search/embedding_backends.py +374 -0
  471. package/python/vendor/hkc/hkc_search/graph_search.py +132 -0
  472. package/python/vendor/hkc/hkc_search/hybrid.py +331 -0
  473. package/python/vendor/hkc/hkc_search/index_build_manager.py +378 -0
  474. package/python/vendor/hkc/hkc_search/vector_search.py +484 -0
  475. package/python/vendor/hmr/__init__.py +0 -0
  476. package/python/vendor/hmr/core/__init__.py +0 -0
  477. package/python/vendor/hmr/core/hmr.py +684 -0
  478. package/python/vendor/hmr/core/models.py +310 -0
  479. package/python/vendor/hmr/engines/__init__.py +0 -0
  480. package/python/vendor/hmr/engines/evolution.py +710 -0
  481. package/python/vendor/hmr/engines/jit_compiler.py +472 -0
  482. package/python/vendor/hmr/engines/lifecycle.py +264 -0
  483. package/python/vendor/hmr/engines/policy.py +625 -0
  484. package/python/vendor/hmr/engines/recall.py +260 -0
  485. package/python/vendor/hmr/engines/runtime_state.py +158 -0
  486. package/python/vendor/hmr/engines/scheduler.py +323 -0
  487. package/python/vendor/hmr/engines/semantic.py +150 -0
  488. package/python/vendor/hmr/engines/temporal.py +261 -0
  489. package/python/vendor/hmr/engines/thought_chain.py +635 -0
  490. package/python/vendor/hmr/graph/__init__.py +0 -0
  491. package/python/vendor/hmr/graph/cwg.py +338 -0
  492. package/python/vendor/hmr/graph/memory_graph.py +530 -0
  493. package/python/vendor/hmr/storage/__init__.py +0 -0
  494. package/python/vendor/hmr/storage/memory_fs.py +300 -0
  495. package/python/vendor/hmr/storage/vector_store.py +520 -0
@@ -0,0 +1,142 @@
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
+ # 关闭联网触发(chaos 只测 LLM 咽喉,不触发搜索)
25
+ import foxclaw.core.web_search as ws # noqa: E402
26
+ ws.needs_search = lambda q: False
27
+
28
+ from foxclaw.core.model_router import ModelRouter, PROVIDER_ENV # noqa: E402
29
+
30
+ _SYS = "你是 Foxclaw 助手。"
31
+ _MSG = [{"role": "user", "content": "用一句话总结量子计算"}]
32
+
33
+
34
+ def _make_router(primary="deepseek", fallback=None):
35
+ """构造一个走真实路径(非 stub)的 router,并把 _client 设成非 None,
36
+ 以便 monkeypatch _call_provider 注入故障。fallback 指定次 provider 并伪造其 key。"""
37
+ if fallback:
38
+ os.environ[PROVIDER_ENV[fallback]] = "chaos-test-key"
39
+ r = ModelRouter(provider=primary)
40
+ r._client = object() # 使 is_stub == False
41
+ r._clients[primary] = object()
42
+ if fallback:
43
+ r._clients[fallback] = object()
44
+ return r
45
+
46
+
47
+ def _boom(provider, client, model, system, messages, max_tokens, images=None):
48
+ raise RuntimeError("chaos: upstream timeout")
49
+
50
+
51
+ def case1_empty_response():
52
+ """[1] 空响应故障:provider 返回空串 → degraded + 显式失败文案,不伪装成功。"""
53
+ r = _make_router("deepseek")
54
+ r._call_provider = lambda *a, **k: ("", None)
55
+ out = r.complete(_SYS, _MSG)
56
+ ok = ("【模型返回为空】" in out) and r.is_degraded()
57
+ return ok, ("degraded=" + str(r.is_degraded())), out[:40]
58
+
59
+
60
+ def case2_call_failure():
61
+ """[2] 调用异常故障:LLM 抛异常 → 全部失败文案 + degraded,绝不拼接 stub 成功文案。"""
62
+ r = _make_router("deepseek")
63
+ r._call_provider = _boom
64
+ out = r.complete(_SYS, _MSG)
65
+ ok = ("【全部模型失败】" in out) and r.is_degraded() and ("已完成" not in out)
66
+ return ok, ("degraded=" + str(r.is_degraded()) + " 伪装成功=" + str("已完成" in out)), out[:40]
67
+
68
+
69
+ def case3_fallback_chain():
70
+ """[3] fallback 链:主 provider 抛异常 → 自动回退到已配次 provider 并记录原因。"""
71
+ r = _make_router("deepseek", fallback="openai")
72
+ calls = []
73
+
74
+ def _flaky(provider, client, model, system, messages, max_tokens, images=None):
75
+ calls.append(provider)
76
+ if provider == "deepseek":
77
+ raise RuntimeError("chaos: deepseek down")
78
+ return "openai 兜底回复", None
79
+
80
+ r._call_provider = _flaky
81
+ out = r.complete(_SYS, _MSG)
82
+ ok = (out == "openai 兜底回复") and (calls[-1] == "openai") and bool(r.fallback_reason)
83
+ return ok, ("调用顺序=" + str(calls) + " 回退原因=" + r.fallback_reason), out[:40]
84
+
85
+
86
+ def case4_circuit_breaker():
87
+ """[4] 断路器:provider 失败后进入冷却,立即重试被跳过(不再紧密重试)。"""
88
+ r = _make_router("deepseek")
89
+ r._call_provider = _boom
90
+ r.complete(_SYS, _MSG) # 第一次失败 → 进入 cooldown
91
+ cooled = "deepseek" in r._cooldown
92
+ t0 = time.perf_counter()
93
+ r.complete(_SYS, _MSG) # 第二次:cooldown 未过期 → 直接跳过,不等超时
94
+ fast = (time.perf_counter() - t0) < 0.5 # 跳过热 provider,应远快于真实超时
95
+ ok = cooled and fast and r.is_degraded()
96
+ return ok, (f"冷却中={cooled} 跳过耗时={((time.perf_counter()-t0)*1000):.0f}ms"), "断路器生效"
97
+
98
+
99
+ def case5_recovery():
100
+ """[5] 恢复:故障清除(cooldown 过期/成功)后,同一 provider 恢复正常服务。"""
101
+ r = _make_router("deepseek")
102
+ r._call_provider = _boom
103
+ r.complete(_SYS, _MSG)
104
+ assert "deepseek" in r._cooldown # 先确认在冷却
105
+ r._cooldown["deepseek"] = 0 # 模拟冷却过期
106
+ r._call_provider = lambda *a, **k: ("恢复后的正常回复", None)
107
+ out = r.complete(_SYS, _MSG)
108
+ # 注:degraded 是「曾降级」的累积运维标志(不因恢复清零),恢复语义看「解冻 + 成功返回」。
109
+ ok = (out == "恢复后的正常回复") and ("deepseek" not in r._cooldown)
110
+ return ok, ("解冻=" + str("deepseek" not in r._cooldown)), out[:40]
111
+
112
+
113
+ def main():
114
+ cases = [
115
+ ("空响应 → degraded(不伪装成功)", case1_empty_response),
116
+ ("调用异常 → 全部失败(不拼接 stub)", case2_call_failure),
117
+ ("fallback 链(主失败→次兜底)", case3_fallback_chain),
118
+ ("断路器(失败进冷却·重试跳过)", case4_circuit_breaker),
119
+ ("恢复(冷却过期→正常服务)", case5_recovery),
120
+ ]
121
+ print("=" * 64)
122
+ print("Foxclaw · 混沌故障注入(ModelRouter 咽喉)")
123
+ print("=" * 64)
124
+ n_ok = 0
125
+ for name, fn in cases:
126
+ try:
127
+ ok, detail, sample = fn()
128
+ n_ok += 1 if ok else 0
129
+ print(f"\n[{name}]")
130
+ print(f" {'✓ 通过' if ok else '✗ 失败'} {detail}")
131
+ print(f" 输出样例: {sample}…")
132
+ except Exception as e:
133
+ print(f"\n[{name}]\n ✗ 异常: {type(e).__name__}: {e}")
134
+ print("\n" + "=" * 64)
135
+ print(f"结论: {n_ok}/{len(cases)} 混沌用例通过 — "
136
+ f"{'✓ 故障下降级语义正确、fallback/断路器/恢复均生效' if n_ok == len(cases) else '✗ 存在故障处理缺口'}")
137
+ print("=" * 64)
138
+ return 0 if n_ok == len(cases) else 1
139
+
140
+
141
+ if __name__ == "__main__":
142
+ sys.exit(main())
@@ -0,0 +1,103 @@
1
+ """
2
+ M2 · 并发压测(数字产物)
3
+
4
+ 回答路线图 M2 的量化问题:
5
+ · 并行 fork-join 相对顺序,墙钟省多少?(latency)
6
+ · 共享库(ThoughtChain/ProcessManager/SystemBus/HMR)在多线程并发写下的吞吐,
7
+ 以及 `database is locked` 计数(应为 0)。
8
+
9
+ 离线:不联网、LLM 用带固定延迟的 stub。
10
+ 用法:HF_HUB_OFFLINE=1 python bench/concurrency.py
11
+ """
12
+ import asyncio
13
+ import sys
14
+ import tempfile
15
+ import threading
16
+ import time
17
+ from pathlib import Path
18
+ sys.path.insert(0, str(Path(__file__).parent.parent))
19
+
20
+ import foxclaw.core.web_search as ws
21
+ ws.needs_search = lambda q: False
22
+
23
+ from foxclaw import Foxclaw
24
+ from foxclaw.core.thought_chain import ThoughtNode
25
+
26
+ SLEEP = 0.4
27
+ PAR_TASK = "同时查今天的日程、分析本月理财、总结这篇论文"
28
+ SEQ_TASK = "先查今天的日程然后分析本月理财然后总结这篇论文"
29
+
30
+
31
+ def _slow(system, messages, max_tokens=2048, agent_id=""):
32
+ time.sleep(SLEEP)
33
+ return "已处理"
34
+
35
+
36
+ def bench_latency(os_):
37
+ print("\n[1] 并行 fork-join vs 顺序(每个子任务 LLM stub 固定 " f"{SLEEP*1000:.0f}ms)")
38
+ r = asyncio.run(os_.supervisor.run_task(PAR_TASK))
39
+ par = r["plan"]["wall_ms"]
40
+ t0 = time.perf_counter()
41
+ asyncio.run(os_.supervisor.run_task(SEQ_TASK))
42
+ seq = (time.perf_counter() - t0) * 1000
43
+ print(f" 顺序墙钟 : {seq:8.0f} ms")
44
+ print(f" 并行墙钟 : {par:8.0f} ms")
45
+ print(f" 加速比 : {seq/par:8.2f}× (3 子任务跨 3 个 Agent)")
46
+
47
+
48
+ def bench_concurrency(os_, threads=12, per_thread=20):
49
+ print(f"\n[2] 共享库并发写压测({threads} 线程 × {per_thread} 轮)")
50
+ tc, pm, bus, hmr = (os_.thought_chain, os_.process_manager,
51
+ os_.system_bus, os_.hmr)
52
+ locked = {"n": 0}
53
+ errors = []
54
+ lk = threading.Lock()
55
+
56
+ def worker(k):
57
+ for i in range(per_thread):
58
+ try:
59
+ node = ThoughtNode.create(session_id=f"b{k}", agent_id="bench",
60
+ type="reasoning", content=f"n{k}_{i}", conf=0.9)
61
+ tc.add_node(node); tc.update_harness(node.id, "PASS")
62
+ p = pm.create(f"t{k}_{i}", agent_id=f"a{k}", schedule={"priority": 1})
63
+ pm.mark_running(p.process_id); pm.mark_completed(p.process_id)
64
+ bus.publish("bench.evt", {"k": k, "i": i})
65
+ hmr.ingest(f"记忆{k}_{i}", type="concept", agent_id=f"a{k}", title=f"m{k}_{i}")
66
+ except Exception as e:
67
+ msg = repr(e)
68
+ if "database is locked" in msg:
69
+ with lk:
70
+ locked["n"] += 1
71
+ with lk:
72
+ errors.append(msg)
73
+
74
+ total = threads * per_thread
75
+ t0 = time.perf_counter()
76
+ ts = [threading.Thread(target=worker, args=(k,)) for k in range(threads)]
77
+ for t in ts: t.start()
78
+ for t in ts: t.join()
79
+ dur = time.perf_counter() - t0
80
+
81
+ with tc._conn() as c:
82
+ nodes = c.execute("SELECT COUNT(*) FROM thought_nodes WHERE agent_id='bench'"
83
+ ).fetchone()[0]
84
+ print(f" 总写入组 : {total} (每组 = 1 审计节点 + 1 进程流转 + 1 事件 + 1 记忆)")
85
+ print(f" 耗时 : {dur*1000:8.0f} ms 吞吐 {total/dur:8.0f} 组/s")
86
+ print(f" 审计落库 : {nodes} / {total} {'✓ 不丢' if nodes >= total else '✗ 丢失'}")
87
+ print(f" database is locked 次数 : {locked['n']} {'✓' if locked['n']==0 else '✗'}")
88
+ print(f" 其它异常 : {len(errors)} {'✓ 无' if not errors else errors[:1]}")
89
+
90
+
91
+ def main():
92
+ os_ = Foxclaw(data_dir=tempfile.mkdtemp())
93
+ os_.router.complete = _slow
94
+ print("=" * 60)
95
+ print("Foxclaw · M2 并发压测")
96
+ print("=" * 60)
97
+ bench_latency(os_)
98
+ bench_concurrency(os_)
99
+ print("\n" + "=" * 60)
100
+
101
+
102
+ if __name__ == "__main__":
103
+ main()
@@ -0,0 +1,95 @@
1
+ """
2
+ 真实多模型 E2E 冒烟(非 stub)· 数字产物
3
+
4
+ 回答步骤 10「真实多模型 E2E」:逐个探测已配 Key 的 provider,各跑一条**真实**最小消息,
5
+ 验证「非 stub、非降级、有真实输出」,产出量化报告。
6
+
7
+ opt-in:只对有真实 Key 的 provider 打真模型(要联网、会消耗额度);一个 Key 都没配则
8
+ 优雅跳过(exit 0)——CI/离线环境绝不误判失败。
9
+
10
+ 用法:
11
+ python bench/e2e_real_models.py # 只测当前环境已配 Key 的 provider
12
+ FOXCLAW_E2E_SKIP_PROVIDER=google python ... # 跳过某 provider(如无 SDK/额度)
13
+ """
14
+ import os
15
+ import sys
16
+ import time
17
+ from pathlib import Path
18
+
19
+ sys.path.insert(0, str(Path(__file__).parent.parent))
20
+
21
+ import foxclaw.core.web_search as ws # noqa: E402
22
+ ws.needs_search = lambda q: False
23
+
24
+ from foxclaw.core.model_router import ModelRouter # noqa: E402
25
+
26
+ _SYS = "你是 Foxclaw 助手。"
27
+ _MSG = [{"role": "user", "content": "请只回复两个字:你好"}]
28
+ _MAX_TOKENS = 64
29
+ # stub/故障 的特征文案(出现即视为"非真实成功")
30
+ _BAD_MARKERS = ("【未执行】", "【模型返回为空】", "【全部模型失败】", "【模型流式中断】", "STUB")
31
+
32
+
33
+ def _smoke(provider: str) -> dict:
34
+ """对一个 provider 跑一条真实最小消息,返回观测结果。"""
35
+ t0 = time.perf_counter()
36
+ r = ModelRouter(provider=provider)
37
+ info = r.info()
38
+ if not info.get("client_ready"):
39
+ return {"provider": provider, "ok": False, "lat_ms": 0,
40
+ "output": "", "reason": "client 未就绪(SDK 缺失或无 Key): " +
41
+ (info.get("fallback_reason") or "未知")}
42
+ try:
43
+ out = r.complete(_SYS, _MSG, max_tokens=_MAX_TOKENS, agent_id="bench")
44
+ lat = (time.perf_counter() - t0) * 1000
45
+ except Exception as e:
46
+ return {"provider": provider, "ok": False, "lat_ms": (time.perf_counter() - t0) * 1000,
47
+ "output": "", "reason": f"调用异常: {type(e).__name__}: {e}"}
48
+
49
+ degraded = r.take_call_degraded()
50
+ bad = any(m in out for m in _BAD_MARKERS)
51
+ ok = bool(out) and not degraded and not bad
52
+ reason = ""
53
+ if degraded:
54
+ reason = "本次调用降级(degraded)"
55
+ elif bad:
56
+ reason = "输出含降级/失败文案"
57
+ return {"provider": provider, "ok": ok, "lat_ms": lat, "output": out, "reason": reason}
58
+
59
+
60
+ def main():
61
+ skip = {s.strip().lower() for s in os.environ.get("FOXCLAW_E2E_SKIP_PROVIDER", "").split(",")
62
+ if s.strip()}
63
+ configured = [p["provider"] for p in ModelRouter.available_providers()
64
+ if p["configured"] and p["provider"] not in skip]
65
+
66
+ print("=" * 64)
67
+ print("Foxclaw · 真实多模型 E2E 冒烟(非 stub)")
68
+ print("=" * 64)
69
+
70
+ if not configured:
71
+ print("未检测到任何已配置 Key 的 provider(见 .env.example)。")
72
+ print("结论: 跳过(无真实模型可测)——离线/无额度环境属预期,非失败。")
73
+ print("=" * 64)
74
+ return 0
75
+
76
+ print(f"检测到 {len(configured)} 个 provider: {', '.join(configured)}\n")
77
+ n_ok = 0
78
+ for p in configured:
79
+ r = _smoke(p)
80
+ mark = "✓" if r["ok"] else "✗"
81
+ n_ok += 1 if r["ok"] else 0
82
+ sample = (r["output"].strip().replace("\n", " ")[:36]) if r["output"] else "(空)"
83
+ print(f"[{mark}] {p:12s} {r['lat_ms']:7.0f} ms → {sample}")
84
+ if r["reason"]:
85
+ print(f" 原因: {r['reason']}")
86
+
87
+ print("\n" + "=" * 64)
88
+ print(f"结论: {n_ok}/{len(configured)} 个真实模型冒烟通过 — "
89
+ f"{'✓ 多模型真实链路可用' if n_ok == len(configured) else '✗ 有 provider 未通过(见上)'}")
90
+ print("=" * 64)
91
+ return 0 if n_ok == len(configured) else 1
92
+
93
+
94
+ if __name__ == "__main__":
95
+ sys.exit(main())
@@ -0,0 +1,248 @@
1
+ """
2
+ bench/llm_retry_test.py
3
+ C 阶段 _retry_llm helper 单元测试。
4
+
5
+ 覆盖:
6
+ 1. 正常调用 1 次成功
7
+ 2. 429 抛 1 次后成功
8
+ 3. 429 抛 max_retries 次后 raise
9
+ 4. 非 retryable 异常立即 raise
10
+ 5. Retry-After header 优先于 backoff
11
+ 6. 5xx 也 retry
12
+ 7. ConnectionError / Timeout retry
13
+ 8. 指数退避 (backoff 1→2→4) + max_delay 上限
14
+ 9. openai RateLimitError (name 含 RateLimit) 也 retry
15
+ """
16
+ import os, sys, time
17
+
18
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'vendor'))
19
+ from hkc.hkc_kde.extractor import Extractor
20
+
21
+
22
+ # ── Mock 异常类型 ────────────────────────────────────
23
+
24
+ class FakeResponse:
25
+ def __init__(self, headers=None):
26
+ self.headers = headers or {}
27
+
28
+
29
+ class Fake429Error(Exception):
30
+ def __init__(self, msg="rate limited", retry_after=None):
31
+ super().__init__(msg)
32
+ self.status_code = 429
33
+ self.headers = {"Retry-After": str(retry_after)} if retry_after else {}
34
+
35
+
36
+ class Fake5xxError(Exception):
37
+ def __init__(self, msg="server error", status=500):
38
+ super().__init__(msg)
39
+ self.status_code = status
40
+
41
+
42
+ class FakeTimeoutError(TimeoutError):
43
+ def __init__(self, msg="timed out"):
44
+ super().__init__(msg)
45
+ self.status_code = None
46
+
47
+
48
+ class FakeConnError(ConnectionError):
49
+ pass
50
+
51
+
52
+ class FakeRateLimitName(Exception):
53
+ """模拟 openai.RateLimitError(type name 含 'RateLimit',无 status_code)"""
54
+ def __init__(self, msg="rate"):
55
+ super().__init__(msg)
56
+ # 不设 status_code;靠 type name 'RateLimit' 识别
57
+
58
+
59
+ class FakeValueError(Exception):
60
+ """非 retryable"""
61
+ def __init__(self, msg="bad request"):
62
+ super().__init__(msg)
63
+ self.status_code = 400
64
+
65
+
66
+ # ── 测试实例化 ───────────────────────────────────────
67
+ # KnowledgeExtractor __init__ 不调 LLM,只是记 provider/model 配置
68
+ # _retry_llm 是 instance method,任意实例都能调
69
+ ext = Extractor(provider="mock", model="mock-model")
70
+
71
+
72
+ def make_func(side_effects):
73
+ """side_effects: 异常类或值,依次抛出,最后返回 'ok' 或 raise"""
74
+ call_count = [0]
75
+ sleeps = []
76
+
77
+ def _func(*args, **kwargs):
78
+ i = call_count[0]
79
+ call_count[0] += 1
80
+ if i < len(side_effects):
81
+ item = side_effects[i]
82
+ if isinstance(item, Exception):
83
+ raise item
84
+ return item
85
+ return "ok"
86
+
87
+ return _func, call_count, sleeps
88
+
89
+
90
+ def patch_sleep():
91
+ """monkey-patch time.sleep 不真睡,记录时长"""
92
+ import hkc.hkc_kde.extractor as ex
93
+ original = ex.time.sleep
94
+ sleeps = []
95
+ def fake_sleep(s):
96
+ sleeps.append(s)
97
+ ex.time.sleep = fake_sleep
98
+ return ex, original, sleeps
99
+
100
+
101
+ def restore_sleep(ex, original):
102
+ ex.time.sleep = original
103
+
104
+
105
+ def t1_ok():
106
+ """1. 正常调用 1 次成功"""
107
+ func, calls, _ = make_func(["ok"])
108
+ r = ext._retry_llm(func, max_retries=3, base_delay=0.01)
109
+ assert r == "ok" and calls[0] == 1
110
+ print(" 1. ok 1 次成功: PASS")
111
+
112
+
113
+ def t2_429_retry_success():
114
+ """2. 429 抛 1 次后成功"""
115
+ func, calls, _ = make_func([Fake429Error(), "ok"])
116
+ ex, original, sleeps = patch_sleep()
117
+ try:
118
+ r = ext._retry_llm(func, max_retries=3, base_delay=0.01, max_delay=0.1)
119
+ assert r == "ok" and calls[0] == 2
120
+ assert len(sleeps) == 1 and sleeps[0] == 0.01
121
+ print(" 2. 429 1 次后成功 + sleep 0.01: PASS")
122
+ finally:
123
+ restore_sleep(ex, original)
124
+
125
+
126
+ def t3_429_exhaust():
127
+ """3. 429 抛 max_retries+1 次后 raise"""
128
+ func, calls, _ = make_func([Fake429Error()] * 5)
129
+ ex, original, sleeps = patch_sleep()
130
+ try:
131
+ try:
132
+ ext._retry_llm(func, max_retries=3, base_delay=0.001, max_delay=0.01)
133
+ assert False, "应该 raise"
134
+ except Fake429Error:
135
+ pass
136
+ assert calls[0] == 4 # 1 + 3 retries
137
+ assert len(sleeps) == 3 # 3 次 retry 才 sleep
138
+ print(" 3. 429 4 次后 raise (1+3 retry): PASS, calls={}, sleeps={}".format(calls[0], sleeps))
139
+ finally:
140
+ restore_sleep(ex, original)
141
+
142
+
143
+ def t4_non_retryable():
144
+ """4. 非 retryable (400) 立即 raise"""
145
+ func, calls, _ = make_func([FakeValueError()])
146
+ ex, original, sleeps = patch_sleep()
147
+ try:
148
+ try:
149
+ ext._retry_llm(func, max_retries=3, base_delay=0.01)
150
+ assert False, "应该 raise"
151
+ except FakeValueError:
152
+ pass
153
+ assert calls[0] == 1
154
+ assert len(sleeps) == 0
155
+ print(" 4. 400 立即 raise 不 sleep: PASS")
156
+ finally:
157
+ restore_sleep(ex, original)
158
+
159
+
160
+ def t5_retry_after():
161
+ """5. Retry-After header 优先于 backoff"""
162
+ func, calls, _ = make_func([Fake429Error(retry_after=0.05), "ok"])
163
+ ex, original, sleeps = patch_sleep()
164
+ try:
165
+ r = ext._retry_llm(func, max_retries=3, base_delay=1.0, max_delay=10.0)
166
+ assert r == "ok"
167
+ assert len(sleeps) == 1
168
+ assert sleeps[0] == 0.05, f"expected 0.05, got {sleeps[0]}"
169
+ print(" 5. Retry-After=0.05 优先 backoff=1.0: PASS, slept={}".format(sleeps))
170
+ finally:
171
+ restore_sleep(ex, original)
172
+
173
+
174
+ def t6_5xx():
175
+ """6. 5xx (500) retry"""
176
+ func, calls, _ = make_func([Fake5xxError(status=500), Fake5xxError(status=503), "ok"])
177
+ ex, original, sleeps = patch_sleep()
178
+ try:
179
+ r = ext._retry_llm(func, max_retries=3, base_delay=0.01, max_delay=0.1)
180
+ assert r == "ok" and calls[0] == 3
181
+ assert sleeps == [0.01, 0.02]
182
+ print(" 6. 5xx 500+503 后成功: PASS, sleeps={}".format(sleeps))
183
+ finally:
184
+ restore_sleep(ex, original)
185
+
186
+
187
+ def t7_conn_timeout():
188
+ """7. ConnectionError / Timeout retry"""
189
+ for cls, name in [(FakeConnError, "ConnectionError"), (FakeTimeoutError, "Timeout")]:
190
+ func, calls, _ = make_func([cls(), "ok"])
191
+ ex, original, sleeps = patch_sleep()
192
+ try:
193
+ r = ext._retry_llm(func, max_retries=3, base_delay=0.01)
194
+ assert r == "ok" and calls[0] == 2
195
+ assert len(sleeps) == 1
196
+ print(" 7. {} retry: PASS".format(name))
197
+ finally:
198
+ restore_sleep(ex, original)
199
+
200
+
201
+ def t8_backoff():
202
+ """8. 指数退避 1→2→4 + max_delay 上限"""
203
+ func, calls, _ = make_func([Fake429Error()] * 4)
204
+ ex, original, sleeps = patch_sleep()
205
+ try:
206
+ try:
207
+ ext._retry_llm(func, max_retries=3, base_delay=1.0, max_delay=3.0)
208
+ except Fake429Error:
209
+ pass
210
+ # 3 次 retry:sleeps = [1, 2, 3] (4 cap at max_delay=3)
211
+ assert sleeps == [1.0, 2.0, 3.0], f"expected [1,2,3], got {sleeps}"
212
+ print(" 8. 退避 1→2→3 (max_delay cap): PASS, sleeps={}".format(sleeps))
213
+ finally:
214
+ restore_sleep(ex, original)
215
+
216
+
217
+ def t9_rate_limit_name():
218
+ """9. type name 含 'RateLimit' (openai.RateLimitError 风格) 也 retry"""
219
+ func, calls, _ = make_func([FakeRateLimitName(), "ok"])
220
+ ex, original, sleeps = patch_sleep()
221
+ try:
222
+ r = ext._retry_llm(func, max_retries=3, base_delay=0.01)
223
+ assert r == "ok" and calls[0] == 2
224
+ print(" 9. RateLimit class name → retry: PASS")
225
+ finally:
226
+ restore_sleep(ex, original)
227
+
228
+
229
+ def main():
230
+ print("=" * 60)
231
+ print("C 阶段 _retry_llm helper 单元测试 (9 项)")
232
+ print("=" * 60)
233
+ t1_ok()
234
+ t2_429_retry_success()
235
+ t3_429_exhaust()
236
+ t4_non_retryable()
237
+ t5_retry_after()
238
+ t6_5xx()
239
+ t7_conn_timeout()
240
+ t8_backoff()
241
+ t9_rate_limit_name()
242
+ print("=" * 60)
243
+ print("全部 9 项 PASS")
244
+ print("=" * 60)
245
+
246
+
247
+ if __name__ == "__main__":
248
+ main()