foxclaw 0.1.2 → 0.1.4

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 (368) hide show
  1. package/index.js +1 -8
  2. package/package.json +1 -1
  3. package/python/.env.example +8 -0
  4. package/python/.github/workflows/ci.yml +47 -2
  5. package/python/.github/workflows/release.yml +5 -0
  6. package/python/CHANGELOG.md +100 -0
  7. package/python/CHANGELOG_CN.md +100 -0
  8. package/python/README.md +17 -2
  9. package/python/README_CN.md +38 -20
  10. package/python/bench/bm25_audit.py +1 -14
  11. package/python/bench/bm25_regression.py +0 -1
  12. package/python/bench/bm25_stress.py +0 -4
  13. package/python/bench/cd_audit.py +0 -23
  14. package/python/bench/chaos.py +6 -8
  15. package/python/bench/e2e_real_models.py +0 -1
  16. package/python/bench/llm_retry_test.py +2 -9
  17. package/python/bench/load_test.py +5 -8
  18. package/python/bench/vector_ivf_upgrade.py +1 -7
  19. package/python/docs/CONTENT_GUARD.md +189 -0
  20. package/python/docs/CONTENT_GUARD_CN.md +189 -0
  21. package/python/docs/RELEASE.md +78 -37
  22. package/python/docs/RUN_CN.md +2 -2
  23. package/python/evals/governance/cases.py +0 -37
  24. package/python/evals/governance/run_evals.py +5 -37
  25. package/python/examples/governance_middleware_demo.py +1 -6
  26. package/python/examples/langgraph_governance_demo.py +7 -11
  27. package/python/foxclaw/__init__.py +74 -152
  28. package/python/foxclaw/agents/base.py +82 -136
  29. package/python/foxclaw/agents/impl/_helpers.py +2 -10
  30. package/python/foxclaw/agents/impl/finance.py +4 -21
  31. package/python/foxclaw/agents/impl/office.py +7 -24
  32. package/python/foxclaw/agents/impl/supervisor.py +12 -79
  33. package/python/foxclaw/agents/mixins/_context.py +0 -23
  34. package/python/foxclaw/agents/mixins/augment.py +10 -16
  35. package/python/foxclaw/agents/mixins/llm.py +7 -6
  36. package/python/foxclaw/agents/mixins/memory.py +23 -3
  37. package/python/foxclaw/agents/mixins/preference.py +58 -8
  38. package/python/foxclaw/agents/mixins/prompt.py +59 -12
  39. package/python/foxclaw/agents/mixins/skillbox.py +6 -6
  40. package/python/foxclaw/agents/mixins/tool_exec.py +16 -42
  41. package/python/foxclaw/agents/rules/finance/RULES.md +131 -0
  42. package/python/foxclaw/api/__init__.py +18 -77
  43. package/python/foxclaw/api/_state.py +43 -0
  44. package/python/foxclaw/api/bootstrap.py +114 -0
  45. package/python/foxclaw/api/middleware.py +5 -28
  46. package/python/foxclaw/api/routes/ahs.py +60 -73
  47. package/python/foxclaw/api/routes/attachments.py +5 -6
  48. package/python/foxclaw/api/routes/browser.py +7 -11
  49. package/python/foxclaw/api/routes/domain.py +18 -26
  50. package/python/foxclaw/api/routes/knowledge.py +3 -21
  51. package/python/foxclaw/api/routes/llm_admin.py +21 -33
  52. package/python/foxclaw/api/routes/memory.py +50 -12
  53. package/python/foxclaw/api/routes/orchestration.py +3 -5
  54. package/python/foxclaw/api/routes/recovery.py +4 -3
  55. package/python/foxclaw/api/routes/review.py +2 -11
  56. package/python/foxclaw/api/routes/search.py +3 -7
  57. package/python/foxclaw/api/routes/system.py +33 -49
  58. package/python/foxclaw/api/routes/task.py +31 -49
  59. package/python/foxclaw/api/routes/ws.py +2 -6
  60. package/python/foxclaw/api/spine.py +1 -4
  61. package/python/foxclaw/behavior/__init__.py +20 -0
  62. package/python/foxclaw/behavior/behavior.yaml +62 -0
  63. package/python/foxclaw/behavior/spec.py +287 -0
  64. package/python/foxclaw/browser/action/action_executor.py +1 -5
  65. package/python/foxclaw/browser/action/action_types.py +0 -1
  66. package/python/foxclaw/browser/action/url_policy.py +1 -5
  67. package/python/foxclaw/browser/adapter/web_adapter.py +1 -1
  68. package/python/foxclaw/browser/driver/playwright_driver.py +20 -16
  69. package/python/foxclaw/browser/perception/dom_extractor.py +0 -1
  70. package/python/foxclaw/browser/perception/element_indexer.py +1 -2
  71. package/python/foxclaw/browser/service.py +7 -11
  72. package/python/foxclaw/capability/embedding_service.py +4 -6
  73. package/python/foxclaw/channels/__init__.py +9 -11
  74. package/python/foxclaw/channels/discord.py +4 -13
  75. package/python/foxclaw/channels/dispatcher.py +2 -27
  76. package/python/foxclaw/channels/feishu.py +10 -20
  77. package/python/foxclaw/channels/openclaw_bridge.py +4 -9
  78. package/python/foxclaw/channels/signing.py +9 -9
  79. package/python/foxclaw/channels/telegram.py +3 -4
  80. package/python/foxclaw/channels/weixin.py +7 -17
  81. package/python/foxclaw/cli.py +42 -13
  82. package/python/foxclaw/cognitive/reasoner.py +9 -10
  83. package/python/foxclaw/cognitive/state.py +18 -24
  84. package/python/foxclaw/core/audit_chain.py +1 -3
  85. package/python/foxclaw/core/audit_sink.py +3 -3
  86. package/python/foxclaw/core/compaction.py +8 -26
  87. package/python/foxclaw/core/config.py +2 -2
  88. package/python/foxclaw/core/confirm_nonce.py +6 -12
  89. package/python/foxclaw/core/content_guard.py +341 -0
  90. package/python/foxclaw/core/envfile.py +2 -2
  91. package/python/foxclaw/core/failure_policy.py +17 -26
  92. package/python/foxclaw/core/gateway.py +6 -10
  93. package/python/foxclaw/core/governance_audit.py +3 -7
  94. package/python/foxclaw/core/idempotency.py +15 -22
  95. package/python/foxclaw/core/injection_guard.py +10 -29
  96. package/python/foxclaw/core/memory_adapter.py +53 -60
  97. package/python/foxclaw/core/metrics.py +4 -5
  98. package/python/foxclaw/core/model_router.py +74 -150
  99. package/python/foxclaw/core/paths.py +1 -1
  100. package/python/foxclaw/core/recovery.py +21 -34
  101. package/python/foxclaw/core/scheduler.py +23 -26
  102. package/python/foxclaw/core/secrets.py +0 -3
  103. package/python/foxclaw/core/sensitive_write.py +283 -0
  104. package/python/foxclaw/core/sessions.py +4 -6
  105. package/python/foxclaw/core/settings.py +16 -32
  106. package/python/foxclaw/core/shared_write.py +11 -23
  107. package/python/foxclaw/core/steering_queue.py +12 -11
  108. package/python/foxclaw/core/system_bus.py +1 -1
  109. package/python/foxclaw/core/thought_chain.py +30 -32
  110. package/python/foxclaw/core/token_meter.py +9 -13
  111. package/python/foxclaw/core/tools.py +4 -12
  112. package/python/foxclaw/core/tracing.py +25 -40
  113. package/python/foxclaw/core/truth_pipeline.py +5 -9
  114. package/python/foxclaw/core/update_check.py +149 -0
  115. package/python/foxclaw/core/vec_recall_adapter.py +11 -37
  116. package/python/foxclaw/core/verify.py +6 -14
  117. package/python/foxclaw/core/web_search.py +4 -28
  118. package/python/foxclaw/finance/evidence.py +14 -18
  119. package/python/foxclaw/finance/fx.py +1 -3
  120. package/python/foxclaw/finance/graph.py +9 -18
  121. package/python/foxclaw/finance/models.py +20 -24
  122. package/python/foxclaw/finance/nlparse.py +12 -72
  123. package/python/foxclaw/finance/policy.py +8 -36
  124. package/python/foxclaw/finance/reasoner.py +3 -11
  125. package/python/foxclaw/finance/reasoning.py +13 -34
  126. package/python/foxclaw/finance/reconcile.py +5 -10
  127. package/python/foxclaw/finance/runtime.py +19 -46
  128. package/python/foxclaw/governance/middleware.py +13 -21
  129. package/python/foxclaw/harness/_base.py +2 -8
  130. package/python/foxclaw/harness/_policy.py +19 -38
  131. package/python/foxclaw/harness/drift.py +1 -13
  132. package/python/foxclaw/harness/event_bus.py +2 -5
  133. package/python/foxclaw/harness/harness.py +9 -97
  134. package/python/foxclaw/harness/loop_detection.py +11 -18
  135. package/python/foxclaw/itinerary/amap_client.py +2 -5
  136. package/python/foxclaw/itinerary/models.py +7 -7
  137. package/python/foxclaw/itinerary/reasoner.py +1 -6
  138. package/python/foxclaw/itinerary/reasoning.py +15 -21
  139. package/python/foxclaw/itinerary/runtime.py +8 -12
  140. package/python/foxclaw/kernel/capabilities.py +5 -6
  141. package/python/foxclaw/kernel/policy_gate.py +4 -10
  142. package/python/foxclaw/kernel/process_manager.py +17 -36
  143. package/python/foxclaw/kernel/scheduler.py +3 -9
  144. package/python/foxclaw/kernel/sentinel.py +13 -23
  145. package/python/foxclaw/kernel/sentinel_client.py +12 -15
  146. package/python/foxclaw/kernel/spine.py +9 -9
  147. package/python/foxclaw/knowledge/__init__.py +4 -20
  148. package/python/foxclaw/knowledge/truth_crystallizer.py +5 -8
  149. package/python/foxclaw/learning/knowledge_graph.py +4 -9
  150. package/python/foxclaw/learning/models.py +13 -13
  151. package/python/foxclaw/learning/reasoner.py +1 -4
  152. package/python/foxclaw/learning/runtime.py +19 -31
  153. package/python/foxclaw/learning/spaced_repetition.py +2 -7
  154. package/python/foxclaw/mcp_server.py +2 -3
  155. package/python/foxclaw/orchestration/__init__.py +16 -33
  156. package/python/foxclaw/pipeline/cognitive_pipeline.py +45 -97
  157. package/python/foxclaw/pipeline/stage_meta.py +21 -0
  158. package/python/foxclaw/pipeline/supervisor_stages.py +13 -42
  159. package/python/foxclaw/planning/llm_planner.py +3 -3
  160. package/python/foxclaw/planning/planner.py +6 -11
  161. package/python/foxclaw/search/agent_port.py +7 -11
  162. package/python/foxclaw/search/evidence.py +0 -4
  163. package/python/foxclaw/search/internal_search.py +3 -5
  164. package/python/foxclaw/search/models.py +11 -13
  165. package/python/foxclaw/search/planner.py +3 -3
  166. package/python/foxclaw/search/retriever.py +1 -1
  167. package/python/foxclaw/search/runtime.py +1 -8
  168. package/python/foxclaw/skills/__init__.py +46 -100
  169. package/python/foxclaw/skills/archive.py +111 -0
  170. package/python/foxclaw/skills/fetch.py +226 -0
  171. package/python/foxclaw/skills/hooks_spec.py +21 -25
  172. package/python/foxclaw/skills/install_cmd.py +216 -0
  173. package/python/foxclaw/skills/lifecycle.py +16 -42
  174. package/python/foxclaw/skills/markdown_skill.py +10 -9
  175. package/python/foxclaw/skills/mcp_client.py +5 -11
  176. package/python/foxclaw/skills/mcp_store.py +0 -3
  177. package/python/foxclaw/skills/permission.py +13 -48
  178. package/python/foxclaw/skills/pipeline.py +21 -53
  179. package/python/foxclaw/skills/policy.py +4 -10
  180. package/python/foxclaw/skills/review.py +3 -3
  181. package/python/frontend/deck/auth.js +0 -7
  182. package/python/frontend/deck/chat_model_picker.js +0 -54
  183. package/python/frontend/deck/domain.js +0 -20
  184. package/python/frontend/deck/main.js +0 -144
  185. package/python/frontend/deck/markdown.js +0 -4
  186. package/python/frontend/deck/settings.js +1 -18
  187. package/python/frontend/deck/state.js +0 -1
  188. package/python/frontend/deck/util.js +0 -2
  189. package/python/frontend/deck/visuals.js +0 -22
  190. package/python/frontend/planet/planet.js +18 -201
  191. package/python/frontend/v3/assets/index-JKfHlb-a.css +1 -0
  192. package/python/frontend/v3/assets/index-tN90dpUH.js +34 -0
  193. package/python/frontend/v3/index.html +3 -3
  194. package/python/frontend/widget_runtime.js +0 -43
  195. package/python/frontend/widgets.js +0 -57
  196. package/python/frontend_src/index.html +1 -1
  197. package/python/frontend_src/package-lock.json +2 -2
  198. package/python/frontend_src/package.json +1 -1
  199. package/python/frontend_src/src/App.vue +3 -7
  200. package/python/frontend_src/src/api.js +0 -7
  201. package/python/frontend_src/src/components/ChatDock.vue +27 -39
  202. package/python/frontend_src/src/components/DocViewer.vue +0 -9
  203. package/python/frontend_src/src/components/Inspector.vue +0 -6
  204. package/python/frontend_src/src/components/LeftRail.vue +60 -39
  205. package/python/frontend_src/src/components/MenuPopover.vue +9 -11
  206. package/python/frontend_src/src/components/MessageStream.vue +18 -30
  207. package/python/frontend_src/src/components/ModelPicker.vue +0 -5
  208. package/python/frontend_src/src/components/QuickIngest.vue +0 -6
  209. package/python/frontend_src/src/components/Stage.vue +71 -25
  210. package/python/frontend_src/src/components/ToolMenu.vue +0 -3
  211. package/python/frontend_src/src/components/TopBar.vue +38 -10
  212. package/python/frontend_src/src/components/panels/ActivityPanel.vue +0 -2
  213. package/python/frontend_src/src/components/panels/AgentsPanel.vue +2 -5
  214. package/python/frontend_src/src/components/panels/KnowledgePanel.vue +0 -16
  215. package/python/frontend_src/src/components/panels/MemoryPanel.vue +42 -3
  216. package/python/frontend_src/src/components/panels/PanelBox.vue +6 -7
  217. package/python/frontend_src/src/components/panels/ReviewPanel.vue +0 -16
  218. package/python/frontend_src/src/components/panels/SettingsPanel.vue +142 -38
  219. package/python/frontend_src/src/components/panels/TasksPanel.vue +0 -1
  220. package/python/frontend_src/src/components/panels/ToolsPanel.vue +125 -19
  221. package/python/frontend_src/src/lib/fmt.js +0 -4
  222. package/python/frontend_src/src/lib/i18n.js +53 -0
  223. package/python/frontend_src/src/lib/locales/en.js +173 -0
  224. package/python/frontend_src/src/lib/locales/zh.js +174 -0
  225. package/python/frontend_src/src/lib/md.js +0 -14
  226. package/python/frontend_src/src/main.js +2 -0
  227. package/python/frontend_src/src/stores/app.js +17 -32
  228. package/python/frontend_src/src/stores/chat.js +0 -34
  229. package/python/frontend_src/src/stores/model.js +0 -8
  230. package/python/frontend_src/src/stores/panels.js +16 -44
  231. package/python/frontend_src/src/stores/profile.js +31 -0
  232. package/python/frontend_src/src/stores/system.js +25 -0
  233. package/python/frontend_src/vite.config.js +0 -7
  234. package/python/pyproject.toml +17 -3
  235. package/python/requirements.txt +311 -0
  236. package/python/tests/conftest.py +0 -22
  237. package/python/tests/run_all.py +4 -66
  238. package/python/tests/test_agent_lifecycle.py +4 -38
  239. package/python/tests/test_agent_rules.py +81 -0
  240. package/python/tests/test_ahs_p2.py +4 -25
  241. package/python/tests/test_anthropic_tools.py +1 -15
  242. package/python/tests/test_api.py +19 -9
  243. package/python/tests/test_api_auth.py +1 -5
  244. package/python/tests/test_attachments.py +9 -25
  245. package/python/tests/test_audit_chain.py +2 -7
  246. package/python/tests/test_audit_sink.py +0 -6
  247. package/python/tests/test_behavior_spec.py +198 -0
  248. package/python/tests/test_browser.py +2 -6
  249. package/python/tests/test_browser_request_guard.py +0 -4
  250. package/python/tests/test_capabilities.py +0 -6
  251. package/python/tests/test_channel_signing.py +2 -9
  252. package/python/tests/test_channels.py +1 -11
  253. package/python/tests/test_config_layering.py +0 -4
  254. package/python/tests/test_confirm_nonce.py +1 -12
  255. package/python/tests/test_conflict_dismiss.py +143 -0
  256. package/python/tests/test_content_guard.py +177 -0
  257. package/python/tests/test_context_compaction_wiring.py +1 -7
  258. package/python/tests/test_dispatch.py +0 -6
  259. package/python/tests/test_domain_evals.py +11 -80
  260. package/python/tests/test_domain_import.py +0 -6
  261. package/python/tests/test_domain_reasoners.py +0 -14
  262. package/python/tests/test_e2e.py +0 -17
  263. package/python/tests/test_emb_cache.py +7 -17
  264. package/python/tests/test_embedding_singleton.py +1 -3
  265. package/python/tests/test_evals_smoke.py +0 -24
  266. package/python/tests/test_event_spine.py +10 -18
  267. package/python/tests/test_evidence_semantic.py +1 -14
  268. package/python/tests/test_evidence_sse.py +3 -6
  269. package/python/tests/test_file_ingest.py +0 -11
  270. package/python/tests/test_finance.py +0 -9
  271. package/python/tests/test_finance_ledger.py +5 -12
  272. package/python/tests/test_finance_persist.py +8 -12
  273. package/python/tests/test_gateway.py +3 -22
  274. package/python/tests/test_governance.py +5 -52
  275. package/python/tests/test_governance_audit.py +4 -39
  276. package/python/tests/test_governance_middleware.py +1 -7
  277. package/python/tests/test_hkc.py +6 -24
  278. package/python/tests/test_hkc_index_build_manager.py +0 -33
  279. package/python/tests/test_hkc_manifest_reuse.py +4 -27
  280. package/python/tests/test_hkc_search_event_sync.py +3 -54
  281. package/python/tests/test_import_path_guard.py +0 -6
  282. package/python/tests/test_injection_guard.py +18 -36
  283. package/python/tests/test_integration.py +5 -29
  284. package/python/tests/test_itinerary.py +0 -3
  285. package/python/tests/test_kee_semantic.py +0 -5
  286. package/python/tests/test_kernel.py +1 -11
  287. package/python/tests/test_learning.py +3 -12
  288. package/python/tests/test_llm_admin.py +4 -19
  289. package/python/tests/test_llm_planner.py +1 -23
  290. package/python/tests/test_markdown_skills.py +3 -14
  291. package/python/tests/test_markdown_skills_openclaw.py +4 -9
  292. package/python/tests/test_mcp_connectors.py +3 -15
  293. package/python/tests/test_mcp_guardian_discovery.py +2 -6
  294. package/python/tests/test_mcp_no_memory.py +3 -3
  295. package/python/tests/test_mcp_policy_gate.py +0 -1
  296. package/python/tests/test_mcp_server.py +6 -9
  297. package/python/tests/test_memory_recall.py +1 -4
  298. package/python/tests/test_metrics.py +0 -8
  299. package/python/tests/test_multi_chain.py +1 -13
  300. package/python/tests/test_multimodal.py +5 -18
  301. package/python/tests/test_negotiation.py +3 -13
  302. package/python/tests/test_orchestration.py +1 -11
  303. package/python/tests/test_orchestration_decoupled.py +0 -6
  304. package/python/tests/test_p3_p5_hardening.py +3 -17
  305. package/python/tests/test_parallel_dispatch.py +4 -12
  306. package/python/tests/test_pipeline.py +17 -58
  307. package/python/tests/test_plan_visible.py +4 -4
  308. package/python/tests/test_planner.py +0 -7
  309. package/python/tests/test_policy_gate.py +2 -3
  310. package/python/tests/test_profile.py +67 -0
  311. package/python/tests/test_readme_claims.py +163 -0
  312. package/python/tests/test_reasoner.py +2 -17
  313. package/python/tests/test_release_check.py +2 -13
  314. package/python/tests/test_resume_checkpoint.py +7 -15
  315. package/python/tests/test_routing.py +2 -12
  316. package/python/tests/test_scheduler.py +7 -10
  317. package/python/tests/test_search.py +1 -11
  318. package/python/tests/test_secrets.py +4 -10
  319. package/python/tests/test_semantic.py +1 -10
  320. package/python/tests/test_sensitive_write.py +181 -0
  321. package/python/tests/test_sessions.py +3 -4
  322. package/python/tests/test_settings.py +2 -18
  323. package/python/tests/test_shadow_learning.py +3 -43
  324. package/python/tests/test_shared_write.py +0 -7
  325. package/python/tests/test_skill_taint_gate.py +1 -10
  326. package/python/tests/test_skills.py +2 -18
  327. package/python/tests/test_skills_fetch.py +282 -0
  328. package/python/tests/test_stats_and_events.py +2 -5
  329. package/python/tests/test_streaming.py +2 -7
  330. package/python/tests/test_supervisor_pipeline.py +4 -5
  331. package/python/tests/test_supervisor_triage.py +4 -10
  332. package/python/tests/test_taint.py +2 -7
  333. package/python/tests/test_task_stream.py +1 -12
  334. package/python/tests/test_thinking_sse.py +0 -1
  335. package/python/tests/test_tool_center.py +1 -17
  336. package/python/tests/test_tool_parallel.py +2 -2
  337. package/python/tests/test_tool_params.py +1 -5
  338. package/python/tests/test_tool_use.py +0 -13
  339. package/python/tests/test_toplevel_concurrency.py +3 -4
  340. package/python/tests/test_tracing.py +7 -41
  341. package/python/tests/test_truth_crystallizer.py +4 -6
  342. package/python/tests/test_truth_flow.py +2 -7
  343. package/python/tests/test_update_check.py +95 -0
  344. package/python/tests/test_verify_evidence.py +8 -21
  345. package/python/tests/test_version_sync.py +35 -6
  346. package/python/tests/test_web_trigger.py +0 -17
  347. package/python/tools/build_content_lexicon.py +218 -0
  348. package/python/tools/bundle_web.py +98 -0
  349. package/python/tools/check_faiss_type.py +0 -1
  350. package/python/tools/check_log.ps1 +0 -8
  351. package/python/tools/mypy_baseline.txt +12 -4
  352. package/python/tools/mypy_ratchet.py +52 -6
  353. package/python/tools/pack.py +0 -5
  354. package/python/tools/recheck_conflicts.py +156 -0
  355. package/python/tools/release_check.py +24 -15
  356. package/python/tools/restart_foxclaw.ps1 +5 -8
  357. package/python/tools/shared_write_audit.py +1 -15
  358. package/python/tools/size_check.py +1 -1
  359. package/python/tools/strip_comments.py +228 -0
  360. package/python/vendor/hkc/hkc_api/routes/ability.py +31 -0
  361. package/python/vendor/hkc/hkc_api/schemas.py +11 -0
  362. package/python/vendor/hkc/hkc_kee/kee.py +124 -3
  363. package/python/vendor/hkc/hkc_sdk/base.py +22 -0
  364. package/python/vendor/hkc/hkc_sdk/direct_client.py +10 -0
  365. package/python/vendor/hkc/hkc_sdk/http_client.py +19 -0
  366. package/python/foxclaw/api/app.py +0 -123
  367. package/python/frontend/v3/assets/index-BD7VPbMV.js +0 -33
  368. package/python/frontend/v3/assets/index-CAvnYWGH.css +0 -1
