foxclaw 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (344) 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/CHANGELOG.md +57 -0
  6. package/python/CHANGELOG_CN.md +57 -0
  7. package/python/README.md +11 -2
  8. package/python/README_CN.md +32 -20
  9. package/python/bench/bm25_audit.py +1 -14
  10. package/python/bench/bm25_regression.py +0 -1
  11. package/python/bench/bm25_stress.py +0 -4
  12. package/python/bench/cd_audit.py +0 -23
  13. package/python/bench/chaos.py +6 -8
  14. package/python/bench/e2e_real_models.py +0 -1
  15. package/python/bench/llm_retry_test.py +2 -9
  16. package/python/bench/load_test.py +5 -8
  17. package/python/bench/vector_ivf_upgrade.py +1 -7
  18. package/python/docs/RELEASE.md +4 -3
  19. package/python/docs/RUN_CN.md +2 -2
  20. package/python/evals/governance/cases.py +0 -37
  21. package/python/evals/governance/run_evals.py +5 -37
  22. package/python/examples/governance_middleware_demo.py +1 -6
  23. package/python/examples/langgraph_governance_demo.py +7 -11
  24. package/python/foxclaw/__init__.py +34 -152
  25. package/python/foxclaw/agents/base.py +65 -136
  26. package/python/foxclaw/agents/impl/_helpers.py +2 -10
  27. package/python/foxclaw/agents/impl/finance.py +4 -21
  28. package/python/foxclaw/agents/impl/office.py +7 -24
  29. package/python/foxclaw/agents/impl/supervisor.py +12 -79
  30. package/python/foxclaw/agents/mixins/_context.py +0 -23
  31. package/python/foxclaw/agents/mixins/augment.py +10 -16
  32. package/python/foxclaw/agents/mixins/llm.py +7 -6
  33. package/python/foxclaw/agents/mixins/memory.py +7 -1
  34. package/python/foxclaw/agents/mixins/preference.py +58 -8
  35. package/python/foxclaw/agents/mixins/prompt.py +0 -6
  36. package/python/foxclaw/agents/mixins/skillbox.py +6 -6
  37. package/python/foxclaw/agents/mixins/tool_exec.py +16 -42
  38. package/python/foxclaw/api/__init__.py +18 -77
  39. package/python/foxclaw/api/_state.py +43 -0
  40. package/python/foxclaw/api/{app.py → bootstrap.py} +5 -32
  41. package/python/foxclaw/api/middleware.py +5 -28
  42. package/python/foxclaw/api/routes/ahs.py +60 -73
  43. package/python/foxclaw/api/routes/attachments.py +5 -6
  44. package/python/foxclaw/api/routes/browser.py +7 -11
  45. package/python/foxclaw/api/routes/domain.py +18 -26
  46. package/python/foxclaw/api/routes/knowledge.py +3 -21
  47. package/python/foxclaw/api/routes/llm_admin.py +21 -33
  48. package/python/foxclaw/api/routes/memory.py +28 -12
  49. package/python/foxclaw/api/routes/orchestration.py +3 -5
  50. package/python/foxclaw/api/routes/recovery.py +4 -3
  51. package/python/foxclaw/api/routes/review.py +2 -11
  52. package/python/foxclaw/api/routes/search.py +3 -7
  53. package/python/foxclaw/api/routes/system.py +33 -49
  54. package/python/foxclaw/api/routes/task.py +31 -49
  55. package/python/foxclaw/api/routes/ws.py +2 -6
  56. package/python/foxclaw/api/spine.py +1 -4
  57. package/python/foxclaw/browser/action/action_executor.py +1 -5
  58. package/python/foxclaw/browser/action/action_types.py +0 -1
  59. package/python/foxclaw/browser/action/url_policy.py +1 -5
  60. package/python/foxclaw/browser/adapter/web_adapter.py +1 -1
  61. package/python/foxclaw/browser/driver/playwright_driver.py +20 -16
  62. package/python/foxclaw/browser/perception/dom_extractor.py +0 -1
  63. package/python/foxclaw/browser/perception/element_indexer.py +1 -2
  64. package/python/foxclaw/browser/service.py +7 -11
  65. package/python/foxclaw/capability/embedding_service.py +4 -6
  66. package/python/foxclaw/channels/__init__.py +9 -11
  67. package/python/foxclaw/channels/discord.py +4 -13
  68. package/python/foxclaw/channels/dispatcher.py +2 -27
  69. package/python/foxclaw/channels/feishu.py +10 -20
  70. package/python/foxclaw/channels/openclaw_bridge.py +4 -9
  71. package/python/foxclaw/channels/signing.py +9 -9
  72. package/python/foxclaw/channels/telegram.py +3 -4
  73. package/python/foxclaw/channels/weixin.py +7 -17
  74. package/python/foxclaw/cli.py +42 -13
  75. package/python/foxclaw/cognitive/reasoner.py +9 -10
  76. package/python/foxclaw/cognitive/state.py +18 -24
  77. package/python/foxclaw/core/audit_chain.py +1 -3
  78. package/python/foxclaw/core/audit_sink.py +3 -3
  79. package/python/foxclaw/core/compaction.py +8 -26
  80. package/python/foxclaw/core/config.py +2 -2
  81. package/python/foxclaw/core/confirm_nonce.py +6 -12
  82. package/python/foxclaw/core/envfile.py +2 -2
  83. package/python/foxclaw/core/failure_policy.py +17 -26
  84. package/python/foxclaw/core/gateway.py +6 -10
  85. package/python/foxclaw/core/governance_audit.py +3 -7
  86. package/python/foxclaw/core/idempotency.py +15 -22
  87. package/python/foxclaw/core/injection_guard.py +10 -29
  88. package/python/foxclaw/core/memory_adapter.py +30 -57
  89. package/python/foxclaw/core/metrics.py +4 -5
  90. package/python/foxclaw/core/model_router.py +36 -148
  91. package/python/foxclaw/core/paths.py +1 -1
  92. package/python/foxclaw/core/recovery.py +21 -34
  93. package/python/foxclaw/core/scheduler.py +23 -26
  94. package/python/foxclaw/core/secrets.py +0 -3
  95. package/python/foxclaw/core/sessions.py +4 -6
  96. package/python/foxclaw/core/settings.py +16 -32
  97. package/python/foxclaw/core/shared_write.py +11 -23
  98. package/python/foxclaw/core/steering_queue.py +12 -11
  99. package/python/foxclaw/core/system_bus.py +1 -1
  100. package/python/foxclaw/core/thought_chain.py +30 -32
  101. package/python/foxclaw/core/token_meter.py +9 -13
  102. package/python/foxclaw/core/tools.py +4 -12
  103. package/python/foxclaw/core/tracing.py +25 -40
  104. package/python/foxclaw/core/truth_pipeline.py +5 -9
  105. package/python/foxclaw/core/update_check.py +149 -0
  106. package/python/foxclaw/core/vec_recall_adapter.py +11 -37
  107. package/python/foxclaw/core/verify.py +6 -14
  108. package/python/foxclaw/core/web_search.py +4 -28
  109. package/python/foxclaw/finance/evidence.py +14 -18
  110. package/python/foxclaw/finance/fx.py +1 -3
  111. package/python/foxclaw/finance/graph.py +9 -18
  112. package/python/foxclaw/finance/models.py +20 -24
  113. package/python/foxclaw/finance/nlparse.py +12 -72
  114. package/python/foxclaw/finance/policy.py +8 -36
  115. package/python/foxclaw/finance/reasoner.py +3 -11
  116. package/python/foxclaw/finance/reasoning.py +13 -34
  117. package/python/foxclaw/finance/reconcile.py +5 -10
  118. package/python/foxclaw/finance/runtime.py +19 -46
  119. package/python/foxclaw/governance/middleware.py +13 -21
  120. package/python/foxclaw/harness/_base.py +2 -8
  121. package/python/foxclaw/harness/_policy.py +19 -38
  122. package/python/foxclaw/harness/drift.py +1 -13
  123. package/python/foxclaw/harness/event_bus.py +2 -5
  124. package/python/foxclaw/harness/harness.py +9 -97
  125. package/python/foxclaw/harness/loop_detection.py +11 -18
  126. package/python/foxclaw/itinerary/amap_client.py +2 -5
  127. package/python/foxclaw/itinerary/models.py +7 -7
  128. package/python/foxclaw/itinerary/reasoner.py +1 -6
  129. package/python/foxclaw/itinerary/reasoning.py +15 -21
  130. package/python/foxclaw/itinerary/runtime.py +8 -12
  131. package/python/foxclaw/kernel/capabilities.py +5 -6
  132. package/python/foxclaw/kernel/policy_gate.py +4 -10
  133. package/python/foxclaw/kernel/process_manager.py +17 -36
  134. package/python/foxclaw/kernel/scheduler.py +3 -9
  135. package/python/foxclaw/kernel/sentinel.py +13 -23
  136. package/python/foxclaw/kernel/sentinel_client.py +12 -15
  137. package/python/foxclaw/kernel/spine.py +9 -9
  138. package/python/foxclaw/knowledge/__init__.py +4 -20
  139. package/python/foxclaw/knowledge/truth_crystallizer.py +5 -8
  140. package/python/foxclaw/learning/knowledge_graph.py +4 -9
  141. package/python/foxclaw/learning/models.py +13 -13
  142. package/python/foxclaw/learning/reasoner.py +1 -4
  143. package/python/foxclaw/learning/runtime.py +19 -31
  144. package/python/foxclaw/learning/spaced_repetition.py +2 -7
  145. package/python/foxclaw/mcp_server.py +2 -3
  146. package/python/foxclaw/orchestration/__init__.py +16 -33
  147. package/python/foxclaw/pipeline/cognitive_pipeline.py +45 -97
  148. package/python/foxclaw/pipeline/stage_meta.py +21 -0
  149. package/python/foxclaw/pipeline/supervisor_stages.py +13 -42
  150. package/python/foxclaw/planning/llm_planner.py +3 -3
  151. package/python/foxclaw/planning/planner.py +6 -11
  152. package/python/foxclaw/search/agent_port.py +7 -11
  153. package/python/foxclaw/search/evidence.py +0 -4
  154. package/python/foxclaw/search/internal_search.py +3 -5
  155. package/python/foxclaw/search/models.py +11 -13
  156. package/python/foxclaw/search/planner.py +3 -3
  157. package/python/foxclaw/search/retriever.py +1 -1
  158. package/python/foxclaw/search/runtime.py +1 -8
  159. package/python/foxclaw/skills/__init__.py +46 -100
  160. package/python/foxclaw/skills/archive.py +111 -0
  161. package/python/foxclaw/skills/fetch.py +226 -0
  162. package/python/foxclaw/skills/hooks_spec.py +21 -25
  163. package/python/foxclaw/skills/install_cmd.py +216 -0
  164. package/python/foxclaw/skills/lifecycle.py +16 -42
  165. package/python/foxclaw/skills/markdown_skill.py +10 -9
  166. package/python/foxclaw/skills/mcp_client.py +5 -11
  167. package/python/foxclaw/skills/mcp_store.py +0 -3
  168. package/python/foxclaw/skills/permission.py +13 -48
  169. package/python/foxclaw/skills/pipeline.py +21 -53
  170. package/python/foxclaw/skills/policy.py +4 -10
  171. package/python/foxclaw/skills/review.py +3 -3
  172. package/python/frontend/deck/auth.js +0 -7
  173. package/python/frontend/deck/chat_model_picker.js +0 -54
  174. package/python/frontend/deck/domain.js +0 -20
  175. package/python/frontend/deck/main.js +0 -144
  176. package/python/frontend/deck/markdown.js +0 -4
  177. package/python/frontend/deck/settings.js +1 -18
  178. package/python/frontend/deck/state.js +0 -1
  179. package/python/frontend/deck/util.js +0 -2
  180. package/python/frontend/deck/visuals.js +0 -22
  181. package/python/frontend/planet/planet.js +0 -201
  182. package/python/frontend/v3/assets/index-CQSBdxk4.js +34 -0
  183. package/python/frontend/v3/assets/index-D5nIsjms.css +1 -0
  184. package/python/frontend/v3/index.html +3 -3
  185. package/python/frontend/widget_runtime.js +0 -43
  186. package/python/frontend/widgets.js +0 -57
  187. package/python/frontend_src/index.html +1 -1
  188. package/python/frontend_src/package-lock.json +2 -2
  189. package/python/frontend_src/package.json +1 -1
  190. package/python/frontend_src/src/App.vue +3 -7
  191. package/python/frontend_src/src/api.js +0 -7
  192. package/python/frontend_src/src/components/ChatDock.vue +27 -39
  193. package/python/frontend_src/src/components/DocViewer.vue +0 -9
  194. package/python/frontend_src/src/components/Inspector.vue +0 -6
  195. package/python/frontend_src/src/components/LeftRail.vue +60 -39
  196. package/python/frontend_src/src/components/MenuPopover.vue +9 -11
  197. package/python/frontend_src/src/components/MessageStream.vue +18 -30
  198. package/python/frontend_src/src/components/ModelPicker.vue +0 -5
  199. package/python/frontend_src/src/components/QuickIngest.vue +0 -6
  200. package/python/frontend_src/src/components/Stage.vue +71 -25
  201. package/python/frontend_src/src/components/ToolMenu.vue +0 -3
  202. package/python/frontend_src/src/components/TopBar.vue +38 -10
  203. package/python/frontend_src/src/components/panels/ActivityPanel.vue +0 -2
  204. package/python/frontend_src/src/components/panels/AgentsPanel.vue +2 -5
  205. package/python/frontend_src/src/components/panels/KnowledgePanel.vue +0 -16
  206. package/python/frontend_src/src/components/panels/MemoryPanel.vue +0 -1
  207. package/python/frontend_src/src/components/panels/PanelBox.vue +6 -7
  208. package/python/frontend_src/src/components/panels/ReviewPanel.vue +0 -16
  209. package/python/frontend_src/src/components/panels/SettingsPanel.vue +142 -38
  210. package/python/frontend_src/src/components/panels/TasksPanel.vue +0 -1
  211. package/python/frontend_src/src/components/panels/ToolsPanel.vue +125 -19
  212. package/python/frontend_src/src/lib/fmt.js +0 -4
  213. package/python/frontend_src/src/lib/i18n.js +53 -0
  214. package/python/frontend_src/src/lib/locales/en.js +173 -0
  215. package/python/frontend_src/src/lib/locales/zh.js +174 -0
  216. package/python/frontend_src/src/lib/md.js +0 -14
  217. package/python/frontend_src/src/main.js +2 -0
  218. package/python/frontend_src/src/stores/app.js +17 -32
  219. package/python/frontend_src/src/stores/chat.js +0 -34
  220. package/python/frontend_src/src/stores/model.js +0 -8
  221. package/python/frontend_src/src/stores/panels.js +0 -41
  222. package/python/frontend_src/src/stores/profile.js +31 -0
  223. package/python/frontend_src/src/stores/system.js +25 -0
  224. package/python/frontend_src/vite.config.js +0 -7
  225. package/python/pyproject.toml +6 -3
  226. package/python/requirements.txt +311 -0
  227. package/python/tests/conftest.py +0 -22
  228. package/python/tests/run_all.py +3 -66
  229. package/python/tests/test_agent_lifecycle.py +4 -38
  230. package/python/tests/test_ahs_p2.py +4 -25
  231. package/python/tests/test_anthropic_tools.py +1 -15
  232. package/python/tests/test_api.py +19 -9
  233. package/python/tests/test_api_auth.py +1 -5
  234. package/python/tests/test_attachments.py +9 -25
  235. package/python/tests/test_audit_chain.py +2 -7
  236. package/python/tests/test_audit_sink.py +0 -6
  237. package/python/tests/test_browser.py +2 -6
  238. package/python/tests/test_browser_request_guard.py +0 -4
  239. package/python/tests/test_capabilities.py +0 -6
  240. package/python/tests/test_channel_signing.py +2 -9
  241. package/python/tests/test_channels.py +1 -11
  242. package/python/tests/test_config_layering.py +0 -4
  243. package/python/tests/test_confirm_nonce.py +1 -12
  244. package/python/tests/test_context_compaction_wiring.py +1 -7
  245. package/python/tests/test_dispatch.py +0 -6
  246. package/python/tests/test_domain_evals.py +11 -80
  247. package/python/tests/test_domain_import.py +0 -6
  248. package/python/tests/test_domain_reasoners.py +0 -14
  249. package/python/tests/test_e2e.py +0 -17
  250. package/python/tests/test_emb_cache.py +7 -17
  251. package/python/tests/test_embedding_singleton.py +1 -3
  252. package/python/tests/test_evals_smoke.py +0 -24
  253. package/python/tests/test_event_spine.py +10 -18
  254. package/python/tests/test_evidence_semantic.py +1 -14
  255. package/python/tests/test_evidence_sse.py +3 -6
  256. package/python/tests/test_file_ingest.py +0 -11
  257. package/python/tests/test_finance.py +0 -9
  258. package/python/tests/test_finance_ledger.py +5 -12
  259. package/python/tests/test_finance_persist.py +8 -12
  260. package/python/tests/test_gateway.py +3 -22
  261. package/python/tests/test_governance.py +5 -52
  262. package/python/tests/test_governance_audit.py +4 -39
  263. package/python/tests/test_governance_middleware.py +1 -7
  264. package/python/tests/test_hkc.py +6 -24
  265. package/python/tests/test_hkc_index_build_manager.py +0 -33
  266. package/python/tests/test_hkc_manifest_reuse.py +4 -27
  267. package/python/tests/test_hkc_search_event_sync.py +3 -54
  268. package/python/tests/test_import_path_guard.py +0 -6
  269. package/python/tests/test_injection_guard.py +18 -36
  270. package/python/tests/test_integration.py +5 -29
  271. package/python/tests/test_itinerary.py +0 -3
  272. package/python/tests/test_kee_semantic.py +0 -5
  273. package/python/tests/test_kernel.py +1 -11
  274. package/python/tests/test_learning.py +3 -12
  275. package/python/tests/test_llm_admin.py +4 -19
  276. package/python/tests/test_llm_planner.py +1 -23
  277. package/python/tests/test_markdown_skills.py +3 -14
  278. package/python/tests/test_markdown_skills_openclaw.py +4 -9
  279. package/python/tests/test_mcp_connectors.py +3 -15
  280. package/python/tests/test_mcp_guardian_discovery.py +2 -6
  281. package/python/tests/test_mcp_no_memory.py +3 -3
  282. package/python/tests/test_mcp_policy_gate.py +0 -1
  283. package/python/tests/test_mcp_server.py +6 -9
  284. package/python/tests/test_memory_recall.py +1 -4
  285. package/python/tests/test_metrics.py +0 -8
  286. package/python/tests/test_multi_chain.py +1 -13
  287. package/python/tests/test_multimodal.py +5 -18
  288. package/python/tests/test_negotiation.py +3 -13
  289. package/python/tests/test_orchestration.py +1 -11
  290. package/python/tests/test_orchestration_decoupled.py +0 -6
  291. package/python/tests/test_p3_p5_hardening.py +3 -17
  292. package/python/tests/test_parallel_dispatch.py +4 -12
  293. package/python/tests/test_pipeline.py +17 -58
  294. package/python/tests/test_plan_visible.py +4 -4
  295. package/python/tests/test_planner.py +0 -7
  296. package/python/tests/test_policy_gate.py +2 -3
  297. package/python/tests/test_profile.py +67 -0
  298. package/python/tests/test_readme_claims.py +163 -0
  299. package/python/tests/test_reasoner.py +2 -17
  300. package/python/tests/test_release_check.py +2 -13
  301. package/python/tests/test_resume_checkpoint.py +7 -15
  302. package/python/tests/test_routing.py +2 -12
  303. package/python/tests/test_scheduler.py +7 -10
  304. package/python/tests/test_search.py +1 -11
  305. package/python/tests/test_secrets.py +4 -10
  306. package/python/tests/test_semantic.py +1 -10
  307. package/python/tests/test_sessions.py +3 -4
  308. package/python/tests/test_settings.py +2 -18
  309. package/python/tests/test_shadow_learning.py +3 -43
  310. package/python/tests/test_shared_write.py +0 -7
  311. package/python/tests/test_skill_taint_gate.py +1 -10
  312. package/python/tests/test_skills.py +2 -18
  313. package/python/tests/test_skills_fetch.py +282 -0
  314. package/python/tests/test_stats_and_events.py +2 -5
  315. package/python/tests/test_streaming.py +2 -7
  316. package/python/tests/test_supervisor_pipeline.py +4 -5
  317. package/python/tests/test_supervisor_triage.py +4 -10
  318. package/python/tests/test_taint.py +2 -7
  319. package/python/tests/test_task_stream.py +1 -12
  320. package/python/tests/test_thinking_sse.py +0 -1
  321. package/python/tests/test_tool_center.py +1 -17
  322. package/python/tests/test_tool_parallel.py +2 -2
  323. package/python/tests/test_tool_params.py +1 -5
  324. package/python/tests/test_tool_use.py +0 -13
  325. package/python/tests/test_toplevel_concurrency.py +3 -4
  326. package/python/tests/test_tracing.py +7 -41
  327. package/python/tests/test_truth_crystallizer.py +4 -6
  328. package/python/tests/test_truth_flow.py +2 -7
  329. package/python/tests/test_update_check.py +95 -0
  330. package/python/tests/test_verify_evidence.py +8 -21
  331. package/python/tests/test_version_sync.py +35 -6
  332. package/python/tests/test_web_trigger.py +0 -17
  333. package/python/tools/check_faiss_type.py +0 -1
  334. package/python/tools/check_log.ps1 +0 -8
  335. package/python/tools/mypy_baseline.txt +12 -4
  336. package/python/tools/mypy_ratchet.py +52 -6
  337. package/python/tools/pack.py +0 -5
  338. package/python/tools/release_check.py +7 -7
  339. package/python/tools/restart_foxclaw.ps1 +5 -8
  340. package/python/tools/shared_write_audit.py +1 -15
  341. package/python/tools/size_check.py +1 -1
  342. package/python/tools/strip_comments.py +228 -0
  343. package/python/frontend/v3/assets/index-BD7VPbMV.js +0 -33
  344. package/python/frontend/v3/assets/index-CAvnYWGH.css +0 -1
