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,516 @@
1
+ """
2
+ HMR 适配层 — 把 Foxclaw 的统一调用接口翻译成 HMR v2.0 的真实 API。
3
+
4
+ 接口差异:
5
+ Foxclaw 调用 HMR v2.0 真实 API
6
+ ───────────────────────────────── ──────────────────────────────────
7
+ ingest(content, type=, agent_id=) ingest(content, memory_type=, metadata=)
8
+ recall(query, agent_id=, top_k=) recall(query, top_k=) -> RecallResult
9
+ get_workspace(agent_id) get_workspace(agent_id) -> AgentWorkspace
10
+
11
+ Agent 隔离:HMR v2.0 的 ingest/recall 无 agent_id 概念,
12
+ 适配层用 metadata.tags 注入 `agent:<id>` 标签实现按 Agent 区隔,
13
+ recall 后按标签过滤,保证五个 Agent 记忆互不污染。
14
+ """
15
+
16
+ import os
17
+ import re
18
+ import sys
19
+ import threading
20
+ from pathlib import Path
21
+
22
+ from .shared_write import guard_shared_write
23
+
24
+
25
+ def _ensure_hmr_importable():
26
+ """把 vendor/ 加入 sys.path,使内嵌的 HMR 可被导入。
27
+ 同时把 vendor/hkc 也加入,使 HMR 内部能 `from hkc_search.embedding_backends import get_st_model`
28
+ 走 HKC 进程缓存(同名 instance 只 load 1 次)—— 否则会兜底自己 SentenceTransformer Loading 第 2 次。
29
+ """
30
+ vendor = Path(__file__).resolve().parent.parent.parent / "vendor"
31
+ if vendor.exists() and str(vendor) not in sys.path:
32
+ sys.path.insert(0, str(vendor))
33
+ vendor_hkc = vendor / "hkc"
34
+ if vendor_hkc.exists() and str(vendor_hkc) not in sys.path:
35
+ sys.path.insert(0, str(vendor_hkc))
36
+
37
+
38
+ class HMRAdapter:
39
+ """
40
+ 包装真实 HMR v2.0,对外暴露 Foxclaw 期望的统一接口。
41
+ 若 HMR 不可用,调用方应回退到 MockHMR。
42
+ """
43
+
44
+ def __init__(self, storage_path: str = "./hmr_data", llm_api_key: str = None,
45
+ embedding_model: str = None):
46
+ """【P1·share-bge-m3】新增 embedding_model 参数。
47
+ 上层(Foxclaw)通常在 HKC 已装配后再调本构造,bge-m3 已入 HKC 缓存;
48
+ 传具体模型名(如 HKC_EMBEDDING_MODEL)使 HMR 与 HKC 共享同一 SentenceTransformer 实例。
49
+ 不传则走 vendor/hmr/storage/vector_store.py 的 env 解析链(HKC_EMBEDDING_MODEL>bge-m3)。"""
50
+ _ensure_hmr_importable()
51
+ from hmr.core.hmr import HMR # 内嵌包
52
+
53
+ # 不传 None 就用 .env / HKC 的配置;只有上层显式 override 才用 _embedding_model
54
+ import os as _os
55
+ resolved_model = (embedding_model
56
+ or _os.environ.get("EMBEDDING_MODEL") # 【P2·单 embedding 入口】最高优先级
57
+ or _os.environ.get("HKC_EMBEDDING_MODEL")
58
+ or _os.environ.get("FOXCLAW_EMBEDDING_MODEL")
59
+ or "all-MiniLM-L6-v2")
60
+ self._hmr = HMR(storage_path=storage_path, llm_api_key=llm_api_key,
61
+ embedding_model=resolved_model)
62
+ self.backend = "HMR"
63
+ self.version = getattr(self._hmr, "VERSION", "2.0.0")
64
+ # 并行子 Agent 会并发 ingest/recall,底层 HMR 的向量库/记忆图并非线程安全
65
+ # (向量索引并发 add+search 是未定义行为)。串行化对 HMR 的访问。叶子组件:
66
+ # 锁内不回调 pm/bus,故不参与全局锁序,无死锁风险。
67
+ self._lock = threading.RLock()
68
+
69
+ # HMR 未给 title 时会退化成 "Concept 03:11" 这类纯时间戳(见 hmr.py 的
70
+ # `title or f"{type.title()} {HH:MM}"`),在前端"记忆信息条"里一眼看不出是什么。
71
+ _AUTO_TITLE = re.compile(r"^[A-Za-z_]+\s+\d{1,2}:\d{2}$")
72
+
73
+ @staticmethod
74
+ def _derive_title(content: str) -> str:
75
+ """从内容提炼可读标题:剥前缀标签 → 取"问"侧 → 截到首个句读 → 限长。"""
76
+ s = str(content or "").strip()
77
+ if not s:
78
+ return ""
79
+ for pfx in ("对话:", "对话:", "用户偏好:", "用户偏好:", "任务:", "任务:"):
80
+ if s.startswith(pfx):
81
+ s = s[len(pfx):].strip()
82
+ break
83
+ for sep in ("→", "->"): # 对话记忆是"问 → 答",标题取问
84
+ if sep in s:
85
+ s = s.split(sep, 1)[0].strip()
86
+ break
87
+ s = re.split(r"[。!?!?\n\r]", s, 1)[0].strip()
88
+ return " ".join(s.split())[:24]
89
+
90
+ # ── 记忆摄入 ──────────────────────────────────────────
91
+ def ingest(self, content: str, type: str = "concept", agent_id: str = None,
92
+ title: str = None, tags: list = None, shared: bool = False, **kwargs):
93
+ """统一摄入接口。agent_id 写入 tags 实现隔离;shared=True 则写入共享层。
94
+ 未给 title 时从内容提炼一个可读标题(否则 HMR 会生成纯时间戳标题)。"""
95
+ meta_tags = list(tags or [])
96
+ if agent_id:
97
+ meta_tags.append(f"agent:{agent_id}")
98
+ if shared:
99
+ meta_tags.append("shared") # 共享记忆:所有 Agent 可召回
100
+ metadata = {"tags": meta_tags}
101
+ if "confidence" in kwargs:
102
+ metadata["confidence"] = kwargs["confidence"]
103
+ valid = {"concept", "project", "decision", "runtime", "task",
104
+ "workflow", "agent_memory", "execution", "reflection"}
105
+ mtype = type if type in valid else "concept"
106
+ if not title:
107
+ title = self._derive_title(content) or None
108
+ # 【P0-3】共享层写入闸门:必须在**落库之前**判定(见 core/shared_write.py)。
109
+ # 未挂策略时返回 None 直接放行;audit 只记事件;enforce 才抛 SharedWriteDenied。
110
+ if shared:
111
+ guard_shared_write(self, agent_id, content, mtype, meta_tags)
112
+ with self._lock:
113
+ return self._hmr.ingest(content, memory_type=mtype, title=title, metadata=metadata)
114
+
115
+ # ── 记忆召回 ──────────────────────────────────────────
116
+ def recall(self, query: str = "", agent_id: str = None, top_k: int = 5,
117
+ include_shared: bool = True, **kwargs):
118
+ """统一召回接口。返回 list[dict]。
119
+ 默认召回:本 Agent 的记忆 + 共享层(shared)记忆;include_shared=False 只看自己。
120
+
121
+ M4 根治(隔离召回完整性):优先走 tag 预过滤路径 —— 先把候选池缩到"本 Agent + shared"
122
+ 的**全集**,再在这个子集内按语义相似度排序取 top_k。这样本 Agent 的记忆一定进入排序,
123
+ 不会因"库中其他 Agent 有大量强相关记忆挤占全局 top-k"而被漏掉(老路径的完整性缺陷)。
124
+ 底层 HMR 结构不可用(如 MockHMR / 版本差异)时,回退到老的"全局召回再过滤"路径。
125
+ 隔离性两条路径都成立:只有带 agent:<id> 或 shared 标签的记忆才会返回。"""
126
+ filtered = self._filtered_recall(query, agent_id, top_k, include_shared)
127
+ if filtered is not None:
128
+ return filtered
129
+ return self._legacy_recall(query, agent_id, top_k, include_shared)
130
+
131
+ def recall_by_tag(self, agent_id=None, tag="preference", limit=8, include_shared=True):
132
+ """按 **tag** 拉记忆(**不做语义排序**)——用于"总是注入"的偏好类记忆:不管当前 query 语义,
133
+ 只要带该 tag 且属本 Agent + shared,就按新近取近 limit 条。这正是普通语义 recall 做不到的
134
+ (语义不相近就召不回),是"偏好优先召回"的核心。**用 tag 而非 type**:tag 是自由 metadata、
135
+ 不受 `ingest` 的 memory_type 白名单限制(preference 不在白名单会被降级 concept)。结构不可用则 []。"""
136
+ fs = getattr(self._hmr, "memory_fs", None)
137
+ if fs is None or not hasattr(fs, "list_memories"):
138
+ return []
139
+ try:
140
+ with self._lock:
141
+ out = []
142
+ for m in fs.list_memories():
143
+ tags = getattr(m, "tags", []) or []
144
+ if tag not in tags:
145
+ continue
146
+ if agent_id:
147
+ own = f"agent:{agent_id}" in tags
148
+ shared = include_shared and ("shared" in tags)
149
+ if not (own or shared):
150
+ continue
151
+ out.append({
152
+ "id": getattr(m, "id", ""), "content": getattr(m, "content", ""),
153
+ "tags": tags,
154
+ "_ts": getattr(m, "created_at", None) or getattr(m, "timestamp", None) or "",
155
+ })
156
+ out.sort(key=lambda x: str(x["_ts"]), reverse=True) # 新近优先
157
+ # 按内容去重(保留最新):否则同一偏好反复表达存成多条,会挤满 limit、把别的偏好挤出召回
158
+ deduped, seen_c = [], set()
159
+ for x in out:
160
+ if x["content"] in seen_c:
161
+ continue
162
+ seen_c.add(x["content"])
163
+ deduped.append(x)
164
+ return deduped[:limit]
165
+ except Exception:
166
+ return []
167
+
168
+ def delete_memory(self, memory_id):
169
+ """删一条记忆(memory 文件 + 向量)——用于误存偏好的清理(F3)。返回是否删到 memory 文件。
170
+ 向量删失败不影响召回正确性:recall 从 list_memories 迭代,文件删了就不会再命中;向量残留
171
+ 仅占空间,一并清掉更干净。"""
172
+ fs = getattr(self._hmr, "memory_fs", None)
173
+ vs = getattr(self._hmr, "vector_store", None)
174
+ ok = False
175
+ with self._lock:
176
+ if fs is not None and hasattr(fs, "delete_memory"):
177
+ try:
178
+ ok = bool(fs.delete_memory(memory_id))
179
+ except Exception:
180
+ ok = False
181
+ if vs is not None and hasattr(vs, "delete"):
182
+ try:
183
+ vs.delete(memory_id)
184
+ except Exception:
185
+ pass
186
+ return ok
187
+
188
+ def _filtered_recall(self, query, agent_id, top_k, include_shared):
189
+ """根治路径:在"本 Agent + shared"全集内语义排序。结构不可用则返回 None(触发回退)。"""
190
+ vs = getattr(self._hmr, "vector_store", None)
191
+ fs = getattr(self._hmr, "memory_fs", None)
192
+ if vs is None or fs is None or not hasattr(vs, "vectors") \
193
+ or not hasattr(vs, "embedder") or not hasattr(fs, "list_memories"):
194
+ return None
195
+ try:
196
+ with self._lock:
197
+ mems = fs.list_memories()
198
+ cand = [] # tag 预过滤:本 Agent 私有 + 共享
199
+ for m in mems:
200
+ tags = getattr(m, "tags", []) or []
201
+ if agent_id:
202
+ own = f"agent:{agent_id}" in tags
203
+ shared = include_shared and ("shared" in tags)
204
+ if not (own or shared):
205
+ continue
206
+ cand.append((m, tags))
207
+ if not cand:
208
+ return []
209
+ qv = vs.embedder.embed(query or "")
210
+ scored = []
211
+ for m, tags in cand:
212
+ vec = vs.vectors.get(getattr(m, "id", None))
213
+ if vec is None: # 少见:该记忆尚无向量 → 现算
214
+ try:
215
+ text = getattr(m, "semantic_summary", "") or (m.content or "")[:500]
216
+ vec = vs.embedder.embed(text)
217
+ except Exception:
218
+ continue
219
+ scored.append((vs._cosine_similarity(qv, vec), m, tags))
220
+ scored.sort(key=lambda x: x[0], reverse=True)
221
+ out = []
222
+ for score, m, tags in scored[:top_k]:
223
+ out.append({
224
+ "id": getattr(m, "id", ""),
225
+ "title": getattr(m, "title", ""),
226
+ "content": getattr(m, "content", ""),
227
+ "type": getattr(m, "type", "concept"),
228
+ "confidence": getattr(m, "confidence", 1.0),
229
+ "tags": tags,
230
+ "shared": "shared" in tags,
231
+ "score": round(float(score), 4),
232
+ })
233
+ return out
234
+ except Exception:
235
+ return None # 任何异常 → 回退老路径,绝不因根治优化而丢召回能力
236
+
237
+ def _legacy_recall(self, query, agent_id, top_k, include_shared):
238
+ """回退路径:全局召回再按 tag 过滤(HMR 结构不可访问时用)。有完整性局限,见 recall。"""
239
+ if agent_id:
240
+ fetch_k = min(max(top_k * 10, 50), 200)
241
+ else:
242
+ fetch_k = top_k
243
+ with self._lock:
244
+ result = self._hmr.recall(query=query, top_k=fetch_k)
245
+ objs = getattr(result, "memory_objects", []) or []
246
+ out = []
247
+ for m in objs:
248
+ tags = getattr(m, "tags", []) or []
249
+ if agent_id:
250
+ own = f"agent:{agent_id}" in tags
251
+ shared = include_shared and ("shared" in tags)
252
+ if not (own or shared):
253
+ continue # 既不是本 Agent 的,也不是共享的 → 跳过(隔离保证)
254
+ out.append({
255
+ "id": getattr(m, "id", ""),
256
+ "title": getattr(m, "title", ""),
257
+ "content": getattr(m, "content", ""),
258
+ "type": getattr(m, "type", "concept"),
259
+ "confidence": getattr(m, "confidence", 1.0),
260
+ "tags": tags,
261
+ "shared": "shared" in tags,
262
+ })
263
+ if len(out) >= top_k:
264
+ break
265
+ return out
266
+
267
+ # ── 工作区 ────────────────────────────────────────────
268
+ # 【深审修复·补齐锁覆盖】适配层自述"串行化**所有** HMR 访问"(底层向量库/记忆图非线程安全),
269
+ # 但此前只有 ingest/recall 加锁;下列同样触碰 self._hmr 的方法漏了锁——与并发 ingest/recall
270
+ # (并行子 Agent / 并发请求)同时跑会读到 mutation 中的向量索引/字典(未定义行为/迭代崩溃)。
271
+ # 统一补 self._lock(RLock 叶子锁,锁内不回调 pm/bus,无死锁)。
272
+ def get_workspace(self, agent_id: str):
273
+ with self._lock:
274
+ return self._hmr.get_workspace(agent_id)
275
+
276
+ def save_workspace(self, agent_id: str):
277
+ with self._lock:
278
+ return self._hmr.save_workspace(agent_id)
279
+
280
+ # ── 运行时状态 ────────────────────────────────────────
281
+ def save_runtime_state(self, **kwargs):
282
+ with self._lock:
283
+ return self._hmr.save_runtime_state(**kwargs)
284
+
285
+ def restore_runtime_state(self):
286
+ with self._lock:
287
+ return self._hmr.restore_runtime_state()
288
+
289
+ # ── 系统状态 ──────────────────────────────────────────
290
+ def get_system_status(self):
291
+ try:
292
+ with self._lock:
293
+ return self._hmr.get_system_status()
294
+ except Exception:
295
+ return {"backend": "HMR", "version": self.version}
296
+
297
+ def recent(self, limit: int = 20) -> list:
298
+ """最近的记忆条目(按创建时间倒序),供前端右栏"记忆信息条"展示。
299
+ 只回展示所需的轻字段,不回完整 embedding(那是几百维浮点,白白撑大负载)。"""
300
+ out = []
301
+ try:
302
+ with self._lock:
303
+ mfs = getattr(self._hmr, "memory_fs", None)
304
+ mems = mfs.list_memories() if (mfs is not None and hasattr(mfs, "list_memories")) else []
305
+ except Exception:
306
+ mems = []
307
+
308
+ def _ts(m):
309
+ v = getattr(m, "created_at", None)
310
+ try:
311
+ return v.timestamp() if hasattr(v, "timestamp") else float(v or 0)
312
+ except Exception:
313
+ return 0.0
314
+
315
+ try:
316
+ mems = sorted(mems or [], key=_ts, reverse=True)
317
+ except Exception:
318
+ mems = list(mems or [])
319
+ for m in mems[:max(1, int(limit))]:
320
+ tags = list(getattr(m, "tags", None) or [])
321
+ agent = ""
322
+ for t in tags:
323
+ if isinstance(t, str) and t.startswith("agent:"):
324
+ agent = t.split(":", 1)[1]
325
+ break
326
+ created = getattr(m, "created_at", None)
327
+ title = (getattr(m, "title", "") or "").strip()
328
+ # 历史记忆多是 "Concept 03:11" 这类时间戳标题(修复前存的)→ 展示时现推一个可读的,
329
+ # 让老数据也能看懂;推不出来才退回原标题。
330
+ if not title or self._AUTO_TITLE.match(title):
331
+ title = self._derive_title(
332
+ getattr(m, "content", "") or getattr(m, "semantic_summary", "")) or title
333
+ out.append({
334
+ "id": getattr(m, "id", ""),
335
+ "type": getattr(m, "type", ""),
336
+ "title": title[:120],
337
+ "summary": (getattr(m, "semantic_summary", "") or getattr(m, "content", "") or "")[:180],
338
+ "agent": agent,
339
+ "shared": "shared" in tags,
340
+ "created_at": created.isoformat() if hasattr(created, "isoformat") else str(created or ""),
341
+ })
342
+ return out
343
+
344
+ def stats(self) -> dict:
345
+ """记忆统计:供前端记忆中心(vecRecall)展示真实数字。"""
346
+ total = 0
347
+ relations = 0
348
+ shared_count = 0
349
+ by_agent = {}
350
+ embedding = "TF-IDF n-gram"
351
+ try:
352
+ # 序列化对 HMR 内部结构(向量库/记忆图)的读,与 ingest/recall 的写互斥,
353
+ # 避免并发迭代 store.memories("dict changed size")/读 mutation 中的 faiss 索引。
354
+ with self._lock:
355
+ # 权威来源:memory_fs 里的真实记忆对象。
356
+ # 历史 bug:此前从 vector_store 探测 count/size/memories 取 total——但真实记忆存在
357
+ # memory_fs,vector_store 只存嵌入且无这些属性 → total 恒为 0,而 relations 却有值(109),
358
+ # 自相矛盾。改为直接数 memory_fs.list_memories()(与 get_statistics 同源)。
359
+ mems = []
360
+ mfs = getattr(self._hmr, "memory_fs", None)
361
+ if mfs is not None and hasattr(mfs, "list_memories"):
362
+ try:
363
+ mems = mfs.list_memories() or []
364
+ except Exception:
365
+ mems = []
366
+ total = len(mems)
367
+ if not total: # 兜底:memory_fs 不可用 → 数 vector_store.vectors(dict)
368
+ vs0 = getattr(self._hmr, "vector_store", None)
369
+ vecs = getattr(vs0, "vectors", None) if vs0 is not None else None
370
+ if isinstance(vecs, dict):
371
+ total = len(vecs)
372
+ # shared / 按 Agent 分布:agent_id 编码在 tags 里的 "agent:<id>"
373
+ for m in mems:
374
+ tags = getattr(m, "tags", None)
375
+ if tags is None and isinstance(m, dict):
376
+ tags = m.get("tags")
377
+ tags = tags or []
378
+ if "shared" in tags:
379
+ shared_count += 1
380
+ for t in tags:
381
+ if isinstance(t, str) and t.startswith("agent:"):
382
+ aid = t.split(":", 1)[1] or "unknown"
383
+ by_agent[aid] = by_agent.get(aid, 0) + 1
384
+ graph = getattr(self._hmr, "memory_graph", None)
385
+ if graph is not None:
386
+ edges = getattr(graph, "edges", None)
387
+ if edges is not None:
388
+ relations = len(edges) if hasattr(edges, "__len__") else 0
389
+ # embedding 模型探测
390
+ emb = getattr(self._hmr, "embedding", None) or getattr(self._hmr, "embedder", None)
391
+ if emb is not None:
392
+ name = type(emb).__name__
393
+ if "openai" in name.lower() or "OpenAI" in name:
394
+ embedding = "OpenAI Embedding"
395
+ elif "sentence" in name.lower() or "ST" in name:
396
+ embedding = "Sentence-Transformers"
397
+ except Exception:
398
+ pass
399
+ return {
400
+ "backend": getattr(self, "backend", "HMR"),
401
+ "version": self.version,
402
+ "total_memories": int(total),
403
+ "relations": int(relations),
404
+ "shared": int(shared_count),
405
+ "private": int(max(total - shared_count, 0)),
406
+ "by_agent": by_agent,
407
+ "embedding": embedding,
408
+ }
409
+
410
+
411
+ class MockHMR:
412
+ """HMR 不可用时的最小替代,保证系统可启动。"""
413
+
414
+ backend = "Mock"
415
+ version = "mock"
416
+
417
+ def get_workspace(self, agent_id: str):
418
+ return type("Workspace", (), {
419
+ "agent_id": agent_id, "active_goal": "", "status": "idle",
420
+ "push_task": lambda self, t: None,
421
+ })()
422
+
423
+ def save_workspace(self, agent_id: str):
424
+ pass
425
+
426
+ def ingest(self, content, type="concept", agent_id=None, shared=False, **kwargs):
427
+ # 【P0-3】与 HMRAdapter 同一道闸门:Mock 后端也过策略。否则在无 HMR 的环境里
428
+ # 策略形同虚设 —— 而测试与降级场景恰好都落在这一支,那是最需要行为一致的地方。
429
+ if shared:
430
+ guard_shared_write(self, agent_id, content, type, kwargs.get("tags") or [])
431
+ from types import SimpleNamespace
432
+ # 注意:不能用内置 type() 构造对象——参数名 type 已覆盖内置 type。
433
+ return SimpleNamespace(id="mock_mem", type=type)
434
+
435
+ def recall(self, query: str = "", agent_id: str = None, top_k: int = 5, **kwargs):
436
+ from types import SimpleNamespace
437
+ # 返回带 memory_objects 的对象,与真实 HMR 的 RecallResult 结构一致
438
+ # (HMRAdapter.recall 会读 result.memory_objects)。
439
+ return SimpleNamespace(memory_objects=[])
440
+
441
+ def save_runtime_state(self, **kwargs):
442
+ pass
443
+
444
+ def restore_runtime_state(self):
445
+ return None
446
+
447
+ def get_system_status(self):
448
+ return {"backend": "Mock", "version": "mock"}
449
+
450
+ def stats(self) -> dict:
451
+ return {"backend": "Mock", "version": "mock",
452
+ "total_memories": 0, "relations": 0}
453
+
454
+
455
+ def _vecrecall_dir(storage_path: str) -> str:
456
+ """与 HMR 的存储目录强制分开。
457
+
458
+ 两套后端落盘格式完全不同(HMR 是 memories/*.json + vectors.json,vecRecall 是
459
+ sqlite)。放同一个目录会让新后端初始化时看到"一半旧一半新",切回旧后端时又可能
460
+ 被新文件干扰 —— 隔离目录是切换可逆的前提。
461
+ """
462
+ p = Path(str(storage_path))
463
+ if p.name.lower() == "hmr":
464
+ return str(p.parent / "vecrecall")
465
+ return str(p.parent / ((p.name or "memory") + "_vecrecall"))
466
+
467
+
468
+ def _env_first(*names: str, default: str = "") -> str:
469
+ """按顺序取第一个非空环境变量。
470
+
471
+ Foxclaw 的配置从 hestiaOS 继承而来,老 .env 里是 `HESTIA_*` 前缀。两个名字都认,
472
+ `FOXCLAW_*` 优先 —— 这样旧配置直接搬过来就能用,不必逐条改名。
473
+ """
474
+ for n in names:
475
+ v = (os.environ.get(n) or "").strip()
476
+ if v:
477
+ return v
478
+ return default
479
+
480
+
481
+ def _memory_chain_enabled() -> bool:
482
+ """哈希链开关:`1/true/yes/on` 为开。只对 vecRecall 生效。"""
483
+ raw = _env_first("FOXCLAW_MEMORY_CHAIN", "HESTIA_MEMORY_CHAIN")
484
+ return raw.lower() in ("1", "true", "yes", "on")
485
+
486
+
487
+ def create_memory_backend(storage_path: str = "./hmr_data", llm_api_key: str = None):
488
+ """工厂:默认 HMR;`FOXCLAW_MEMORY_BACKEND=vecrecall` 时才切 vecRecall。
489
+
490
+ 顺序 vecRecall → HMR → MockHMR,任一层失败退到下一层,且**明确打印**实际用的是
491
+ 哪个内核 —— 换记忆后端这种事,日志里必须看得见,不能静默替换。
492
+ """
493
+ choice = _env_first("FOXCLAW_MEMORY_BACKEND", "HESTIA_MEMORY_BACKEND",
494
+ default="hmr").lower()
495
+ if choice in ("vecrecall", "vec_recall"):
496
+ try:
497
+ from .vec_recall_adapter import VecRecallAdapter
498
+ use_chain = _memory_chain_enabled()
499
+ adapter = VecRecallAdapter(storage_path=_vecrecall_dir(storage_path),
500
+ use_chain=use_chain)
501
+ print(f"[Foxclaw] ✓ vecRecall v{adapter.version} 已接入 "
502
+ f"(backend={adapter.backend}, chain={'on' if use_chain else 'off'}, "
503
+ f"path={adapter.storage_path})")
504
+ return adapter
505
+ except Exception as e:
506
+ print(f"[Foxclaw] ⚠ vecRecall 不可用 ({e.__class__.__name__}: {e}),回退 HMR")
507
+ elif choice != "hmr":
508
+ print(f"[Foxclaw] ⚠ FOXCLAW_MEMORY_BACKEND={choice!r} 无法识别,按 HMR 处理")
509
+
510
+ try:
511
+ adapter = HMRAdapter(storage_path=storage_path, llm_api_key=llm_api_key)
512
+ print(f"[Foxclaw] ✓ HMR v{adapter.version} 已接入 (backend={adapter.backend})")
513
+ return adapter
514
+ except Exception as e:
515
+ print(f"[Foxclaw] ⚠ HMR 不可用 ({e.__class__.__name__}: {e}),降级 MockHMR")
516
+ return MockHMR()
@@ -0,0 +1,89 @@
1
+ """进程内轻量可观测(ops 监控):请求/LLM 的计数、错误率、延迟分位(p50/p95)。
2
+ 无外部依赖、线程安全(仿 token_meter)。生产用 /api/metrics(JSON) 或 /metrics(Prometheus 文本)拉取。"""
3
+ from __future__ import annotations
4
+ import time
5
+ import threading
6
+ from collections import defaultdict, deque
7
+
8
+ _MAX_SAMPLES = 500 # 每个 key 保留最近 N 次延迟(有界,防内存涨)
9
+
10
+
11
+ class _Stat:
12
+ __slots__ = ("count", "errors", "samples")
13
+
14
+ def __init__(self):
15
+ self.count = 0
16
+ self.errors = 0
17
+ self.samples = deque(maxlen=_MAX_SAMPLES)
18
+
19
+
20
+ def _pct(sorted_vals, p: float) -> float:
21
+ if not sorted_vals:
22
+ return 0.0
23
+ k = int(round((p / 100.0) * (len(sorted_vals) - 1)))
24
+ return float(sorted_vals[max(0, min(k, len(sorted_vals) - 1))])
25
+
26
+
27
+ class MetricsCollector:
28
+ """请求 + LLM 调用的计数/错误/延迟。record_* 高频调用(锁内 O(1))。"""
29
+
30
+ def __init__(self):
31
+ self._lock = threading.RLock()
32
+ self._ep: dict = defaultdict(_Stat) # endpoint(路由模板) -> _Stat
33
+ self._llm: dict = defaultdict(_Stat) # provider -> _Stat
34
+ self._start = time.time()
35
+
36
+ def record_request(self, endpoint: str, status: int, ms: float):
37
+ with self._lock:
38
+ s = self._ep[endpoint]
39
+ s.count += 1
40
+ if status >= 500 or status == 0:
41
+ s.errors += 1
42
+ s.samples.append(ms)
43
+
44
+ def record_llm(self, provider: str, ok: bool, ms: float):
45
+ with self._lock:
46
+ s = self._llm[provider or "?"]
47
+ s.count += 1
48
+ if not ok:
49
+ s.errors += 1
50
+ s.samples.append(ms)
51
+
52
+ @staticmethod
53
+ def _stat_dict(s: _Stat) -> dict:
54
+ vals = sorted(s.samples)
55
+ return {
56
+ "count": s.count, "errors": s.errors,
57
+ "error_rate": round(s.errors / s.count, 4) if s.count else 0.0,
58
+ "p50_ms": round(_pct(vals, 50), 1),
59
+ "p95_ms": round(_pct(vals, 95), 1),
60
+ "avg_ms": round(sum(vals) / len(vals), 1) if vals else 0.0,
61
+ }
62
+
63
+ def snapshot(self) -> dict:
64
+ with self._lock:
65
+ return {
66
+ "uptime_s": round(time.time() - self._start, 1),
67
+ "endpoints": {k: self._stat_dict(v) for k, v in self._ep.items()},
68
+ "llm": {k: self._stat_dict(v) for k, v in self._llm.items()},
69
+ }
70
+
71
+ def prometheus_text(self) -> str:
72
+ snap = self.snapshot()
73
+ out = ["# Foxclaw metrics", f"foxclaw_uptime_seconds {snap['uptime_s']}"]
74
+ for ep, s in snap["endpoints"].items():
75
+ lbl = 'endpoint="%s"' % ep.replace('"', "'")
76
+ out.append(f'foxclaw_requests_total{{{lbl}}} {s["count"]}')
77
+ out.append(f'foxclaw_request_errors_total{{{lbl}}} {s["errors"]}')
78
+ out.append(f'foxclaw_request_latency_p50_ms{{{lbl}}} {s["p50_ms"]}')
79
+ out.append(f'foxclaw_request_latency_p95_ms{{{lbl}}} {s["p95_ms"]}')
80
+ for p, s in snap["llm"].items():
81
+ lbl = 'provider="%s"' % p.replace('"', "'")
82
+ out.append(f'foxclaw_llm_calls_total{{{lbl}}} {s["count"]}')
83
+ out.append(f'foxclaw_llm_errors_total{{{lbl}}} {s["errors"]}')
84
+ out.append(f'foxclaw_llm_latency_p95_ms{{{lbl}}} {s["p95_ms"]}')
85
+ return "\n".join(out) + "\n"
86
+
87
+
88
+ # 进程级单例:中间件 + router 都往这里写,端点从这里读。
89
+ METRICS = MetricsCollector()