@@ -29,14 +29,11 @@ import contextvars
29
29
  from collections import deque
30
30
  from contextlib import contextmanager
31
31
  from . import settings
32
+ from typing import Any
32
33
 
33
34
  logger = logging.getLogger(__name__)
34
35
 
35
36
 
36
- # ── 采样策略 ────────────────────────────────────────────────
37
- # 内存环存只留最近 N 条、重启即失 → 查不了"昨天那次慢请求"。持久化解决它,但不能无脑全存:
38
- # 正常请求的 trace 数量大、价值低。故:**慢的 / 出错的必存**(那才是你要查的),
39
- # 其余按采样率(默认 0 —— 个人 OS 留下的每条都值得看)。
40
37
  def _slow_ms() -> float:
41
38
  return float(settings.get_safe("FOXCLAW_TRACE_SLOW_MS"))
42
39
 
@@ -50,14 +47,9 @@ def _retain() -> int:
50
47
 
51
48
  _current: contextvars.ContextVar = contextvars.ContextVar("foxclaw_trace", default=None)
52
49
 
53
- MAX_SPANS_PER_TRACE = 200 # 单条 trace 的 span 上限,防病态循环把内存撑爆
50
+ MAX_SPANS_PER_TRACE = 200
54
51
 
55
52
 