@@ -11,12 +11,13 @@ from dataclasses import fields
11
11
  from datetime import datetime
12
12
  from pathlib import Path
13
13
 
14
- from .models import Transaction, Budget, FinancialEntity
14
+ from .models import Budget, EntityType, FinancialEntity, Transaction
15
15
  from .reasoning import FinanceReasoning
16
16
  from .policy import FinancePolicyEngine
17
17
  from .graph import FinancialDataGraph
18
18
  from .fx import FxTable
19
19
  from .nlparse import CATEGORY_LABELS, parse as parse_expense_text
20
+ from typing import Any
20
21
 
21
22
 
22
23
  def _period_of(txn, default: str = "") -> str:
@@ -46,27 +47,22 @@ class FinanceRuntime:
46
47
  """财务 Agent 的资金行为操作系统内核。"""
47
48
 
48
49
  def __init__(self, policy_path: str = "./finance_policy.yaml", hmr=None,
49
- data_path: str = None):
50
+ data_path: str | None = None):
50
51
  self.transactions: list = []
51
52
  self.budgets: list = []
52
- self.entities: dict = {} # name -> FinancialEntity
53
- self.account_balances: dict = {} # account -> balance
53
+ self.entities: dict = {}
54
+ self.account_balances: dict = {}
54
55
  self.policy = FinancePolicyEngine(policy_path)
