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,503 @@
1
+ """
2
+ VecRecall 适配层 — 可选记忆后端,对外接口与 `HMRAdapter` 完全一致。
3
+
4
+ 为什么要这一层:业务代码只认 `HMRAdapter` 的 9 个方法(ingest / recall /
5
+ recall_by_tag / delete_memory / get_workspace / save_workspace /
6
+ save_runtime_state / restore_runtime_state / recent / stats)。
7
+ `VecRecall` 的 API 形状与之不同,直接替换会让调用方炸在一个与被调用能力无关的地方。
8
+
9
+ 几处**按源码**而非按文档写的映射(v2.1 实测):
10
+ Foxclaw VecRecall 2.1
11
+ ───────────────────────── ───────────────────────────────────────────
12
+ ingest(...) add(content, memory_type=, visibility=, owner=, ...)
13
+ recall(query, agent_id=) build_context(query, viewer=agent_id) 的 L2+L3
14
+ recall_by_tag("preference") query_by_topic_key("preference")
15
+ delete_memory(id) delete(id)
16
+ recent(limit) list_memories(sort="recent", full=True)
17
+ get_workspace(agent_id) 无对应概念 → 返回空壳(字段无人读,见 agents/base.py)
18
+
19
+ 注意两点与直觉相反的地方:
20
+ 1. `VecRecall.recall(memory_id)` **不是检索**,是"复习并增强记忆强度"。检索入口是
21
+ `build_context`(带 viewer 隔离) / `search`(不做任何隔离)。用错名字会得到
22
+ TypeError 或越权召回。
23
+ 2. 隔离走 `owner` + `visibility`,在**召回前**生效 —— 这正好修掉 HMR 路径
24
+ "全局召回 K 条再按 tag 筛"的完整性缺陷(别人的记忆挤满 top-k 时自己的召不回)。
25
+ """
26
+ import os
27
+ import re
28
+ import sys
29
+ import threading
30
+ from pathlib import Path
31
+ from types import SimpleNamespace
32
+
33
+ from .shared_write import guard_shared_write
34
+
35
+ # Foxclaw 的 type 白名单 → vecRecall 的 memory_type(episodic/semantic/procedural)。
36
+ # 分错不影响读写,但影响 vecRecall 的遗忘曲线与 procedural 精确匹配。
37
+ _FOXCLAW_TO_VR = {
38
+ "concept": "semantic",
39
+ "project": "semantic",
40
+ "task": "semantic",
41
+ "agent_memory": "semantic",
42
+ "reflection": "semantic",
43
+ "workflow": "procedural",
44
+ "decision": "episodic",
45
+ "runtime": "episodic",
46
+ "execution": "episodic",
47
+ }
48
+ _VR_TO_FOXCLAW = {
49
+ "semantic": "concept",
50
+ "procedural": "workflow",
51
+ "episodic": "concept",
52
+ }
53
+ _VALID_TYPES = set(_FOXCLAW_TO_VR)
54
+
55
+ # 偏好类记忆走 topic_key(精确查询),因为"总是注入的偏好"不该做语义模糊匹配。
56
+ _PREFERENCE_TAG = "preference"
57
+
58
+
59
+ def _ensure_vendor_importable():
60
+ """把 vendor/ 与 vendor/hkc 加入 sys.path,使内嵌 HKC 的共享 embedding 可用。"""
61
+ vendor = Path(__file__).resolve().parent.parent.parent / "vendor"
62
+ for p in (vendor, vendor / "hkc"):
63
+ if p.exists() and str(p) not in sys.path:
64
+ sys.path.insert(0, str(p))
65
+
66
+
67
+ class _HKCSharedEmbedding:
68
+ """与 HMR 共用同一个 SentenceTransformer 实例。
69
+
70
+ 两套后端跑同一个模型才能让影子对比有意义(维度、分数可比);若各自
71
+ `SentenceTransformer(...)` 一次,bge-m3 级别的模型会多占约 1GB 常驻内存。
72
+ HKC 的 `get_st_model` 已经是进程级缓存,这里只做一层薄包装。
73
+ """
74
+
75
+ backend = "hkc-shared"
76
+
77
+ def __init__(self, model_name: str):
78
+ self.model_name = model_name
79
+ self._model = None
80
+ self._dim = None
81
+
82
+ def _load(self):
83
+ if self._model is None:
84
+ _ensure_vendor_importable()
85
+ from hkc_search.embedding_backends import get_st_model
86
+ model = get_st_model(self.model_name)
87
+ if model is None:
88
+ # 不回退到直连 SentenceTransformer:那会绕过 HKC 的进程级共享缓存、
89
+ # 多加载一份模型(bge-m3 常驻约 1GB)。失败就明确报错让工厂回退 HMR。
90
+ raise RuntimeError(
91
+ f"embedding 模型不可用: {self.model_name}(HKC 共享缓存加载失败)")
92
+ self._model = model
93
+ return self._model
94
+
95
+ def embed(self, text: str):
96
+ vec = self._load().encode([text or ""], normalize_embeddings=True)[0]
97
+ return [float(x) for x in vec]
98
+
99
+ def embed_batch(self, texts):
100
+ arr = self._load().encode(list(texts), normalize_embeddings=True)
101
+ return [[float(x) for x in row] for row in arr]
102
+
103
+ @property
104
+ def dim(self):
105
+ if self._dim is None:
106
+ try:
107
+ model = self._load()
108
+ # ST 6.x 把 get_sentence_embedding_dimension 改名了,两个都试
109
+ getter = (getattr(model, "get_embedding_dimension", None)
110
+ or getattr(model, "get_sentence_embedding_dimension", None))
111
+ self._dim = (int(getter() or 0) or None) if getter else None
112
+ except Exception:
113
+ self._dim = None
114
+ return self._dim
115
+
116
+
117
+ class VecRecallAdapter:
118
+ """把 vecRecall v2.1 包装成 Foxclaw 期望的记忆接口。"""
119
+
120
+ backend = "vecRecall"
121
+
122
+ def __init__(self, storage_path: str = "./vecrecall_data", wing: str = "foxclaw",
123
+ use_chain: bool = False, embedding_model: str = None,
124
+ dedup: bool = True, embedding_backend=None, vector_backend=None):
125
+ """`embedding_backend` / `vector_backend` 用于注入(测试传轻量实现)。
126
+
127
+ 默认为 None 时走生产配置:HKC 共享的 SentenceTransformer + vecRecall 默认
128
+ 的 sqlite 向量库。测试若不注入,会去加载 bge-m3 级别模型,命令长时间无输出
129
+ —— 看起来像死锁,实际是在下载/加载权重。
130
+ """
131
+ from vecrecall import VecRecall
132
+
133
+ self.wing = wing
134
+ self.storage_path = str(storage_path)
135
+ self.version = self._resolve_version()
136
+ # vecRecall 本体没有任何锁(只有 REST server 层有 RLock),而它用的是
137
+ # check_same_thread=False 的单条 sqlite 连接 + 内存 dict 索引。并行子 Agent
138
+ # 同时 add/search 属于未定义行为,和 HMR 是同一类坑 —— 这里沿用同一套串行化。
139
+ self._lock = threading.RLock()
140
+
141
+ self._emb = embedding_backend or _HKCSharedEmbedding(
142
+ self._resolve_model(embedding_model))
143
+ self._assert_dim_matches()
144
+
145
+ extra = {"vector_backend": vector_backend} if vector_backend is not None else {}
146
+ self._vr = VecRecall(
147
+ base_dir=self.storage_path,
148
+ wing=wing,
149
+ embedding_backend=self._emb,
150
+ use_blockchain=use_chain,
151
+ dedup=dedup,
152
+ **extra,
153
+ )
154
+
155
+ # ── 构造辅助 ──────────────────────────────────────────
156
+ @staticmethod
157
+ def _resolve_version() -> str:
158
+ try:
159
+ import vecrecall as _pkg
160
+ return str(getattr(_pkg, "__version__", "unknown"))
161
+ except Exception:
162
+ return "unknown"
163
+
164
+ @staticmethod
165
+ def _resolve_model(embedding_model: str = None) -> str:
166
+ """与 HMRAdapter 同一条解析链,保证两套后端用同一个模型(分数与维度可比)。"""
167
+ return (embedding_model
168
+ or os.environ.get("EMBEDDING_MODEL")
169
+ or os.environ.get("HKC_EMBEDDING_MODEL")
170
+ or os.environ.get("FOXCLAW_EMBEDDING_MODEL")
171
+ or "all-MiniLM-L6-v2")
172
+
173
+ def _assert_dim_matches(self) -> None:
174
+ """已有向量维度与当前嵌入维度不一致时明确失败。
175
+
176
+ vecRecall 的 SqliteVectorBackend 与 HMR 一样用 zip() 求内积:两侧维度不同时
177
+ 短的一侧被静默截断,分数照常返回、不抛错。症状是"换了 embedding 模型之后
178
+ 召回突然变成噪声",比直接崩更难定位。
179
+ """
180
+ db = Path(self.storage_path) / "vectors.db"
181
+ if not db.exists():
182
+ return
183
+ import sqlite3
184
+ try:
185
+ conn = sqlite3.connect(str(db))
186
+ rows = conn.execute("SELECT DISTINCT dim FROM vectors LIMIT 2").fetchall()
187
+ conn.close()
188
+ except Exception:
189
+ return
190
+ dims = {int(r[0]) for r in rows}
191
+ want = getattr(self._emb, "dim", None) # 轻量后端无 dim 属性时跳过
192
+ if dims and want and dims != {int(want)}:
193
+ raise RuntimeError(
194
+ f"已有向量维度 {sorted(dims)} 与当前嵌入维度 {want} 不一致。"
195
+ f"换 embedding 模型必须重建索引:删除 {self.storage_path} 后重启"
196
+ )
197
+
198
+ # ── 摄入 ──────────────────────────────────────────────
199
+ _AUTO_TITLE = re.compile(r"^[A-Za-z_]+\s+\d{1,2}:\d{2}$")
200
+
201
+ @staticmethod
202
+ def _derive_title(content: str) -> str:
203
+ s = str(content or "").strip()
204
+ if not s:
205
+ return ""
206
+ for pfx in ("对话:", "对话:", "用户偏好:", "用户偏好:", "任务:", "任务:"):
207
+ if s.startswith(pfx):
208
+ s = s[len(pfx):].strip()
209
+ break
210
+ for sep in ("→", "->"):
211
+ if sep in s:
212
+ s = s.split(sep, 1)[0].strip()
213
+ break
214
+ s = re.split(r"[。!?!?\n\r]", s, 1)[0].strip()
215
+ return " ".join(s.split())[:80]
216
+
217
+ def ingest(self, content: str, type: str = "concept", agent_id: str = None,
218
+ title: str = None, tags: list = None, shared: bool = False, **kwargs):
219
+ """统一摄入。签名与 `HMRAdapter.ingest` 一致。
220
+
221
+ 隔离承载:`agent_id` → owner,`shared=True` → visibility="shared"。
222
+ 不再依赖 tag 前缀做隔离,但 tag 仍然写进 metadata(前端展示与偏好查询要用)。
223
+ """
224
+ meta_tags = list(tags or [])
225
+ if agent_id:
226
+ meta_tags.append(f"agent:{agent_id}")
227
+ if shared:
228
+ meta_tags.append("shared")
229
+
230
+ mtype = type if type in _VALID_TYPES else "concept"
231
+ if not title:
232
+ title = self._derive_title(content) or ""
233
+
234
+ # 【P0-3】共享层写入闸门:必须在落库之前判定,语义与 HMR 路径完全一致,
235
+ # 否则同一个策略在两个后端上表现不同。
236
+ if shared:
237
+ guard_shared_write(self, agent_id, content, mtype, meta_tags)
238
+
239
+ metadata = {"tags": meta_tags, "foxclaw_type": mtype}
240
+ if "confidence" in kwargs:
241
+ metadata["confidence"] = kwargs["confidence"]
242
+
243
+ # execution 是审计证据:被遗忘曲线衰减或被去重合并,就无法再证明"当时确实
244
+ # 这么做了"。vecRecall 的 immutable 同时免于衰减与去重,正好对上这个语义。
245
+ retention = "immutable" if mtype == "execution" else "decay"
246
+ topic_key = kwargs.get("topic_key") or (
247
+ _PREFERENCE_TAG if _PREFERENCE_TAG in meta_tags else "")
248
+
249
+ with self._lock:
250
+ return self._vr.add(
251
+ content=content,
252
+ wing=self.wing,
253
+ importance=kwargs.get("importance"),
254
+ ui_summary=(title or "")[:200],
255
+ metadata=metadata,
256
+ memory_type=_FOXCLAW_TO_VR.get(mtype, "semantic"),
257
+ visibility="shared" if shared else "private",
258
+ owner=(agent_id or "") if not shared else "",
259
+ retention_policy=retention,
260
+ topic_key=topic_key,
261
+ )
262
+
263
+ # ── 召回 ──────────────────────────────────────────────
264
+ def _visible(self, mem, agent_id: str, include_shared: bool) -> bool:
265
+ """可见性判定,与 vecRecall `_visible_to` 规则保持同源。
266
+
267
+ - shared 记忆:仅当 include_shared 时可见
268
+ - private 记忆:owner 为空(老数据)视为公开;否则必须等于当前 agent
269
+ - agent_id 为空:不过滤(与 HMR 路径 `if agent_id:` 的行为一致)
270
+ """
271
+ vis = getattr(mem, "visibility", "private") or "private"
272
+ owner = getattr(mem, "owner", "") or ""
273
+ if not agent_id:
274
+ return True
275
+ if vis == "shared":
276
+ return include_shared
277
+ return owner == agent_id or not owner
278
+
279
+ def recall(self, query: str = "", agent_id: str = None, top_k: int = 5,
280
+ include_shared: bool = True, **kwargs):
281
+ """统一召回,返回 list[dict]。
282
+
283
+ 走 `build_context(viewer=...)` 而不是 `search()`:前者在 L1/L2/L3 全层按
284
+ visibility+owner 过滤,是**召回前**生效的隔离;`search()` 源码里明写
285
+ "不经过任何结构过滤",拿它做按 Agent 召回会越权。
286
+
287
+ 只取 L2+L3:L1 是"全库 importance top-15 + immutable 全额并入",与当前
288
+ query 无关,混进来会挤占 top_k。
289
+ """
290
+ fetch_k = min(max(top_k * 5, 20), 100) if agent_id else top_k
291
+ results = []
292
+ try:
293
+ with self._lock:
294
+ bundle = self._vr.build_context(
295
+ current_query=query or "",
296
+ load_l2=True,
297
+ load_l3=bool(query and query.strip()),
298
+ viewer=agent_id,
299
+ )
300
+ results = list(getattr(bundle, "l2_topic_context", []) or []) + \
301
+ list(getattr(bundle, "l3_deep_results", []) or [])
302
+ if not results and (query or "").strip():
303
+ # L2 有 0.55 阈值:命中偏弱时整层为空。HMR 的 recall 是无条件
304
+ # 返回 top_k,为不改变上层"总能拿到几条"的预期,退化到全量语义
305
+ # 检索,再在下面用 _visible 补上隔离(search 本身不隔离)。
306
+ results = list(self._vr.search(query, n=fetch_k) or [])
307
+ except Exception:
308
+ return []
309
+
310
+ out, seen = [], set()
311
+ for r in results:
312
+ mem = getattr(r, "memory", None)
313
+ if mem is None:
314
+ continue
315
+ mid = getattr(mem, "id", "")
316
+ if not mid or mid in seen:
317
+ continue
318
+ if not self._visible(mem, agent_id, include_shared):
319
+ continue
320
+ seen.add(mid)
321
+ row = self._memory_to_row(mem)
322
+ row["score"] = round(float(getattr(r, "score", 0.0) or 0.0), 4)
323
+ out.append(row)
324
+
325
+ out.sort(key=lambda x: x.get("score", 0.0), reverse=True)
326
+ return out[:top_k]
327
+
328
+ def _memory_to_row(self, mem) -> dict:
329
+ """Memory → Foxclaw 的召回结果结构。"""
330
+ meta = getattr(mem, "metadata", None) or {}
331
+ tags = [t for t in (meta.get("tags") or []) if isinstance(t, str)]
332
+ owner = getattr(mem, "owner", "") or ""
333
+ if owner and f"agent:{owner}" not in tags:
334
+ tags.append(f"agent:{owner}")
335
+ shared = (getattr(mem, "visibility", "") == "shared")
336
+ if shared and "shared" not in tags:
337
+ tags.append("shared")
338
+ ftype = (meta.get("foxclaw_type")
339
+ or _VR_TO_FOXCLAW.get(getattr(mem, "memory_type", ""), "concept"))
340
+ return {
341
+ "id": getattr(mem, "id", ""),
342
+ "title": (getattr(mem, "ui_summary", "") or "")[:120],
343
+ "content": getattr(mem, "content", "") or "",
344
+ "type": ftype,
345
+ "confidence": meta.get("confidence", 1.0),
346
+ "tags": tags,
347
+ "shared": shared,
348
+ }
349
+
350
+ def recall_by_tag(self, agent_id=None, tag="preference", limit=8, include_shared=True):
351
+ """按 tag 精确拉取(不做语义排序),用于"总是注入"的偏好类记忆。
352
+
353
+ vecRecall 的硬边界禁止用 wing/topic 结构过滤**检索**,但偏好本来就不该做
354
+ 语义模糊匹配 —— 它走 `query_by_topic_key`(精确、时间序),语义反而更准。
355
+ 非偏好 tag 退化为全量列表筛选,避免调用方拿到静默的空结果。
356
+ """
357
+ mems = []
358
+ try:
359
+ with self._lock:
360
+ if tag == _PREFERENCE_TAG:
361
+ mems = list(self._vr.query_by_topic_key(tag, wing=self.wing) or [])
362
+ if not mems:
363
+ listed = self._vr.list_memories(wing=self.wing, limit=500,
364
+ full=True, sort="recent")
365
+ for d in (listed.get("memories") or []):
366
+ t = (d.get("metadata") or {}).get("tags") or []
367
+ if tag in t:
368
+ mems.append(SimpleNamespace(
369
+ id=d.get("id", ""), content=d.get("content", ""),
370
+ metadata=d.get("metadata") or {},
371
+ visibility=d.get("visibility", "private"),
372
+ owner="", timestamp=d.get("timestamp", ""),
373
+ ui_summary=d.get("ui_summary", ""),
374
+ memory_type=d.get("memory_type", ""),
375
+ ))
376
+ except Exception:
377
+ return []
378
+
379
+ rows = []
380
+ for m in mems:
381
+ if not self._visible(m, agent_id, include_shared):
382
+ continue
383
+ row = self._memory_to_row(m)
384
+ row["_ts"] = getattr(m, "timestamp", "") or ""
385
+ rows.append(row)
386
+
387
+ rows.sort(key=lambda x: str(x.get("_ts")), reverse=True) # 新近优先
388
+ # 同一偏好反复表达会存成多条,去重保留最新,否则会挤满 limit
389
+ deduped, seen_c = [], set()
390
+ for x in rows:
391
+ if x["content"] in seen_c:
392
+ continue
393
+ seen_c.add(x["content"])
394
+ deduped.append(x)
395
+ return deduped[:limit]
396
+
397
+ def delete_memory(self, memory_id):
398
+ with self._lock:
399
+ try:
400
+ return bool(self._vr.delete(memory_id))
401
+ except Exception:
402
+ return False
403
+
404
+ # ── 展示 ──────────────────────────────────────────────
405
+ def recent(self, limit: int = 20) -> list:
406
+ """最近的记忆条目(新→旧),供前端右栏"记忆信息条"。"""
407
+ rows = []
408
+ try:
409
+ with self._lock:
410
+ listed = self._vr.list_memories(wing=self.wing, limit=max(1, int(limit)),
411
+ full=True, sort="recent")
412
+ except Exception:
413
+ return []
414
+ for d in (listed.get("memories") or []):
415
+ meta = d.get("metadata") or {}
416
+ tags = [t for t in (meta.get("tags") or []) if isinstance(t, str)]
417
+ agent = ""
418
+ for t in tags:
419
+ if t.startswith("agent:"):
420
+ agent = t.split(":", 1)[1]
421
+ break
422
+ title = (d.get("ui_summary") or "").strip()
423
+ if not title or self._AUTO_TITLE.match(title):
424
+ title = self._derive_title(d.get("content") or "") or title
425
+ rows.append({
426
+ "id": d.get("id", ""),
427
+ "type": meta.get("foxclaw_type") or _VR_TO_FOXCLAW.get(
428
+ d.get("memory_type", ""), ""),
429
+ "title": title[:120],
430
+ "summary": (d.get("ui_summary") or d.get("content_preview")
431
+ or d.get("content") or "")[:180],
432
+ "agent": agent,
433
+ "shared": d.get("visibility") == "shared",
434
+ "created_at": str(d.get("timestamp") or ""),
435
+ })
436
+ return rows
437
+
438
+ # ── HMR 专有接口的占位 ────────────────────────────────
439
+ # vecRecall 没有"会话工作区 / 运行时快照"这两个概念。留占位而不是让调用方
440
+ # AttributeError:上层有统一的"后端接口"假设,缺方法会炸在一个与被调用能力
441
+ # 无关的地方。agents/base.py 取到 workspace 后并不读其字段(active_goal 的
442
+ # 写入已移除),所以空壳没有实际成本。
443
+ def get_workspace(self, agent_id: str):
444
+ return SimpleNamespace(agent_id=agent_id, active_goal="", status="idle",
445
+ push_task=lambda _t: None)
446
+
447
+ def save_workspace(self, agent_id: str):
448
+ return None
449
+
450
+ def save_runtime_state(self, **kwargs):
451
+ return None
452
+
453
+ def restore_runtime_state(self):
454
+ return None
455
+
456
+ # ── 状态 ──────────────────────────────────────────────
457
+ def get_system_status(self):
458
+ return {"backend": self.backend, "version": self.version, "wing": self.wing}
459
+
460
+ def stats(self) -> dict:
461
+ """记忆统计:前端记忆中心展示真实数字。字段与 HMRAdapter.stats 一致。"""
462
+ total = shared_count = relations = 0
463
+ by_agent = {}
464
+ embedding = type(self._emb).__name__
465
+ try:
466
+ with self._lock:
467
+ listed = self._vr.list_memories(wing=self.wing, limit=5000,
468
+ full=True, sort="recent")
469
+ mems = listed.get("memories") or []
470
+ total = int(listed.get("total") or len(mems))
471
+ for d in mems:
472
+ if d.get("visibility") == "shared":
473
+ shared_count += 1
474
+ for t in ((d.get("metadata") or {}).get("tags") or []):
475
+ if isinstance(t, str) and t.startswith("agent:"):
476
+ aid = t.split(":", 1)[1] or "unknown"
477
+ by_agent[aid] = by_agent.get(aid, 0) + 1
478
+ g = ((self._vr.stats() or {}).get("graph") or {})
479
+ for k in ("edges", "total_edges", "relations"):
480
+ if isinstance(g.get(k), int):
481
+ relations = g[k]
482
+ break
483
+ except Exception:
484
+ pass
485
+ return {
486
+ "backend": self.backend,
487
+ "version": self.version,
488
+ "total_memories": int(total),
489
+ "relations": int(relations),
490
+ "shared": int(shared_count),
491
+ "private": int(max(total - shared_count, 0)),
492
+ "by_agent": by_agent,
493
+ "embedding": embedding,
494
+ }
495
+
496
+ def close(self):
497
+ with self._lock:
498
+ closer = getattr(self._vr, "close", None)
499
+ if closer is not None:
500
+ try:
501
+ closer()
502
+ except Exception:
503
+ pass
@@ -0,0 +1,181 @@
1
+ """执行后核验:这一步到底做成了没有,以及凭什么这么说。
2
+
3
+ 病根:工具返回一段文本,模型接着把它写成"已完成"。可这段文本**既不是成功证明,
4
+ 也不是失败证明**——它只是工具说了什么。于是出现了审核报告里记的那类静默假成功:
5
+ 发送没发出去、写入没落盘,回答里照样写"已搞定"。人只有事后翻记录才发现。
6
+
7
+ 这里给每个工具结果一个**三态结论** + 可展开的证据:
8
+ · ``verified`` —— 拿到了可核对的后置条件(如搜索结果里的来源链接、命中的本地文件);
9
+ · ``unverified`` —— 执行没报错,但**没有可观察的后置条件**(写/发送/支付类),
10
+ 此时模型不许断言"已完成",只能描述"我执行了什么";
11
+ · ``failed`` —— 明确失败/认不出来。
12
+
13
+ 刻意不做的事:
14
+ - **不猜**。没有后置条件的工具就是 ``unverified``,不会因为文本里有"成功"二字就转成
15
+ ``verified``——那句"成功"是工具自己写的,不是证据。
16
+ - **不解析自然语言断言**。不去从模型答案里抽取"已完成"再逐个核对(那要靠第二次 LLM
17
+ 调用,既慢又会编);证据只从**工具真实返回**里抽,绑在步骤上,由 UI 展示给人看。
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import os
22
+ import re
23
+ import time
24
+ from dataclasses import dataclass, field
25
+ from urllib.parse import urlparse
26
+
27
+ from . import failure_policy as _policy
28
+
29
+ VERIFIED = "verified"
30
+ UNVERIFIED = "unverified"
31
+ FAILED = "failed"
32
+
33
+ #: 每条工具结果最多挂几条证据(再多就成了噪声,用户不会看)
34
+ MAX_EVIDENCE = 8
35
+
36
+ _URL_RE = re.compile(r"https?://[^\s<>\"'))\]}]+")
37
+ #: 只保留"真实存在"的路径 —— 正则从文本里认出来的路径九成是误报(URL 片段、示例路径),
38
+ #: 只有能在磁盘上 stat 到的才算证据。这不是限制,是"证据"二字的含义。
39
+ _WIN_PATH_RE = re.compile(r"[A-Za-z]:[\\/][^\s\"'<>|*?,。;]+")
40
+ _POSIX_PATH_RE = re.compile(r"(?<![\w:])/(?:[\w.\-]+/)+[\w.\-]+")
41
+
42
+
43
+ @dataclass(frozen=True)
44
+ class Evidence:
45
+ """一条可核对的凭据:一个链接、一个真实存在的文件、或一次工具的原始命中。"""
46
+
47
+ kind: str # url / file
48
+ ref: str # 链接或绝对路径
49
+ label: str # 展示名(域名 / 文件名)
50
+ source: str # 哪个工具产出的
51
+ at: float = field(default_factory=time.time)
52
+
53
+ def to_dict(self) -> dict:
54
+ return {"kind": self.kind, "ref": self.ref, "label": self.label,
55
+ "source": self.source, "at": self.at}
56
+
57
+
58
+ @dataclass
59
+ class Verification:
60
+ """一次工具调用的核验结论。"""
61
+
62
+ status: str
63
+ reason: str = ""
64
+ evidence: list[Evidence] = field(default_factory=list)
65
+
66
+ @property
67
+ def ok(self) -> bool:
68
+ return self.status == VERIFIED
69
+
70
+ def to_dict(self) -> dict:
71
+ return {"status": self.status, "reason": self.reason,
72
+ "evidence": [e.to_dict() for e in self.evidence]}
73
+
74
+
75
+ def _label_for_url(url: str) -> str:
76
+ try:
77
+ host = urlparse(url).netloc
78
+ return host[4:] if host.startswith("www.") else host or url[:40]
79
+ except Exception:
80
+ return url[:40]
81
+
82
+
83
+ def _paths_in(text: str) -> list[str]:
84
+ out: list[str] = []
85
+ for m in _WIN_PATH_RE.finditer(text or ""):
86
+ out.append(m.group(0).rstrip(".,;:,。;:"))
87
+ for m in _POSIX_PATH_RE.finditer(text or ""):
88
+ out.append(m.group(0).rstrip(".,;:,。;:"))
89
+ return out
90
+
91
+
92
+ def extract_evidence(source: str, text: str, limit: int = MAX_EVIDENCE) -> list[Evidence]:
93
+ """从工具返回文本里抽可核对的证据。
94
+
95
+ 只认两类硬凭据:URL(去重、截断到 8 条)与**磁盘上确实存在**的路径。
96
+ 正文摘要不算证据——那是工具的自述,不是可复核的事实。
97
+ """
98
+ seen: set[str] = set()
99
+ out: list[Evidence] = []
100
+ for m in _URL_RE.finditer(text or ""):
101
+ url = m.group(0).rstrip(".,;:!?,。;:!?")
102
+ key = url.split("#", 1)[0]
103
+ if key in seen:
104
+ continue
105
+ seen.add(key)
106
+ out.append(Evidence("url", url, _label_for_url(url), source))
107
+ if len(out) >= limit:
108
+ return out
109
+ for p in _paths_in(text):
110
+ if p in seen:
111
+ continue
112
+ try:
113
+ if not os.path.exists(p):
114
+ continue
115
+ except Exception:
116
+ continue
117
+ seen.add(p)
118
+ out.append(Evidence("file", p, os.path.basename(p), source))
119
+ if len(out) >= limit:
120
+ break
121
+ return out
122
+
123
+
124
+ #: 工具专属的后置条件检查。返回 (是否通过, 不通过的原因)。
125
+ #: 没登记的工具走通用规则(见 verify),不会被当成"已验证"。
126
+ _CHECKERS: dict[str, "callable"] = {
127
+ # 联网搜索:结论必须能指回来源,否则就是"模型自己写的" —— 标未核验而不是验证通过
128
+ "web_search": lambda args, text: (
129
+ (True, "") if _URL_RE.search(text or "")
130
+ else (False, "搜索结果没有可核对的来源链接")),
131
+ # 个人记忆:召回为空是合法结果(确实没记过),非空即有凭据
132
+ "recall_memory": lambda args, text: (
133
+ (True, "") if (text or "").strip().startswith("【召回的相关记忆】")
134
+ else (False, "未返回记忆条目")),
135
+ }
136
+
137
+
138
+ def verify(name: str, args: dict | None, text: str,
139
+ capability: dict | None = None) -> Verification:
140
+ """给一次工具调用的结果下结论(不抛异常:核验本身失败时不拖垮工具轮)。"""
141
+ kind = _policy.classify(text)
142
+ if kind not in (_policy.OK, _policy.UNKNOWN):
143
+ return Verification(FAILED, reason=f"失败类型:{kind}")
144
+ if kind == _policy.UNKNOWN:
145
+ return Verification(FAILED, reason="认不出的返回(不当作成功)")
146
+
147
+ ev = extract_evidence(name, text)
148
+ checker = _CHECKERS.get(name)
149
+ if checker is not None:
150
+ try:
151
+ passed, why = checker(args or {}, text or "")
152
+ except Exception:
153
+ passed, why = False, "后置条件检查自身出错"
154
+ if not passed:
155
+ return Verification(UNVERIFIED, reason=why, evidence=ev)
156
+ return Verification(VERIFIED, evidence=ev)
157
+
158
+ read_only = bool((capability or {}).get("read_only"))
159
+ if read_only:
160
+ # 只读 + 有返回 = 这一步确实发生过(内容是否可信由污点/证据层管)
161
+ return Verification(VERIFIED, evidence=ev)
162
+ # 有副作用但没登记后置条件:不能证明效果已发生 → 未核验(不许声称完成)
163
+ return Verification(UNVERIFIED, reason="该工具未声明可观察的后置条件", evidence=ev)
164
+
165
+
166
+ def note_for_model(v: Verification, read_only: bool = False) -> str:
167
+ """给模型的硬提示。**只对未核验/失败**追加;已验证的成功结果一个字都不改。
168
+
169
+ ``read_only`` 决定未核验时要不要训话:只读工具的"没核对到来源"照样记进证据区
170
+ (UI 显示未核验),但不往结果文本里塞提示 —— 否则每条搜索/记忆命中都会多一段
171
+ 劝诫,既挤占上下文又改变既有行为。**有副作用**的未核验必须训:那才是"没做成
172
+ 却可能被写成做成了"的地方。
173
+ """
174
+ if v.status == VERIFIED:
175
+ return ""
176
+ if v.status == FAILED:
177
+ return f"[核验] 该步骤未成功({v.reason}),不要向用户声称它已完成。"
178
+ if read_only:
179
+ return ""
180
+ return (f"[核验] 该步骤执行了但没有可核对的凭据({v.reason})。"
181
+ f"只描述你执行了什么,不要断言「已完成/已成功」。")