56
- # ── W3C Trace Context(零依赖跨进程传播)────────────────────────
57
- # 为什么不上 opentelemetry-sdk:Foxclaw 是单进程 + local-first,为它装 SDK/exporter/grpc、
58
- # 再要求跑个 Jaeger,代价远大于收益。但它确实有一条真实跨进程边界——**MCP 双向**。
59
- # traceparent 是 W3C 标准、纯字符串、零依赖,恰好覆盖这条边界:
60
- # 00-{trace_id:32hex}-{span_id:16hex}-{flags:2hex}
61
53
  _TP_RE = __import__("re").compile(r"^00-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$")
62
54
 
63
55
 
@@ -69,12 +61,12 @@ def parse_traceparent(header: str):
69
61
  if not m:
70
62
  return None
71
63
  tid = m.group(1)
72
- if tid == "0" * 32: # 全零非法(规范要求)
64
+ if tid == "0" * 32:
73
65
  return None
74
66
  return tid
75
67
 
76
68
 
77
- def to_traceparent(trace_id: str, span_id: str = None) -> str:
69
+ def to_traceparent(trace_id: str, span_id: str | None = None) -> str:
78
70
  """给出站请求生成 traceparent。span_id 缺省随机(本进程作为对端的 parent)。"""
79
71
  tid = (trace_id or uuid.uuid4().hex)[:32].rjust(32, "0")