55
56
  self.reasoning = FinanceReasoning()
56
- self.hmr = hmr # 可选:把交易写入 HMR 共享认知
57
- # 交易/预算/余额/实体的落盘点:默认与 policy 同目录(调用方传的就是 data_dir)。
58
- # 此前这些只在内存,进程一重启就清零 → 财务分析永远是"0 笔交易"。
57
+ self.hmr = hmr
59
58
  self._data_path = Path(data_path) if data_path else (
60
59
  Path(policy_path).resolve().parent / "finance_data.json")
61
- # 写侧串行:runtime 是单例,请求线程会并发 add/import,analyze 还会触发阈值自校准写盘。
62
60
  self._lock = threading.RLock()
63
61
  self.load_error: str = ""
64
62
  self.save_error: str = ""
65
- # 多币种:账本按本位币(CNY)算账,汇率表与账本同目录
66
63
  self.fx = FxTable(str(self._data_path.parent / "finance_fx.json"))
67
64
  self.load()
68
65
 
69
- # ── 持久化(原子写 tmp→replace;损坏文件只降级、不阻断启动)──────
70
66
  def load(self) -> int:
71
67
  """从落盘点恢复交易/预算/余额/实体,返回恢复的交易条数。
72
68
 
@@ -106,7 +102,7 @@ class FinanceRuntime:
106
102
  except (TypeError, ValueError):
107
103
  continue
108
104
  if not self.entities and self.transactions:
109
- self._rebuild_entities() # 实体段缺失(旧格式/被裁)→ 按交易重算
105
+ self._rebuild_entities()
110
106
  return len(self.transactions)
111
107
 
112
108
  def save(self) -> bool:
@@ -127,11 +123,9 @@ class FinanceRuntime:
127
123
  encoding="utf-8")
128
124
  tmp.replace(self._data_path)
129
125
  self.save_error = ""
130
- # 文件已被本进程的完整快照覆盖 → 上一轮"读不进来"的原因作废(自愈)
131
126
  self.load_error = ""
132
127
  return True
133
128
  except Exception as e:
134
- # 落盘失败不能假装成功:留在内存里 + 暴露错误,让调用方/界面能看到
135
129
  self.save_error = f"{type(e).__name__}: {e}"
136
130
  return False
137
131
 
@@ -140,7 +134,7 @@ class FinanceRuntime:
140
134
  for txn in self.transactions:
141
135
  ent = self.entities.get(txn.entity)
142
136
  if not ent:
143
- etype = "subscription" if txn.is_recurring else "merchant"
137
+ etype: EntityType = "subscription" if txn.is_recurring else "merchant"
144
138
  ent = FinancialEntity(name=txn.entity, entity_type=etype)
145
139
  self.entities[txn.entity] = ent
146
140
  ent.txn_count += 1
@@ -149,17 +143,15 @@ class FinanceRuntime:
149
143
  if txn.is_recurring:
150
144
  ent.recurring_amount = abs(txn.amount)
151
145
 
152
- # ── 数据录入 ──────────────────────────────────────────
153
146
  def add_transaction(self, txn: Transaction,
154
147
  run_gatekeeper: bool = True,
155
148
  save: bool = True) -> dict:
156
149
  """录入一笔交易,经过守门人裁决,并落盘(save=False 供批量导入只写一次)。"""
157
150
  with self._lock:
158
151
  self.transactions.append(txn)
159
- # 更新实体统计
160
152
  ent = self.entities.get(txn.entity)
161
153
  if not ent:
162
- etype = "subscription" if txn.is_recurring else "merchant"
154
+ etype: EntityType = "subscription" if txn.is_recurring else "merchant"
163
155
  ent = FinancialEntity(name=txn.entity, entity_type=etype)
164
156
  self.entities[txn.entity] = ent
165
157
  ent.txn_count += 1
@@ -168,7 +160,6 @@ class FinanceRuntime:
168
160
  if txn.is_recurring:
169
161
  ent.recurring_amount = abs(txn.amount)
170
162
 
171
- # 写入 HMR(可选)
172
163
  if self.hmr:
173
164
  try:
174
165
  self.hmr.ingest(
@@ -179,7 +170,6 @@ class FinanceRuntime:
179
170
  except Exception:
180
171
  pass
181
172
 
182
- # 守门人裁决
183
173
  verdicts = []
184
174
  if run_gatekeeper:
185
175
  bal = self.account_balances.get(txn.account)
@@ -190,7 +180,6 @@ class FinanceRuntime:
190
180
  "gated": any(v.action in ("veto", "freeze", "require_confirmation")
191
181
  for v in verdicts),
192
182
  }
193
- # 写盘放在锁外:避免一次磁盘 IO 把并发进来的读请求全堵住
194
183
  if save:
195
184
  self.save()
196
185
  return result
@@ -206,9 +195,8 @@ class FinanceRuntime:
206
195
  self.account_balances[account] = balance
207
196
  self.save()
208
197
 
209
- # ── 自然语言记账(第一线功能)────────────────────────────
210
198
  def record_text(self, text: str, account: str = "default",
211
- now: datetime = None, dry_run: bool = False) -> dict:
199
+ now: datetime | None = None, dry_run: bool = False) -> dict:
212
200
  """「今天买了一件衣服花了 105 块」→ 落一笔账,返回可读回执。