80
72
  sid = (span_id or uuid.uuid4().hex[:16])[:16].rjust(16, "0")
@@ -88,13 +80,12 @@ def current_traceparent():
88
80
 
89
81
 
90
82
  class Trace:
91
- def __init__(self, name: str, trace_id: str = None):
92
- # 32 hex:直接符合 W3C traceparent 的 trace-id 规格(跨进程可对齐)
83
+ def __init__(self, name: str, trace_id: str | None = None):
93
84
  self.trace_id = trace_id or uuid.uuid4().hex
94
85
  self.name = name
95
86
  self.start_ts = time.time()
96
87
  self._t0 = time.perf_counter()
97
- self.duration_ms = None
88
+ self.duration_ms: float | None = None
98
89
  self.spans: list = []
99
90
  self._lock = threading.Lock()
100
91
  self._dropped = 0
@@ -135,9 +126,8 @@ class TraceStore:
135
126
  def __init__(self, limit: int = 200):
136
127
  self._d: deque = deque(maxlen=limit)
137
128
  self._lock = threading.Lock()
138
- self._db_path = None
129
+ self._db_path: str | None = None
139
130
 
140
- # ── 持久化 ───────────────────────────────────────────
141
131
  def attach_db(self, path):
142
132
  """开启持久化并建表。连接**即建即弃、不跨线程复用**(与 ThoughtChain 同款):
143
133
  SQLite 连接不可跨线程共享,而 trace 写入发生在请求线程/to_thread 工作线程。"""
@@ -154,19 +144,21 @@ class TraceStore:
154
144
  self._db_path = None
155
145
 
156
146
  def _conn(self):
147
+ if self._db_path is None:
148
+ raise RuntimeError("trace 持久化未启用:先 attach_db")
157
149
  conn = sqlite3.connect(self._db_path, timeout=5.0)
158
150
  try:
159
- conn.execute("PRAGMA journal_mode=WAL") # 单写多读,降锁竞争
151
+ conn.execute("PRAGMA journal_mode=WAL")
160
152
  except sqlite3.OperationalError:
161
153
  pass
162
154
  return conn
163
155
 
164
156
  def _should_persist(self, tr: Trace) -> bool:
165
157
  if tr.has_error():
166
- return True # 出错的必留
158
+ return True
167
159
  if (tr.duration_ms or 0) >= _slow_ms():
168
- return True # 慢的必留(正是要查的)
169
- return random.random() < _sample_rate() # 其余按采样率(默认 0)
160
+ return True
161
+ return random.random() < _sample_rate()
170
162
 
171
163
  def _persist(self, tr: Trace):
172
164
  d = tr.to_dict()