213
201
 
214
202
  识别(金额/币种/时间/分类)全部走 `nlparse` 的确定性规则,LLM 不参与算钱。
@@ -279,7 +267,6 @@ class FinanceRuntime:
279
267
  lines.append(f"· {day} {it.get('description') or it.get('entity') or ''}"
280
268
  f" {money} · {label}{flag}")
281
269
 
282
- # 汇总期跟着这笔账走:记的是上月 5 号的账,就别报"本月已花"
283
270
  period = (items[0].get("timestamp") or "")[:7] or None
284
271
  led = self.ledger(period)
285
272
  head = "本月" if period == datetime.now().strftime("%Y-%m") else f"{period} "
@@ -298,7 +285,7 @@ class FinanceRuntime:
298
285
  lines.append("提示:" + ";".join(warns))
299
286
  return "\n".join(lines)
300
287
 
301
- def ledger(self, period: str = None) -> dict:
288
+ def ledger(self, period: str | None = None) -> dict:
302
289
  """记账本:按月汇总(已发生 vs 计划、按天、按分类、按币种)。"""
303
290
  period = period or datetime.now().strftime("%Y-%m")
304
291
  with self._lock:
@@ -311,7 +298,9 @@ class FinanceRuntime:
311
298
  expense = round(sum(t.amount for t in settled if t.amount < 0), 2)
312
299
  income = round(sum(t.amount for t in settled if t.amount > 0), 2)
313
300
 
314
- by_day, by_cat, by_cur = {}, {}, {}
301
+ by_day: dict[str, dict[str, Any]] = {}
302
+ by_cat: dict[str, dict[str, Any]] = {}
303
+ by_cur: dict[str, dict[str, Any]] = {}
315
304
  for t in settled:
316
305
  day = (t.timestamp or "")[:10]
317
306
  d = by_day.setdefault(day, {"date": day, "expense": 0.0, "income": 0.0,
@@ -376,14 +365,12 @@ class FinanceRuntime:
376
365
  def import_csv(self, path: str) -> int:
377
366
  """从 CSV 导入交易。列:amount,category,entity,account,timestamp,tags
378
367
  真实银行 API 接口可在此处替换/扩展。"""
379
- path = Path(path)
368
+ csv_path = Path(path)
380
369
  count = 0
381
- with open(path, encoding="utf-8") as f:
370
+ with open(csv_path, encoding="utf-8") as f:
382
371
  reader = csv.DictReader(f)
383
372
  for row in reader:
384
373
  tags = [t.strip() for t in (row.get("tags") or "").split("|") if t.strip()]
385
- # 可选的 currency 列:外币行按当天记账汇率折成本位币再入账,
386
- # 否则「100 美元」会和「100 元」一起被求和。
387
374
  cur = (row.get("currency") or row.get("币种") or "CNY").strip().upper()
388
375
  raw = float(row["amount"])
389
376
  base, rate = self.fx.to_base(abs(raw), cur)
@@ -401,11 +388,10 @@ class FinanceRuntime:
401
388
  self.add_transaction(txn, run_gatekeeper=False, save=False)
402
389
  count += 1
403
390
  if count:
404
- self.save() # 批量导入只落盘一次,不按每笔交易各做一次磁盘 IO
391
+ self.save()
405
392
  return count
406
393
 
407
- # ── 综合分析(财务 Agent 的核心输出)──────────────────
408
- def analyze(self, period: str = None, current_balance: float = None,
394
+ def analyze(self, period: str | None = None, current_balance: float | None = None,
409
395
  calibrate: bool = True) -> dict:
410
396
  """一次性产出完整财务洞察:预算/风险/预测/守门人裁决。
411
397
 
@@ -413,17 +399,12 @@ class FinanceRuntime:
413
399
  再裁决 —— 让阈值随消费水平自己长,而不是永远停在出厂拍的经验值上。
414
400
  """
415
401
  period = period or datetime.now().strftime("%Y-%m")
416
- # 快照:分析期间可能有并发录入(持久化后 import 端点就是可并发的写方),
417
- # 直接遍历 self.transactions 会撞上 "list changed size during iteration"。
418
- # 计划支出(明天/下个月)必须排除:还没发生的钱不能算进"本月已花",
419
- # 否则「明天交房租 3000」会立刻把这个月的预算打爆。
420
402
  txns = [t for t in self.transactions if not getattr(t, "is_planned", False)]
421
403
  budgets = list(self.budgets)
422
404
  bal = current_balance
423
405
  if bal is None and self.account_balances:
424
406
  bal = sum(dict(self.account_balances).values())
425
407
 
426
- # 阈值自校准:必须在裁决**之前**,否则这次分析用的还是上一轮的旧阈值
427
408
  calibration = (self.policy.apply_calibration(
428
409
  self.reasoning.calibrate_thresholds(txns)) if calibrate else [])
429
410
 
@@ -435,21 +416,16 @@ class FinanceRuntime:
435
416
  cashflow = (self.reasoning.cashflow_projection(txns, bal)
436
417
  if bal is not None else None)
437
418
 
438
- # 超支预测(对每个有预算的分类)—— 提到裁决之前算,好一并交给守门人
439
419
  forecasts = [self.reasoning.overspend_forecast(txns, b, period)
440
420
  for b in budgets]
441
421
  renewals = self.reasoning.detect_upcoming_renewals(txns)
442
422
  pending_refunds = self.reasoning.detect_pending_refunds(txns)
443
423
 
444
- # 守门人对整体状态裁决
445
- # duplicates/forecasts 此前已被算出来、却只进报告不进裁决(检测到了却不吭声);
446
- # renewals/pending_refunds 为新增信号。现在一并交给守门人。
447
424
  state_verdicts = self.policy.evaluate_state(
448
425
  subscription_total=sub_creep["monthly_total"], anomalies=anomalies,
449
426
  duplicates=duplicates, forecasts=forecasts,
450
427
  renewals=renewals, pending_refunds=pending_refunds)
451
428
 
452
- # 结构化证据链(可解释决策,可沉淀到 ThoughtChain)
453
429
  from .evidence import FinanceExplainer
454
430
  evidence_chains = [
455
431
  FinanceExplainer.explain_budget_risk(budget_status).to_dict(),
@@ -467,11 +443,10 @@ class FinanceRuntime:
467
443
  "burn_rate": burn,
468
444
  "cashflow_projection": cashflow,
469
445
  "gatekeeper_verdicts": [v.to_dict() for v in state_verdicts],
470
- "threshold_calibration": calibration, # 阈值自己变了要让用户看见,不能偷偷改
446
+ "threshold_calibration": calibration,
471
447
  "evidence_chains": evidence_chains,
472
448
  }
473
449
 
474
- # ── 守门人审批流(不自动动钱)─────────────────────────
475
450
  def request_action(self, action_type: str, detail: dict) -> dict:
476
451
  """请求一个资金操作(如取消订阅、转账)。
477
452
  不自动执行——返回审批请求,标记需用户确认 / 需接真实支付 API。"""
@@ -489,7 +464,6 @@ class FinanceRuntime:
489
464
  "entities": len(self.entities),
490
465
  "accounts": self.account_balances,
491
466
  "policy": self.policy.summary(),
492
- # 落盘状态一并回给界面:否则"数据到底存哪了/为什么重启就没了"只能靠猜
493
467
  "persist": {"path": str(self._data_path),
494
468
  "exists": self._data_path.exists(),
495
469
  "load_error": self.load_error,
@@ -513,7 +487,6 @@ class FinanceRuntime:
513
487
  返回:匹配/差异分类 + 审计日志。
514
488
  """
515
489
  from .reconcile import Reconciler, ExternalRecord
516
- # 容忍 dict 输入,自动转 ExternalRecord
517
490
  ext = []
518
491
  for r in external_records:
519
492
  if isinstance(r, ExternalRecord):
@@ -13,25 +13,25 @@ import functools
13
13
  import tempfile
14
14
  import uuid
15
15
  from dataclasses import dataclass, field
16
- from typing import Optional
16
+ from typing import Optional, cast
17
17
 
18
18
  from ..harness.harness import Harness
19
19
  from ..harness.loop_detection import ToolLoopDetector
20
- from ..core.thought_chain import ThoughtChain, ThoughtNode
20
+ from ..core.thought_chain import NodeType, ThoughtChain, ThoughtNode
21
21
 
22
22
 
23
23
  @dataclass
24
24
  class GovResult:
25
25
  """一步治理判定的结果(框架无关,不泄露 Foxclaw 内部类型)。"""
26
- status: str # PASS / WARN / BLOCK
26
+ status: str
27
27
  blocked: bool
28
28
  warned: bool
29
29
  delta: float
30
- dimension: str # 主导漂移维度 semantic/tool/scope
30
+ dimension: str
31
31
  reason: Optional[str]
32
32
  node_id: str
33
- scores: dict = field(default_factory=dict) # 各维度分 semantic/tool/scope
34
- loop: Optional[dict] = None # 若检出工具死循环,填充告警
33
+ scores: dict = field(default_factory=dict)
34
+ loop: Optional[dict] = None
35
35
 
36
36
  @property
37
37
  def tool_violation(self) -> bool:
@@ -74,8 +74,8 @@ class GovernanceSession:
74
74
 
75
75
  def __init__(self, goal: str, allowed_tools=None, entities=None,
76
76
  agent_id: str = "agent", shadow_rounds: int = 0,
77
- db_path: str = None, policy_path: str = None,
78
- embedding_model: str = None):
77
+ db_path: str | None = None, policy_path: str | None = None,
78
+ embedding_model: str | None = None):
79
79
  self.agent_id = agent_id
80
80
  self._session_id = f"gov_{uuid.uuid4().hex[:10]}"
81
81
  self.thought_chain = ThoughtChain(
@@ -88,17 +88,13 @@ class GovernanceSession:
88
88
  goal_text=goal,
89
89
  allowed_tool_categories=set(allowed_tools or {"read"}),
90
90
  baseline_entities=set(entities or []))
91
- # shadow_rounds<=0:中间件默认**即时按默认阈值判定**(warn 0.15 / block 0.25),
92
- # 不做"拿第一个样本自校准"(那会让首步逃逸并用单样本污染阈值)。想用真实分布
93
- # 校准的调用方,显式传 shadow_rounds>0 走观测期。
94
91
  if shadow_rounds <= 0:
95
92
  self.harness._shadow_mode = False
96
93
  self._loop = ToolLoopDetector()
97
94
  self._blocked_count = 0
98
95
 
99
- # ── 核心:观测一步 ────────────────────────────────────────
100
- def observe(self, content: str, tool: str = None, entities=None,
101
- node_type: str = None, conf: float = 0.9,
96
+ def observe(self, content: str, tool: str | None = None, entities=None,
97
+ node_type: str | None = None, conf: float = 0.9,
102
98
  semantic: bool = True) -> GovResult:
103
99
  """观测一个推理/工具步骤,返回治理判定。不抛异常(判定在结果里)。
104
100
 
@@ -106,7 +102,7 @@ class GovernanceSession:
106
102
  工具调用串对目标的语义距离是噪声。"""
107
103
  node = ThoughtNode.create(
108
104
  session_id=self._session_id, agent_id=self.agent_id,
109
- type=node_type or ("tool_intent" if tool else "reasoning"),
105
+ type=cast(NodeType, node_type or ("tool_intent" if tool else "reasoning")),
110
106
  content=content, conf=conf, tool_name=tool, entities=entities or [])
111
107
  self.thought_chain.add_node(node)
112
108
  r = self.harness.check(node, ignore_semantic=not semantic)
@@ -121,14 +117,13 @@ class GovernanceSession:
121
117
  delta=r.delta, dimension=r.dominant_dim, reason=r.reason,
122
118
  node_id=node.id, scores=dict(r.scores), loop=loop_warn)
123
119
 
124
- def guard(self, content: str, tool: str = None, entities=None) -> GovResult:
120
+ def guard(self, content: str, tool: str | None = None, entities=None) -> GovResult:
125
121
  """与 observe 相同,但被 BLOCK 时**抛 GovernanceBlocked**(fail-closed 风格)。"""
126
122
  r = self.observe(content, tool=tool, entities=entities)
127
123
  if r.blocked:
128
124
  raise GovernanceBlocked(r)
129
125
  return r
130
126
 
131
- # ── 审计 ────────────────────────────────────────────────
132
127
  def audit(self) -> list[dict]:
133
128
  """取回本会话的完整可审计推理链(带每步 Harness 判定)。"""
134
129
  return [n.to_dict() for n in
@@ -148,7 +143,7 @@ class GovernanceSession:
148
143
  return False
149
144
 
150
145
 
151
- def governed(session: GovernanceSession, tool_name: str = None, entities=None):
146
+ def governed(session: GovernanceSession, tool_name: str | None = None, entities=None):
152
147
  """装饰器:把一个工具函数包成"调用前先过 Harness"。
153
148
 
154
149
  命中越权/跑题/范围蔓延 → 抛 GovernanceBlocked,函数不执行。同步/异步函数都支持。
@@ -158,9 +153,6 @@ def governed(session: GovernanceSession, tool_name: str = None, entities=None):
158
153
  def delete_row(...): ... # 调用即被拦(delete 不在 read 允许集)
159
154
  """
160
155
  def _check(name):
161
- # 工具装饰器只对"这个工具授权了吗"负责 → semantic=False 只按 tool/scope 判定
162
- # (语义是 observe() 的职责;工具名对目标的语义距离是噪声,跨语言时更会饱和,
163
- # 不能作为工具授权依据)。这样中间件语言无关、行为确定、审计不被语义噪声污染。
164
156
  r = session.observe(f"tool call: {name}", tool=name, entities=entities,
165
157
  semantic=False)
166
158
  if r.blocked or r.tool_violation or r.scope_violation:
@@ -8,11 +8,6 @@ import numpy as np
8
8
  from ..core.thought_chain import HarnessStatus
9
9
 
10
10
 
11
- # 目标基线是**每任务**语义(register_goal 每次任务设一次),却存在 Harness 实例上——
12
- # 并发顶层任务打同一 Agent 的 harness 时会互相覆盖,导致 check 对着错误的目标算漂移。
13
- # 与 base.py 同构:放进单个 module 级 contextvar 承载的 dict、按实例 token 分槽。
14
- # 累积学习态(weights/_policy/_delta_history/_run_count/_shadow_mode)仍留在实例上——
15
- # 那本就该按 Agent 累积,且同步 check 内无 await,协作调度下读改写原子安全。
16
11
  _harness_ctx: contextvars.ContextVar = contextvars.ContextVar(
17
12
  "foxclaw_harness_ctx", default=None)
18
13
 
@@ -27,7 +22,6 @@ def _harness_ctx_set(token: str, baseline):
27
22
  _harness_ctx.set(d)
28
23
 
29
24
 
30
- # ── 工具类别白名单 ─────────────────────────────────────────
31
25
  TOOL_CATEGORIES: dict[str, set[str]] = {
32
26
  "read": {"read_file", "search", "fetch", "list", "get", "query"},
33
27
  "write": {"write_file", "create", "update", "save", "append"},
@@ -43,8 +37,8 @@ TOOL_CATEGORIES: dict[str, set[str]] = {
43
37
  class HarnessResult:
44
38
  status: HarnessStatus
45
39
  delta: float
46
- dominant_dim: str # 主导漂移维度
47
- scores: dict # 各维度分数
40
+ dominant_dim: str
41
+ scores: dict
48
42
  reason: Optional[str] = None
49
43
 
50
44
  @property
@@ -4,17 +4,30 @@
4
4
  等实例状态协同工作。纯搬移、零行为变更。
5
5
  """
6
6
  import os
7
+ from datetime import datetime, timezone
8
+ from pathlib import Path
9
+ from typing import Any, TYPE_CHECKING
7
10
 
8
11
  import numpy as np
9
12
  import yaml
10
- from datetime import datetime, timezone
11
13
 
12
14
  from ..core.thought_chain import ThoughtNode
13
15
  from ._base import HarnessResult
14
16
 
17
+ if TYPE_CHECKING:
18
+ from .event_bus import HarnessEventBus
19
+
15
20
 
16
21
  class PolicyMixin:
17
- # ── 学习规则的 action 编译 / 应用 ─────────────────────────
22
+ agent_id: str
23
+ bus: "HarnessEventBus"
24
+ policy_path: Path
25
+ weights: dict[str, float]
26
+ promote_min_obs: int
27
+ _policy: dict
28
+ _baseline: Any
29
+ _delta_history: list[float]
30
+
18
31
 
19
32
  def _reindex_learned_actions(self):
20
33
  """把 self._policy['rules'] 里已学习的 action 编译成 check() 用得上的结构。
@@ -35,7 +48,6 @@ class PolicyMixin:
35
48
  elif atype == "restrict_scope":
36
49
  lim = action.get("entity_expansion_limit")
37
50
  if isinstance(lim, int):
38
- # 多条 restrict 取最严(最小)上限
39
51
  scope_limit = lim if scope_limit is None else min(scope_limit, lim)
40
52
  self._penalized_tools = penalized
41
53
  self._scope_limit = scope_limit
@@ -46,11 +58,7 @@ class PolicyMixin:
46
58
  return False
47
59
  return any(tool_name == p or p in tool_name for p in self._penalized_tools)
48
60
 
49
- # ── Policy 学习(P0-1:影子验证 + 回滚 + 边界)────────────────
50
61
 
51
- # 【P0-1 边界】可生成影子规则的维度。semantic 可学但**仅权重级**(不生成规则——
52
- # 原 increase_semantic_threshold 规则本就无规则级应用,判定行为不变);
53
- # 不在 weights 里的未知维度 → 只记录不学习(fail-closed,防未来新维度被默认自学)。
54
62
  _LEARNABLE_RULE_DIMS = frozenset({"tool", "scope"})
55
63
 
56
64
  def _trigger_policy_learning(self, node: ThoughtNode, result: HarnessResult):
@@ -64,16 +72,14 @@ class PolicyMixin:
64
72
  """
65
73
  dim = result.dominant_dim
66
74
 
67
- # ── 边界:未知维度 fail-closed,只记录不学习(权重也不动)──
68
75
  if dim not in self.weights:
69
76
  self._record_history("skip", "",
70
77
  note=f"未知维度 '{dim}' 禁止自学习(fail-closed)")
71
78
  self._save_policy()
72
79
  return
73
80
 
74
- # ── 规则级:tool/scope 生成影子候选;semantic 仅权重级 ──
75
81
  if dim in self._LEARNABLE_RULE_DIMS:
76
- new_rule = {
82
+ new_rule: dict[str, Any] = {
77
83
  "id": f"rule_{node.id}",
78
84
  "trigger": {
79
85
  "harness_status": "BLOCK",
@@ -84,8 +90,6 @@ class PolicyMixin:
84
90
  "created_at": datetime.now(timezone.utc).isoformat(),
85
91
  "source": "harness_learned",
86
92
  "agent_id": self.agent_id,
87
- # 影子账本:observations=适用次数;would_block=若生效会多拦;
88
- # would_pass=若生效会多放。三者是 promote/discard 的全部证据。
89
93
  "shadow": {
90
94
  "created_at": datetime.now(timezone.utc).isoformat(),
91
95
  "observations": 0, "would_block": 0, "would_pass": 0,
@@ -95,19 +99,12 @@ class PolicyMixin:
95
99
  self._policy.setdefault("shadow_rules", []).append(new_rule)
96
100
  self._record_history("propose", new_rule["id"],
97
101
  note=f"{dim} 维度 BLOCK → 影子观察")
98
- # 不 _reindex_learned_actions —— 影子规则**不得**影响判定
99
102
  self.bus.emit("shadow_rule_proposed", {
100
103
  "agent_id": self.agent_id, "rule_id": new_rule["id"],
101
104
  "dominant_dim": dim,
102
105
  "action_type": new_rule["action"].get("type", ""),
103
106
  })
104
107
 
105
- # 更新权重:触发截断的维度权重 +δ
106
- # 必须**先把新权重算完整再整体赋值**,不能"原地 +0.05 再归一化":
107
- # 后者会让 self.weights 出现「各维之和 = 1.05」的瞬时中间态,而此刻若有人读到它
108
- # (get_policy_summary / policy_updated 的监听者),拿到的就是未归一化的错值。
109
- # 实测:同一份 summary 持有的 weights 引用会被读出 0.35,而稍后再取是 0.333 —— 正是
110
- # 这个中间态被观察到。
111
108
  bumped = dict(self.weights)
112
109
  bumped[dim] = min(bumped[dim] + 0.05, 0.8)
113
110
  total = sum(bumped.values())
@@ -121,7 +118,6 @@ class PolicyMixin:
121
118
  "new_weights": dict(self.weights),
122
119
  })
123
120
 
124
- # ── 影子观察(双轨判定)──────────────────────────────────
125
121
 
126
122
  def _observe_shadow_rules(self, node: ThoughtNode, result: HarnessResult,
127
123
  real_scores: dict):
@@ -146,7 +142,6 @@ class PolicyMixin:
146
142
 
147
143
  sh["observations"] = sh.get("observations", 0) + 1
148
144
 
149
- # 双轨:把该规则的 action 应用到 scores 副本,用**同一套权重与阈值**判定
150
145
  shadow_status = self._status_with_action(rule, node, real_scores)
151
146
  real_blocked = result.is_blocked
152
147
  shadow_blocked = shadow_status == "BLOCK"
@@ -163,7 +158,7 @@ class PolicyMixin:
163
158
  self._discard_rule(rule, "影子证据不达标")
164
159
  need_save = True
165
160
  elif sh["observations"] % 10 == 0:
166
- need_save = True # 计数节流持久化
161
+ need_save = True
167
162
  if need_save:
168
163
  self._save_policy()
169
164
 
@@ -176,13 +171,9 @@ class PolicyMixin:
176
171
  if atype == "penalize_tool":
177
172
  pat = action.get("tool_pattern", "")
178
173
  tool_name = getattr(node, "tool_name", "") or ""
179
- # 与 _is_penalized_tool 同语义:**仅当前工具命中 pattern 才抬分**。
180
- # 若不看匹配、凡 tool 分非零就抬,会把「毫不相干工具的场景」误记成
181
- # would_block —— 影子证据完全失真,好规则会被冤判成误伤 discard。
182
174
  if pat and tool_name and (tool_name == pat or pat in tool_name):
183
175
  scores["tool"] = 1.0
184
176
  elif atype == "restrict_scope":
185
- # 与 check() 里 restrict_scope 的判定同构:新增实体超限则抬 scope
186
177
  lim = action.get("entity_expansion_limit")
187
178
  baseline = self._baseline
188
179
  if isinstance(lim, int) and baseline is not None and baseline.baseline_entities:
@@ -202,10 +193,8 @@ class PolicyMixin:
202
193
  if obs < self.promote_min_obs:
203
194
  return None
204
195
  wb, wp = sh.get("would_block", 0), sh.get("would_pass", 0)
205
- # 会多拦很多(可能误伤)→ 不 promote
206
196
  if wb / max(obs, 1) > 0.3:
207
197
  return False
208
- # 几乎不改变判定 → promote 没意义
209
198
  if wb == 0 and wp == 0:
210
199
  return False
211
200
  return True
@@ -214,7 +203,7 @@ class PolicyMixin:
214
203
  """影子 → 生效:移入 rules、重建索引、记 history、发事件。"""
215
204
  rule.get("shadow", {})["status"] = "promoted"
216
205
  self._policy.setdefault("shadow_rules", []).remove(rule)
217
- rule.pop("shadow", None) # 生效规则不再背影子账本
206
+ rule.pop("shadow", None)
218
207
  self._policy.setdefault("rules", []).append(rule)
219
208
  self._reindex_learned_actions()
220
209
  self._record_history("promote", rule["id"],
@@ -303,7 +292,6 @@ class PolicyMixin:
303
292
  "delta": -0.05,
304
293
  }
305
294
 
306
- # ── 阈值校准 ───────────────────────────────────────────
307
295
 
308
296
  def _calibrate_thresholds(self):
309
297
  """Shadow mode 结束 — 用真实 Δ 分布校准阈值。
@@ -311,20 +299,15 @@ class PolicyMixin:
311
299
  只有真正离群的(如完全跑题)才被拦截。
312
300
  否则用中位数附近的分位数会拦掉一半正常流量。
313
301
  """
314
- arr = np.array(sorted(self._delta_history), dtype=float)
302
+ arr = np.array(sorted(self._delta_history), dtype=np.float64)
315
303
  if arr.size == 0:
316
- # 空历史(理论上不会发生,因调用前每次 check 都 append)→ 用默认阈值,避免 nan
317
304
  self._shadow_mode = False
318
305
  return
319
306
  mean = float(arr.mean())
320
307
  std = float(arr.std())
321
308
  p90 = float(np.percentile(arr, 90))
322
- # BLOCK 阈值取"高分位 / 均值+1.5σ"的较大者,再留 10% 余量,
323
- # 保证正常任务(集中在均值附近)不被误拦
324
309
  block = max(p90, mean + 1.5 * std) * 1.10
325
- # WARN 阈值取 block 的 0.8 倍,给出渐进预警区
326
310
  warn = block * 0.8
327
- # 下限保护:阈值不低于默认值,避免样本噪声把阈值压得过紧
328
311
  self._block_threshold = max(block, 0.25)
329
312
  self._warn_threshold = max(warn, 0.18)
330
313
  self._shadow_mode = False
@@ -337,13 +320,11 @@ class PolicyMixin:
337
320
  "std": round(std, 3),
338
321
  })
339
322
 
340
- # ── Policy 持久化 ──────────────────────────────────────
341
323
 
342
324
  def _load_policy(self) -> dict:
343
325
  if self.policy_path.exists():
344
326
  with open(self.policy_path, encoding="utf-8") as f:
345
327
  return yaml.safe_load(f) or {}
346
- # 默认 Policy 骨架(格式从第一天锁定)
347
328
  return {
348
329
  "version": 1,
349
330
  "agent_id": self.agent_id,
@@ -7,18 +7,12 @@ from sklearn.metrics.pairwise import cosine_similarity
7
7
  from ._base import TOOL_CATEGORIES
8
8
 
9
9
 
10
- # 文本向量由 foxclaw.capability.embedding_service 统一提供(进程内共享,同名只加载一次)。
11
- # 不可用时该服务返回 None,SemanticDrift 自动降级为 n-gram 哈希嵌入。
12
-
13
-
14
10
  class SemanticDrift:
15
11
  """维度一:语义距离。优先 sentence-transformers,不可用则降级 n-gram 哈希嵌入。"""
16
12
 
17
13
  DIM = 256
18
14
 
19
15
  def __init__(self, model_name: str = "all-MiniLM-L6-v2"):
20
- # P0:走进程内共享 EmbeddingService,同名模型全系统只加载一次
21
- # (原来每个 Harness 各 load 一份;5 Agent = 5 份 → 现 1 份共享,只读并发 encode 安全)。
22
16
  from ..capability.embedding_service import get_text_embedder
23
17
  self._model = get_text_embedder(model_name)
24
18
  self._mode = "transformer" if self._model is not None else "hash"
@@ -58,8 +52,6 @@ class SemanticDrift:
58
52
  return 0.0
59
53
  node_vec = self._encode(content)
60
54
  sim = cosine_similarity([baseline_vec], [node_vec])[0][0]
61
- # 余弦相似度范围 [-1, 1],哈希嵌入带符号累加可能产生负相似度,
62
- # 1 - sim 会超过 1.0 → 必须双向裁剪到 [0, 1],否则漂移分超出阈值范围使判定失真。
63
55
  return float(min(1.0, max(0.0, 1.0 - sim)))
64
56
 
65
57
 
@@ -72,17 +64,13 @@ class ToolIntentDrift:
72
64
  for cat, patterns in TOOL_CATEGORIES.items():
73
65
  if any(tool_name.startswith(p) or p in tool_name for p in patterns):
74
66
  return 0.0 if cat in allowed_categories else 1.0
75
- return 0.3 # 未知工具:中等风险
67
+ return 0.3
76
68
 
77
69
 
78
70
  class ScopeCreepDrift:
79
71
  """维度三:范围蔓延"""
80
72
 
81
73
  def score(self, node_entities: set[str], baseline_entities: set[str]) -> float:
82
- # 空基线返回 0 是有意设计:范围蔓延是"相对基线的偏离",没有基线参照就无从
83
- # 谈蔓延。若此处对空基线返回高分,会让所有尚未建立实体基线的早期任务被误判为
84
- # 范围越界(误报)。范围检测的有效性依赖基线实体提取——若该维度长期为 0,
85
- # 应排查实体提取(register_goal 的 entities)是否为空,而非改动本函数。
86
74
  if not baseline_entities:
87
75
  return 0.0
88
76
  new_entities = node_entities - baseline_entities
@@ -5,25 +5,22 @@ from typing import Callable
5
5
  class HarnessEventBus:
6
6
  """阶段一:事件总线(观测优先)"""
7
7
 
8
- # 高频且纯内部的信号不上脊柱,避免 WS 噪音(见审计"哪些事件不应该广播")
9
8
  _SPINE_SKIP = {"goal_registered"}
10
9
 
11
10
  def __init__(self, spine=None, source: str = ""):
12
11
  self._handlers: dict[str, list[Callable]] = {}
13
- self._spine = spine # 【P0·统一脊柱】非空则把 emit 同时转发进全局 EventSpine
14
- self._source = source # 事件来源(agent_id),便于投影侧区分
12
+ self._spine = spine
13
+ self._source = source
15
14
 
16
15
  def on(self, event: str, handler: Callable):
17
16
  self._handlers.setdefault(event, []).append(handler)
18
17
 
19
18
  def emit(self, event: str, data: dict):
20
- # 1) 本地订阅者(向后兼容:agent.harness.bus.on / harness 自订阅 / governance)
21
19
  for h in self._handlers.get(event, []):
22
20
  try:
23
21
  h(data)
24
22
  except Exception as e:
25
23
  print(f"[Harness EventBus] handler error: {e}")
26
- # 2) 汇入全局脊柱:harness 事件与系统事件走同一条脊柱、被同一 WS 投影观测
27
24
  if self._spine is not None and event not in self._SPINE_SKIP:
28
25
  try:
29
26
  self._spine.publish(event, data, source=self._source or "harness")