@@ -176,7 +168,6 @@ class TraceStore:
176
168
  (tr.trace_id, tr.name, tr.start_ts, tr.duration_ms,
177
169
  d["span_count"], 0 if tr.has_error() else 1,
178
170
  json.dumps(d["spans"], ensure_ascii=False)))
179
- # 保留上限:超出删最旧 → 有界增长(不会把磁盘吃光)
180
171
  c.execute("DELETE FROM traces WHERE trace_id IN ("
181
172
  "SELECT trace_id FROM traces ORDER BY start_ts DESC "
182
173
  "LIMIT -1 OFFSET ?)", (_retain(),))
@@ -187,10 +178,10 @@ class TraceStore:
187
178
  with self._lock:
188
179
  self._d.append(tr)
189
180
  if self._db_path and self._should_persist(tr):
190
- self._persist(tr) # 锁外落库(慢 I/O 不挡内存环)
181
+ self._persist(tr)
191
182
  try:
192
- OTLP.submit(tr) # 可选导出:未配 endpoint 即 no-op;
193
- except Exception: # 队列满则丢,绝不阻塞请求
183
+ OTLP.submit(tr)
184
+ except Exception:
194
185
  pass
195
186
 
196
187
  def get(self, trace_id: str):
@@ -219,7 +210,7 @@ class TraceStore:
219
210
  "duration_ms": row[3], "span_count": row[4],
220
211
  "spans": json.loads(row[5] or "[]"), "persisted": True}
221
212
 
222
- def query(self, limit: int = 30, slow_ms: float = None, errors_only: bool = False):
213
+ def query(self, limit: int = 30, slow_ms: float | None = None, errors_only: bool = False):
223
214
  """查已落库的历史 trace(内存环之外)。这才是"找出昨天那次慢请求"的入口。"""
224
215
  if not self._db_path:
225
216
  return []
@@ -250,10 +241,6 @@ class TraceStore:
250
241
  self._d.clear()
251
242
 
252
243
 
253
- # ── OTLP/HTTP-JSON 导出(可选、零依赖)────────────────────────
254
- # 想接 Jaeger/Tempo/collector 的人设 FOXCLAW_OTLP_ENDPOINT 即可;不设则完全不启用、零成本。
255
- # 刻意**不引 opentelemetry-sdk**:OTLP 有 HTTP+JSON 传输,stdlib urllib 就能发。
256
- # 铁律:遥测**绝不能拖慢请求** —— 后台守护线程 + 有界队列,满了就丢(宁丢遥测不拖请求)。
257
244
  def _otlp_endpoint() -> str:
258
245
  return os.environ.get("FOXCLAW_OTLP_ENDPOINT", "").strip().rstrip("/")
259
246
 
@@ -272,16 +259,16 @@ def _trace_to_otlp(tr: "Trace") -> dict:
272
259
  root_id = uuid.uuid4().hex[:16]
273
260
  dur = (tr.duration_ms or 0.0) / 1000.0
274
261
  spans = [{
275
- "traceId": tr.trace_id, "spanId": root_id, "name": tr.name, "kind": 2, # SERVER
262
+ "traceId": tr.trace_id, "spanId": root_id, "name": tr.name, "kind": 2,
276
263
  "startTimeUnixNano": t_ns(tr.start_ts),
277
264
  "endTimeUnixNano": t_ns(tr.start_ts + dur),
278
265
  "status": {"code": 2 if tr.has_error() else 1},
279
266
  }]
280
267
  for s in tr.to_dict()["spans"]:
281
- st = s.get("ts", tr.start_ts) # 缺 ts(手工 add_span)→ 兜底
268
+ st = s.get("ts", tr.start_ts)
282
269
  spans.append({
283
270
  "traceId": tr.trace_id, "spanId": uuid.uuid4().hex[:16], "parentSpanId": root_id,
284
- "name": s.get("name", "span"), "kind": 1, # INTERNAL
271
+ "name": s.get("name", "span"), "kind": 1,
285
272
  "startTimeUnixNano": t_ns(st),
286
273
  "endTimeUnixNano": t_ns(st + (s.get("ms", 0.0) / 1000.0)),
287
274
  "attributes": _attrs(s.get("meta")),
@@ -298,8 +285,8 @@ class _OtlpExporter:
298
285
 
299
286
  def __init__(self, maxsize: int = 512):
300
287
  import queue
301
- self._q = queue.Queue(maxsize=maxsize)
302
- self._th = None
288
+ self._q: queue.Queue[Any] = queue.Queue(maxsize=maxsize)
289
+ self._th: threading.Thread | None = None
303
290
  self._lock = threading.Lock()
304
291
  self.dropped = 0
305
292
  self.sent = 0
@@ -319,7 +306,7 @@ class _OtlpExporter:
319
306
  try:
320
307
  self._q.put_nowait(tr)
321
308
  except queue.Full:
322
- self.dropped += 1 # 宁可丢遥测,也不让导出拖住请求线程
309
+ self.dropped += 1
323
310
 
324
311
  def _run(self):
325
312
  import urllib.request
@@ -338,7 +325,7 @@ class _OtlpExporter:
338
325
  self.sent += 1
339
326
  except Exception as e:
340
327
  self.failed += 1
341
- if self.failed <= 3: # 限量告警,别把日志刷爆
328
+ if self.failed <= 3:
342
329
  logger.warning("OTLP 导出失败(不影响请求): %s", e)
343
330
 
344
331
  def stats(self) -> dict:
@@ -351,7 +338,7 @@ OTLP = _OtlpExporter()
351
338
  TRACES = TraceStore()
352
339
 
353
340
 
354
- def start_trace(name: str, trace_id: str = None) -> Trace:
341
+ def start_trace(name: str, trace_id: str | None = None) -> Trace:
355
342
  """开一条 trace 并绑到当前上下文(由 API 中间件按请求调用)。"""
356
343
  tr = Trace(name, trace_id)
357
344
  _current.set(tr)
@@ -375,8 +362,6 @@ def span(name: str, **meta):
375
362
  yield None
376
363
  return
377
364
  t0 = time.perf_counter()
378
- # ts:span 开始的 epoch 秒。内部只需 ms 就够,但 OTLP 要求每个 span 有 start/end 绝对时刻,
379
- # 故一并记下(开销可忽略;缺失时导出侧会优雅兜底)。
380
365
  rec = {"name": name, "ts": time.time(), "ok": True,
381
366
  **({"meta": meta} if meta else {})}
382
367
  try:
@@ -16,6 +16,7 @@
16
16
  """
17
17
  from __future__ import annotations
18
18
  from datetime import datetime
19
+ from typing import Any
19
20
 
20
21
 
21
22
  class TruthPipeline:
@@ -34,13 +35,10 @@ class TruthPipeline:
34
35
 
35
36
  def __init__(self, bus, hmr=None):
36
37
  self.bus = bus
37
- self.hmr = hmr # 真实 HMR 记忆后端(可空,空则跳过记忆沉淀)
38
+ self.hmr = hmr
38
39
  self.last_trace = None
39
40
  self.last_memory_update = None
40
41
 
41
- # 订阅事件链。【P4-1】三个都是精确 channel 名 → exact=True:
42
- # 门面化后 SystemBus 默认是前缀匹配,不锁定的话 "execution.completed.extra"
43
- # 之类的前缀事件也会触发真值链(语义漂移)。
44
42
  bus.subscribe("execution.completed", self._on_execution_completed, exact=True)
45
43
  bus.subscribe("trace.recorded", self._on_trace_recorded, exact=True)
46
44
  bus.subscribe("truth.appended", self._on_truth_appended, exact=True)
@@ -72,7 +70,6 @@ class TruthPipeline:
72
70
  self.last_memory_update = {"persisted": False, "reason": "blocked"}
73
71
  self.bus.publish("truth.skipped", {**p, "reason": "blocked"})
74
72
  return
75
- # 真值 = "任务被如此处理"的事实 + 产出要点(信息量足够供日后召回)
76
73
  result_brief = str(p.get("result", ""))[:200]
77
74
  truth = {
78
75
  "fact": (f"任务「{p.get('task')}」由 {p.get('agent_id')} 处理。"
@@ -93,18 +90,17 @@ class TruthPipeline:
93
90
  【P3-5】双保险:_on_trace_recorded 已挡 blocked,这里再检查一次。
94
91
  """
95
92
  p = msg.payload
96
- update = {"persisted": False}
93
+ update: dict[str, Any] = {"persisted": False}
97
94
  if self.hmr is not None and not p.get("blocked"):
98
95
  try:
99
- # 沉淀到 HMR 共享层(所有 Agent 可见),用真实的 ingest 接口
100
96
  content = p.get("truth", {}).get("fact", "")
101
97
  origin_agent = p.get("agent_id", "unknown")
102
98
  self.hmr.ingest(
103
99
  content,
104
100
  type="execution",
105
- agent_id="system", # 【P3-0】系统级写入(非来源 Agent)
101
+ agent_id="system",
106
102
  title=f"真值_{datetime.now().strftime('%Y%m%d_%H%M%S')}",
107
- tags=["truth_pipeline", f"origin:{origin_agent}"], # 来源可追溯
103
+ tags=["truth_pipeline", f"origin:{origin_agent}"],
108
104
  shared=True,
109
105
  )
110
106
  update = {"persisted": True, "content": content}
@@ -0,0 +1,149 @@
1
+ """可选的新版本检查(默认关)。
2
+
3
+ 为什么默认关:foxclaw 的定位是本地优先 —— 默认外呼会让「断网也能用」多一个
4
+ 例外,也给安全审计多一个出网点。**只有** 设置 ``FOXCLAW_UPDATE_CHECK=1`` 时才会
5
+ 访问 PyPI,其余情况 ``status()`` 里 ``enabled=False``、``update_available=False``。
6
+
7
+ 失败一律静默:查版本是锦上添花,不能因为它让 ``/api/system/info`` 变慢或报错。
8
+ - 结果按 24h 缓存到 ``<FOXCLAW_DATA_DIR>/update_check.json``;
9
+ - 查询在 daemon 线程里跑,不阻塞请求;
10
+ - 查失败时保留上次成功的结果(只是不刷新时间),避免一次网络抖动让提示消失。
11
+
12
+ 只发一个 GET ``https://pypi.org/pypi/foxclaw/json``,不带任何本机信息。
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import os
19
+ import threading
20
+ import time
21
+ from pathlib import Path
22
+ from typing import Any
23
+
24
+ _PYPI_URL = "https://pypi.org/pypi/foxclaw/json"
25
+ _TTL = 24 * 3600
26
+ _TIMEOUT = 3.0
27
+
28
+ _lock = threading.Lock()
29
+ _inflight = False
30
+ _last_error: str | None = None
31
+
32
+
33
+ def _enabled() -> bool:
34
+ return os.environ.get("FOXCLAW_UPDATE_CHECK", "").strip().lower() in {"1", "true", "yes", "on"}
35
+
36
+
37
+ def _current() -> str:
38
+ from foxclaw import __version__
39
+
40
+ return __version__
41
+
42
+
43
+ def _data_dir() -> Path:
44
+ return Path(os.environ.get("FOXCLAW_DATA_DIR") or "./foxclaw_data").expanduser()
45
+
46
+
47
+ def _cache_path() -> Path:
48
+ return _data_dir() / "update_check.json"
49
+
50
+
51
+ def parse_version(value: str) -> tuple[int, ...]:
52
+ """``"0.1.10"`` → ``(0, 1, 10)``。只取各段的前导数字,预发布/本地后缀丢掉。
53
+
54
+ 不引入 ``packaging``:这里只需要「谁更新」,自己比数字段足够,也少一个依赖。
55
+ """
56
+ parts: list[int] = []
57
+ for chunk in str(value or "").strip().split("."):
58
+ digits = ""
59
+ for ch in chunk:
60
+ if not ch.isdigit():
61
+ break
62
+ digits += ch
63
+ if not digits:
64
+ break
65
+ parts.append(int(digits))
66
+ return tuple(parts)
67
+
68
+
69
+ def is_newer(latest: str, current: str) -> bool:
70
+ """latest 是否严格新于 current(逐段比,缺的段补 0)。"""
71
+ left, right = parse_version(latest), parse_version(current)
72
+ width = max(len(left), len(right))
73
+ return left + (0,) * (width - len(left)) > right + (0,) * (width - len(right))
74
+
75
+
76
+ def _read_cache() -> dict[str, Any]:
77
+ try:
78
+ data = json.loads(_cache_path().read_text(encoding="utf-8"))
79
+ except Exception:
80
+ return {}
81
+ return data if isinstance(data, dict) else {}
82
+
83
+
84
+ def _write_cache(data: dict[str, Any]) -> None:
85
+ try:
86
+ path = _cache_path()
87
+ path.parent.mkdir(parents=True, exist_ok=True)
88
+ path.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
89
+ except Exception:
90
+ pass
91
+
92
+
93
+ def _fetch_latest() -> str:
94
+ import urllib.request
95
+
96
+ req = urllib.request.Request(
97
+ _PYPI_URL,
98
+ headers={"Accept": "application/json", "User-Agent": f"foxclaw/{_current()}"},
99
+ )
100
+ with urllib.request.urlopen(req, timeout=_TIMEOUT) as resp: # noqa: S310 - 地址是常量 https
101
+ payload = json.loads(resp.read().decode("utf-8", "replace"))
102
+ version = ((payload or {}).get("info") or {}).get("version") or ""
103
+ if not version:
104
+ raise ValueError("PyPI 响应里没有 info.version")
105
+ return str(version)
106
+
107
+
108
+ def _refresh() -> None:
109
+ global _inflight, _last_error
110
+ try:
111
+ latest = _fetch_latest()
112
+ _write_cache({"latest": latest, "checked_at": time.time(), "current": _current()})
113
+ _last_error = None
114
+ except Exception as exc:
115
+ _last_error = f"{type(exc).__name__}: {exc}"
116
+ finally:
117
+ with _lock:
118
+ _inflight = False
119
+
120
+
121
+ def maybe_refresh() -> None:
122
+ """启用且缓存过期 → 后台查一次。幂等、非阻塞,可放心在每个 system/info 里调。"""
123
+ global _inflight
124
+ if not _enabled():
125
+ return
126
+ checked = _read_cache().get("checked_at")
127
+ if isinstance(checked, (int, float)) and time.time() - checked < _TTL:
128
+ return
129
+ with _lock:
130
+ if _inflight:
131
+ return
132
+ _inflight = True
133
+ threading.Thread(target=_refresh, name="foxclaw-update-check", daemon=True).start()
134
+
135
+
136
+ def status() -> dict[str, Any]:
137
+ """给 ``/api/system/info`` 的只读快照。未启用时 latest 仍是缓存值,但不报可更新。"""
138
+ enabled = _enabled()
139
+ current = _current()
140
+ cache = _read_cache()
141
+ latest = str(cache.get("latest") or "")
142
+ return {
143
+ "enabled": enabled,
144
+ "current": current,
145
+ "latest": latest or None,
146
+ "update_available": bool(enabled and latest and is_newer(latest, current)),
147
+ "checked_at": cache.get("checked_at"),
148
+ "error": _last_error,
149
+ }
@@ -31,9 +31,8 @@ from pathlib import Path
31
31
  from types import SimpleNamespace
32
32
 
33
33
  from .shared_write import guard_shared_write
34
+ from typing import Any
34
35
 
35
- # Foxclaw 的 type 白名单 → vecRecall 的 memory_type(episodic/semantic/procedural)。
36
- # 分错不影响读写,但影响 vecRecall 的遗忘曲线与 procedural 精确匹配。
37
36
  _FOXCLAW_TO_VR = {
38
37
  "concept": "semantic",
39
38
  "project": "semantic",
@@ -52,7 +51,6 @@ _VR_TO_FOXCLAW = {
52
51
  }
53
52
  _VALID_TYPES = set(_FOXCLAW_TO_VR)
54
53
 
55
- # 偏好类记忆走 topic_key(精确查询),因为"总是注入的偏好"不该做语义模糊匹配。
56
54
  _PREFERENCE_TAG = "preference"
57
55
 
58
56
 
@@ -77,7 +75,7 @@ class _HKCSharedEmbedding:
77
75
  def __init__(self, model_name: str):
78
76
  self.model_name = model_name
79
77
  self._model = None
80
- self._dim = None
78
+ self._dim: int | None = None
81
79
 
82
80
  def _load(self):
83
81
  if self._model is None:
@@ -85,8 +83,6 @@ class _HKCSharedEmbedding:
85
83
  from hkc_search.embedding_backends import get_st_model
86
84
  model = get_st_model(self.model_name)
87
85
  if model is None:
88
- # 不回退到直连 SentenceTransformer:那会绕过 HKC 的进程级共享缓存、
89
- # 多加载一份模型(bge-m3 常驻约 1GB)。失败就明确报错让工厂回退 HMR。
90
86
  raise RuntimeError(
91
87
  f"embedding 模型不可用: {self.model_name}(HKC 共享缓存加载失败)")
92
88
  self._model = model
@@ -105,7 +101,6 @@ class _HKCSharedEmbedding:
105
101
  if self._dim is None:
106
102
  try:
107
103
  model = self._load()
108
- # ST 6.x 把 get_sentence_embedding_dimension 改名了,两个都试
109
104
  getter = (getattr(model, "get_embedding_dimension", None)
110
105
  or getattr(model, "get_sentence_embedding_dimension", None))
111
106
  self._dim = (int(getter() or 0) or None) if getter else None
@@ -120,7 +115,7 @@ class VecRecallAdapter:
120
115
  backend = "vecRecall"
121
116
 
122
117
  def __init__(self, storage_path: str = "./vecrecall_data", wing: str = "foxclaw",
123
- use_chain: bool = False, embedding_model: str = None,
118
+ use_chain: bool = False, embedding_model: str | None = None,
124
119
  dedup: bool = True, embedding_backend=None, vector_backend=None):
125
120
  """`embedding_backend` / `vector_backend` 用于注入(测试传轻量实现)。
126
121
 
@@ -133,9 +128,6 @@ class VecRecallAdapter:
133
128
  self.wing = wing
134
129
  self.storage_path = str(storage_path)
135
130
  self.version = self._resolve_version()
136
- # vecRecall 本体没有任何锁(只有 REST server 层有 RLock),而它用的是
137
- # check_same_thread=False 的单条 sqlite 连接 + 内存 dict 索引。并行子 Agent
138
- # 同时 add/search 属于未定义行为,和 HMR 是同一类坑 —— 这里沿用同一套串行化。
139
131
  self._lock = threading.RLock()
140
132
 
141
133
  self._emb = embedding_backend or _HKCSharedEmbedding(
@@ -152,7 +144,6 @@ class VecRecallAdapter:
152
144
  **extra,
153
145
  )
154
146
 
155
- # ── 构造辅助 ──────────────────────────────────────────
156
147
  @staticmethod
157
148
  def _resolve_version() -> str:
158
149
  try:
@@ -162,7 +153,7 @@ class VecRecallAdapter:
162
153
  return "unknown"
163
154
 
164
155
  @staticmethod
165
- def _resolve_model(embedding_model: str = None) -> str:
156
+ def _resolve_model(embedding_model: str | None = None) -> str:
166
157
  """与 HMRAdapter 同一条解析链,保证两套后端用同一个模型(分数与维度可比)。"""
167
158
  return (embedding_model
168
159
  or os.environ.get("EMBEDDING_MODEL")
@@ -188,14 +179,13 @@ class VecRecallAdapter:
188
179
  except Exception:
189
180
  return
190
181
  dims = {int(r[0]) for r in rows}
191
- want = getattr(self._emb, "dim", None) # 轻量后端无 dim 属性时跳过
182
+ want = getattr(self._emb, "dim", None)
192
183
  if dims and want and dims != {int(want)}:
193
184
  raise RuntimeError(
194
185
  f"已有向量维度 {sorted(dims)} 与当前嵌入维度 {want} 不一致。"
195
186
  f"换 embedding 模型必须重建索引:删除 {self.storage_path} 后重启"
196
187
  )
197
188
 
198
- # ── 摄入 ──────────────────────────────────────────────
199
189
  _AUTO_TITLE = re.compile(r"^[A-Za-z_]+\s+\d{1,2}:\d{2}$")
200
190
 
201
191
  @staticmethod
@@ -214,8 +204,8 @@ class VecRecallAdapter:
214
204
  s = re.split(r"[。!?!?\n\r]", s, 1)[0].strip()
215
205
  return " ".join(s.split())[:80]
216
206
 
217
- def ingest(self, content: str, type: str = "concept", agent_id: str = None,
218
- title: str = None, tags: list = None, shared: bool = False, **kwargs):
207
+ def ingest(self, content: str, type: str = "concept", agent_id: str | None = None,
208
+ title: str | None = None, tags: list | None = None, shared: bool = False, **kwargs):
219
209
  """统一摄入。签名与 `HMRAdapter.ingest` 一致。
220
210
 
221
211
  隔离承载:`agent_id` → owner,`shared=True` → visibility="shared"。
@@ -231,8 +221,6 @@ class VecRecallAdapter:
231
221
  if not title:
232
222
  title = self._derive_title(content) or ""
233
223
 
234
- # 【P0-3】共享层写入闸门:必须在落库之前判定,语义与 HMR 路径完全一致,
235
- # 否则同一个策略在两个后端上表现不同。
236
224
  if shared:
237
225
  guard_shared_write(self, agent_id, content, mtype, meta_tags)
238
226
 
@@ -240,8 +228,6 @@ class VecRecallAdapter:
240
228
  if "confidence" in kwargs:
241
229
  metadata["confidence"] = kwargs["confidence"]
242
230
 
243
- # execution 是审计证据:被遗忘曲线衰减或被去重合并,就无法再证明"当时确实
244
- # 这么做了"。vecRecall 的 immutable 同时免于衰减与去重,正好对上这个语义。
245
231
  retention = "immutable" if mtype == "execution" else "decay"
246
232
  topic_key = kwargs.get("topic_key") or (
247
233
  _PREFERENCE_TAG if _PREFERENCE_TAG in meta_tags else "")
@@ -260,8 +246,7 @@ class VecRecallAdapter:
260
246
  topic_key=topic_key,
261
247
  )
262
248
 
263
- # ── 召回 ──────────────────────────────────────────────
264
- def _visible(self, mem, agent_id: str, include_shared: bool) -> bool:
249
+ def _visible(self, mem, agent_id: str | None, include_shared: bool) -> bool:
265
250
  """可见性判定,与 vecRecall `_visible_to` 规则保持同源。
266
251
 
267
252
  - shared 记忆:仅当 include_shared 时可见
@@ -276,7 +261,7 @@ class VecRecallAdapter:
276
261
  return include_shared
277
262
  return owner == agent_id or not owner
278
263
 
279
- def recall(self, query: str = "", agent_id: str = None, top_k: int = 5,
264
+ def recall(self, query: str = "", agent_id: str | None = None, top_k: int = 5,
280
265
  include_shared: bool = True, **kwargs):
281
266
  """统一召回,返回 list[dict]。
282
267
 
@@ -300,9 +285,6 @@ class VecRecallAdapter:
300
285
  results = list(getattr(bundle, "l2_topic_context", []) or []) + \
301
286
  list(getattr(bundle, "l3_deep_results", []) or [])
302
287
  if not results and (query or "").strip():
303
- # L2 有 0.55 阈值:命中偏弱时整层为空。HMR 的 recall 是无条件
304
- # 返回 top_k,为不改变上层"总能拿到几条"的预期,退化到全量语义
305
- # 检索,再在下面用 _visible 补上隔离(search 本身不隔离)。
306
288
  results = list(self._vr.search(query, n=fetch_k) or [])
307
289
  except Exception:
308
290
  return []
@@ -384,8 +366,7 @@ class VecRecallAdapter:
384
366
  row["_ts"] = getattr(m, "timestamp", "") or ""
385
367
  rows.append(row)
386
368
 
387
- rows.sort(key=lambda x: str(x.get("_ts")), reverse=True) # 新近优先
388
- # 同一偏好反复表达会存成多条,去重保留最新,否则会挤满 limit
369
+ rows.sort(key=lambda x: str(x.get("_ts")), reverse=True)
389
370
  deduped, seen_c = [], set()
390
371
  for x in rows:
391
372
  if x["content"] in seen_c:
@@ -401,7 +382,6 @@ class VecRecallAdapter:
401
382
  except Exception:
402
383
  return False
403
384
 
404
- # ── 展示 ──────────────────────────────────────────────
405
385
  def recent(self, limit: int = 20) -> list:
406
386
  """最近的记忆条目(新→旧),供前端右栏"记忆信息条"。"""
407
387
  rows = []
@@ -435,11 +415,6 @@ class VecRecallAdapter:
435
415
  })
436
416
  return rows
437
417
 
438
- # ── HMR 专有接口的占位 ────────────────────────────────
439
- # vecRecall 没有"会话工作区 / 运行时快照"这两个概念。留占位而不是让调用方
440
- # AttributeError:上层有统一的"后端接口"假设,缺方法会炸在一个与被调用能力
441
- # 无关的地方。agents/base.py 取到 workspace 后并不读其字段(active_goal 的
442
- # 写入已移除),所以空壳没有实际成本。
443
418
  def get_workspace(self, agent_id: str):
444
419
  return SimpleNamespace(agent_id=agent_id, active_goal="", status="idle",
445
420
  push_task=lambda _t: None)
@@ -453,14 +428,13 @@ class VecRecallAdapter:
453
428
  def restore_runtime_state(self):
454
429
  return None
455
430
 
456
- # ── 状态 ──────────────────────────────────────────────
457
431
  def get_system_status(self):
458
432
  return {"backend": self.backend, "version": self.version, "wing": self.wing}
459
433
 
460
434
  def stats(self) -> dict:
461
435
  """记忆统计:前端记忆中心展示真实数字。字段与 HMRAdapter.stats 一致。"""
462
436
  total = shared_count = relations = 0
463
- by_agent = {}
437
+ by_agent: dict[str, Any] = {}
464
438
  embedding = type(self._emb).__name__
465
439
  try:
466
440
  with self._lock:
@@ -25,17 +25,15 @@ from dataclasses import dataclass, field
25
25
  from urllib.parse import urlparse
26
26
 
27
27
  from . import failure_policy as _policy
28
+ from typing import Callable
28
29
 
29
30
  VERIFIED = "verified"
30
31
  UNVERIFIED = "unverified"
31
32
  FAILED = "failed"
32
33
 
33
- #: 每条工具结果最多挂几条证据(再多就成了噪声,用户不会看)
34
34
  MAX_EVIDENCE = 8
35
35
 
36
36
  _URL_RE = re.compile(r"https?://[^\s<>\"'))\]}]+")
37
- #: 只保留"真实存在"的路径 —— 正则从文本里认出来的路径九成是误报(URL 片段、示例路径),
38
- #: 只有能在磁盘上 stat 到的才算证据。这不是限制,是"证据"二字的含义。
39
37
  _WIN_PATH_RE = re.compile(r"[A-Za-z]:[\\/][^\s\"'<>|*?,。;]+")
40
38
  _POSIX_PATH_RE = re.compile(r"(?<![\w:])/(?:[\w.\-]+/)+[\w.\-]+")
41
39
 
@@ -44,10 +42,10 @@ _POSIX_PATH_RE = re.compile(r"(?<![\w:])/(?:[\w.\-]+/)+[\w.\-]+")
44
42
  class Evidence:
45
43
  """一条可核对的凭据:一个链接、一个真实存在的文件、或一次工具的原始命中。"""
46
44
 
47
- kind: str # url / file
48
- ref: str # 链接或绝对路径
49
- label: str # 展示名(域名 / 文件名)
50
- source: str # 哪个工具产出的
45
+ kind: str
46
+ ref: str
47
+ label: str
48
+ source: str
51
49
  at: float = field(default_factory=time.time)
52
50
 
53
51
  def to_dict(self) -> dict:
@@ -121,14 +119,10 @@ def extract_evidence(source: str, text: str, limit: int = MAX_EVIDENCE) -> list[
121
119
  return out
122
120
 
123
121
 
124
- #: 工具专属的后置条件检查。返回 (是否通过, 不通过的原因)。
125
- #: 没登记的工具走通用规则(见 verify),不会被当成"已验证"。
126
- _CHECKERS: dict[str, "callable"] = {
127
- # 联网搜索:结论必须能指回来源,否则就是"模型自己写的" —— 标未核验而不是验证通过
122
+ _CHECKERS: dict[str, Callable[..., tuple[bool, str]]] = {
128
123
  "web_search": lambda args, text: (
129
124
  (True, "") if _URL_RE.search(text or "")
130
125
  else (False, "搜索结果没有可核对的来源链接")),
131
- # 个人记忆:召回为空是合法结果(确实没记过),非空即有凭据
132
126
  "recall_memory": lambda args, text: (
133
127
  (True, "") if (text or "").strip().startswith("【召回的相关记忆】")
134
128
  else (False, "未返回记忆条目")),
@@ -157,9 +151,7 @@ def verify(name: str, args: dict | None, text: str,
157
151
 
158
152
  read_only = bool((capability or {}).get("read_only"))
159
153
  if read_only:
160
- # 只读 + 有返回 = 这一步确实发生过(内容是否可信由污点/证据层管)
161
154
  return Verification(VERIFIED, evidence=ev)
162
- # 有副作用但没登记后置条件:不能证明效果已发生 → 未核验(不许声称完成)
163
155
  return Verification(UNVERIFIED, reason="该工具未声明可观察的后置条件", evidence=ev)
164
156
 
165
157