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
@@ -24,10 +24,6 @@ def test_structural_convergence():
24
24
  for A in (FinanceAgent, OfficeAgent):
25
25
  assert A.run_task is CognitiveAgent.run_task, f"{A.__name__} 不应重写 run_task"
26
26
  assert A._execute is not CognitiveAgent._execute, f"{A.__name__} 应实现 _execute"
27
- # 【P1-1】主管也不再重写 run_task:路由分发拆成 SUPERVISOR_STAGES 七个 pipeline
28
- # stage,走 base 统一模板(获得 hooks/流式/中断/统一 emit)。主管的差异收敛为
29
- # _cognitive_pipeline 覆盖 + 领域钩子,而非另起生命周期。(本断言此前锁的是
30
- # "主管保留自身 run_task" 的旧结构,P1-1 落地时同步翻转——又一处"测试锁旧行为"。)
31
27
  assert SupervisorAgent.run_task is CognitiveAgent.run_task, \
32
28
  "P1-1:主管不再重写 run_task(路由分发在 pipeline stage 里)"
33
29
  assert SupervisorAgent._cognitive_pipeline is not CognitiveAgent._cognitive_pipeline, \
@@ -37,10 +33,8 @@ def test_structural_convergence():
37
33
 
38
34
  def test_domain_hooks():
39
35
  print("\n【2】领域钩子:entities / recall 前缀 / 召回条数 逐字保留")
40
- # 钩子是纯函数,可脱离实例校验(lambda(self,task))
41
36
  assert FinanceAgent._task_entities(None, "x") == ["财务", "收支", "储蓄"]
42
37
  assert FinanceAgent._recall_query(None, "买车") == "财务 买车"
43
- # 行政并入办公:同一 Agent 同时覆盖学习 + 日程两个域
44
38
  assert OfficeAgent._recall_query(None, "开会") == "学习 知识 日程 行程 开会"
45
39
  assert OfficeAgent._task_entities(None, "x") == ["学习", "知识", "文档", "日程", "行程", "会议"]
46
40
  assert OfficeAgent._recall_topk == 8, "办公学习 Agent 覆盖为广泛召回 8"
@@ -58,7 +52,6 @@ def test_unified_block_handling():
58
52
  async def boom(self, task, context, session_id, memories):
59
53
  raise HarnessBlockedError(types.SimpleNamespace(reason="模拟越界"), None)
60
54
 
61
- # 只替换 finance 的领域执行,模板的兜底应把它转成结构化错误
62
55
  os_.finance._execute = types.MethodType(boom, os_.finance)
63
56
  r = asyncio.run(os_.finance.run_task("危险任务"))
64
57
  assert r.get("error") == "harness_blocked", f"应兜底为 harness_blocked,实得 {r}"
@@ -81,16 +74,13 @@ def test_finance_verify_before_recommend():
81
74
  from foxclaw import Foxclaw
82
75
  os_ = Foxclaw(data_dir=d, shadow_rounds=50)
83
76
  fin = os_.finance
84
- # 给账本内核灌入真实交易 → analyze() 有据可依(当前数据源存在)
85
77
  os_.finance_runtime.import_csv(
86
78
  str(Path(__file__).parent.parent / "examples" / "sample_transactions.csv"))
87
- # 捕获传给 LLM 的 prompt(不真调模型)
88
79
  captured = {}
89
80
  async def _capture(self, task, messages):
90
81
  captured["prompt"] = messages[-1]["content"]
91
82
  return "(模拟回答)"
92
83
  fin._call_llm_web = types.MethodType(_capture, fin)
93
- # 一条**过时**召回记忆:若被当作事实会误导用户
94
84
  stale = [{"content": "历史:上月预算结余 8000 元", "id": "m1"}]
95
85
  asyncio.run(fin._execute("这个月还能花多少?", {}, "s1", stale))
96
86
  p = captured["prompt"]
@@ -115,32 +105,23 @@ def test_run_task_stream():
115
105
  try:
116
106
  from foxclaw import Foxclaw
117
107
  os_ = Foxclaw(data_dir=d, shadow_rounds=50)
118
- # 单 Agent 认知管线(7 stage)用领域 Agent 验证:主管走的是 meta/schedule/dispatch
119
- # 另一条管线,不能拿它当"标准 stage 序列"的样本。
120
108
  ag = os_.office
121
- ag.router.provider = "stub" # 强制 stub(is_stub = provider=='stub'):离线确定性(不受本机
122
- # 是否配了 LLM key 影响),且 stub 的 router.stream 走 _chunk_text
123
- # 无损分块 → 可断言"流式 token 拼接 == 最终答案"
109
+ ag.router.provider = "stub"
124
110
 
125
- # (a) 正常流式:collect 所有事件
126
111
  events = list(ag.run_task_stream("帮我总结一下今天"))
127
112
  stages = [e["stage"] for e in events if "stage" in e]
128
113
  deltas = [e["delta"] for e in events if "delta" in e]
129
114
  results = [e["result"] for e in events if "result" in e]
130
115
  assert stages == ["perceive", "recall", "plan", "route", "act", "verify", "reflect"], stages
131
- # delta 非空 = _act_stream contextvar 成功跨线程/asyncio 传到了 _call_llm(核心正确性证明)
132
116
  assert len(deltas) > 0, "act 应逐 token 出字;为空说明 contextvar 没传到 _call_llm"
133
117
  assert len(results) == 1, f"应恰有一个最终 result,实得 {len(results)}"
134
118
  res = results[0]
135
119
  assert res.get("agent") == ag.AGENT_ID and "session_id" in res and "result" in res
136
- # 拼接的流式 token == 最终答案(token 透传忠实;_chunk_text 无损)
137
120
  assert "".join(deltas) == res["result"], "流式 token 拼接应等于最终答案"
138
121
 
139
- # (b) 行为保持:非流式 run_task 仍正常、返回体无 delta 泄漏
140
122
  plain = asyncio.run(ag.run_task("帮我总结一下今天"))
141
123
  assert "result" in plain and "delta" not in plain and plain.get("agent") == ag.AGENT_ID
142
124
 
143
- # (c) stop_event 预置 → 流式在起步即停:无 stage、无 delta、result.interrupted
144
125
  import threading as _th
145
126
  ev = _th.Event(); ev.set()
146
127
  ev_events = list(ag.run_task_stream("这个任务会被中断", stop_event=ev))
@@ -149,8 +130,6 @@ def test_run_task_stream():
149
130
  assert ev_stages == [], f"预置 stop → 0 个 stage 事件,实得 {ev_stages}"
150
131
  assert len(ev_results) == 1 and ev_results[0].get("interrupted") is True, ev_results
151
132
 
152
- # (d) fail-safe:act 流式若抛异常 → 降级非流式 complete、返回正常(不崩、不中断任务)。
153
- # 锁住 _call_llm docstring 承诺的"流式任何异常 → fail-safe 降级 complete"。
154
133
  def _boom(*a, **k):
155
134
  raise RuntimeError("流式炸了")
156
135
  ag.router.stream = _boom
@@ -193,12 +172,10 @@ def test_interrupted_task_not_sunk():
193
172
  captured["blocked"] = blocked
194
173
  return orig(task, result, blocked=blocked, context=context)
195
174
  ag._emit_execution = _spy
196
- # (a) 预置 stop → 中断 → blocked=True(不沉淀部分/空结果为真值)
197
175
  ev = _th.Event(); ev.set()
198
176
  r = asyncio.run(ag.run_task("会被中断的任务", stop_event=ev))
199
177
  assert r.get("interrupted") is True, r
200
178
  assert captured.get("blocked") is True, "中断任务必须 blocked=True(不把部分/空结果当完成真值沉淀)"
201
- # (b) 正常任务 → blocked=False(behavior-preserving:照常沉淀)
202
179
  captured.clear()
203
180
  r2 = asyncio.run(ag.run_task("正常任务"))
204
181
  assert "interrupted" not in r2 and captured.get("blocked") is False, \
@@ -220,7 +197,7 @@ def test_reasoning_lifecycle_hooks():
220
197
  try:
221
198
  from foxclaw import Foxclaw
222
199
  os_ = Foxclaw(data_dir=d, shadow_rounds=50)
223
- ag = os_.office # 单 Agent 认知管线(见上一条注释)
200
+ ag = os_.office
224
201
  ag.router.provider = "stub"
225
202
  seen = {}
226
203
  def _cap_complete(system, messages, max_tokens, agent_id):
@@ -228,13 +205,11 @@ def test_reasoning_lifecycle_hooks():
228
205
  return "(stub 回答)"
229
206
  ag.router.complete = _cap_complete
230
207
 
231
- # (a) 无钩子基线:不注入(behavior-preserving)
232
208
  asyncio.run(ag.run_task("基线任务"))
233
209
  base_msgs = seen.get("messages", [])
234
210
  assert base_msgs and "【E2注入】" not in base_msgs[0].get("content", ""), \
235
211
  f"无 pre_reasoning 钩子时不应注入,实得 {base_msgs[:1]}"
236
212
 
237
- # (b) 注册 pre_reasoning(注入)+ task_complete(观测)
238
213
  completed = []
239
214
  ag.hooks.register("pre_reasoning", lambda c: {"additional_context": "【E2注入】优先考虑省钱"})
240
215
  ag.hooks.register("task_complete", lambda c: completed.append(c.get("result")))
@@ -242,13 +217,11 @@ def test_reasoning_lifecycle_hooks():
242
217
  msgs = seen.get("messages", [])
243
218
  joined = " ".join(m.get("content", "") for m in msgs)
244
219
  assert "【E2注入】优先考虑省钱" in joined, f"pre_reasoning 注入应进入 LLM messages,实得 {msgs}"
245
- # A3:注入须合并进 user 消息,**不得产生连续两条 user**(模型无关铁律,某些端点会拒连续 user)
246
220
  roles = [m.get("role") for m in msgs]
247
221
  assert not any(roles[i] == roles[i + 1] == "user" for i in range(len(roles) - 1)), \
248
222
  f"注入不应产生连续两条 user 消息(违反模型无关铁律),roles={roles}"
249
223
  assert len(completed) == 1, f"task_complete 应触发一次并收到 result,实得 {len(completed)}"
250
224
 
251
- # (c) HarnessBlock 结束路径:task_complete 也触发(观测被拦截任务,带 error)
252
225
  import types
253
226
  async def _boom(self, task, context, session_id, memories):
254
227
  raise HarnessBlockedError(types.SimpleNamespace(reason="E2越界"), None)
@@ -274,32 +247,28 @@ def test_preference_priority_recall():
274
247
  try:
275
248
  from foxclaw import Foxclaw
276
249
  os_ = Foxclaw(data_dir=d, shadow_rounds=50)
277
- ag = os_.office # 单 Agent 认知管线(见上一条注释)
250
+ ag = os_.office
278
251
  ag.router.provider = "stub"
279
252
  seen = {}
280
253
  def _cap(system, messages, max_tokens, agent_id):
281
254
  seen["messages"] = messages; return "(stub)"
282
255
  ag.router.complete = _cap
283
256
 
284
- # (a) 明确偏好表达 → 存成带 preference tag(shared)
285
257
  asyncio.run(ag.run_task("我喜欢简洁直接的回答,不要长篇大论"))
286
258
  prefs = ag.hmr.recall_by_tag(agent_id="supervisor", tag="preference", limit=8)
287
259
  assert any("简洁" in p.get("content", "") for p in prefs), f"偏好应带 preference tag 存下,实得 {prefs}"
288
260
 
289
- # (b) 发**语义完全不相关**的任务 → 偏好仍注入 prompt(纯语义召回此时召不到)
290
261
  seen.clear()
291
262
  asyncio.run(ag.run_task("解释一下光合作用的原理"))
292
263
  joined = " ".join(m.get("content", "") for m in seen.get("messages", []))
293
264
  assert "用户偏好" in joined and "简洁" in joined, \
294
265
  f"语义不相关任务也应注入偏好(越用越懂你),实得 {seen.get('messages')}"
295
266
 
296
- # (c) 无偏好信号的普通任务不误存(保守采集)
297
267
  before = len(ag.hmr.recall_by_tag(agent_id="assistant", tag="preference"))
298
268
  asyncio.run(ag.run_task("光合作用发生在植物的哪个部位"))
299
269
  after = len(ag.hmr.recall_by_tag(agent_id="assistant", tag="preference"))
300
270
  assert after == before, "无偏好信号的任务不应新增偏好"
301
271
 
302
- # (d) 关采集开关 → 偏好句也不存(可关)
303
272
  os.environ["FOXCLAW_PREF_CAPTURE"] = "0"
304
273
  try:
305
274
  asyncio.run(ag.run_task("我喜欢用表格展示数据"))
@@ -308,16 +277,14 @@ def test_preference_priority_recall():
308
277
  finally:
309
278
  os.environ.pop("FOXCLAW_PREF_CAPTURE", None)
310
279
 
311
- # (e) 对话路径(stream_reply)也注入偏好——之前只测了任务路径(深审 F2:claim 必测)
312
280
  seen_stream = []
313
281
  def _cap_stream(system, messages, *a, **k):
314
282
  seen_stream.append(messages); yield "(stub)"
315
283
  ag.router.stream = _cap_stream
316
- list(ag.stream_reply("随便讲点什么")) # 语义与"简洁"偏好不相关
284
+ list(ag.stream_reply("随便讲点什么"))
317
285
  js = " ".join(m.get("content", "") for m in (seen_stream[0] if seen_stream else []))
318
286
  assert "用户偏好" in js and "简洁" in js, f"对话路径(stream_reply)也应注入偏好,实得 {seen_stream}"
319
287
 
320
- # (f) 去重:同一偏好存多条,召回只返一条(深审 F1)
321
288
  for _ in range(3):
322
289
  ag._store_memory("用户偏好:我喜欢简洁直接的回答,不要长篇大论",
323
290
  mem_type="preference", tags=["preference"], shared=True)
@@ -325,7 +292,6 @@ def test_preference_priority_recall():
325
292
  same = [p for p in got if "简洁直接的回答" in p.get("content", "")]
326
293
  assert len(same) == 1, f"重复偏好召回应去重为 1 条,实得 {len(same)}"
327
294
 
328
- # (g) 删偏好途径(F3):存唯一偏好 → list 找到(带 id)→ delete → 确认删除
329
295
  ag._store_memory("用户偏好:请用英文回复", mem_type="preference", tags=["preference"], shared=True)
330
296
  target = [p for p in ag.list_preferences() if "英文" in p.get("content", "")]
331
297
  assert target and target[0].get("id"), f"唯一偏好应可列出带 id,实得 {target}"
@@ -0,0 +1,81 @@
1
+ """
2
+ Agent 规则文件加载测试 —— 全离线。
3
+
4
+ 覆盖:
5
+ 1. agents/rules/<AGENT_ID>/*.md 在组装系统提示词时被读入(启动即生效,不靠代码常驻)。
6
+ 2. 叠加顺序:内核前缀 → 角色职责 → 本 Agent 规则;规则位于最后。
7
+ 3. 回退开关 FOXCLAW_AGENT_RULES=0 可整体关闭;无 AGENT_ID / 无目录时不注入。
8
+ 4. 规则目录必须落在 foxclaw 包内(随 wheel 分发,不能像仓库根 skills/ 那样漏包)。
9
+ """
10
+ import os, sys, types
11
+ from pathlib import Path
12
+ os.environ.setdefault("HF_HUB_OFFLINE", "1")
13
+ os.environ.setdefault("TRANSFORMERS_OFFLINE", "1")
14
+ os.environ["FOXCLAW_EMBEDDING_MODEL"] = "foxclaw-nonexistent-fast"
15
+ sys.path.insert(0, str(Path(__file__).parent.parent))
16
+
17
+ from foxclaw.agents.base import KERNEL_PROMPT
18
+ from foxclaw.agents.mixins.prompt import PromptMixin, RULES_DIR, load_agent_rules
19
+
20
+
21
+ def _fake(role="我是财务角色ROLEXYZ。", agent_id="finance"):
22
+ return types.SimpleNamespace(_role_prompt=role, AGENT_ID=agent_id)
23
+
24
+
25
+ def test_finance_rules_file_is_loaded():
26
+ text = load_agent_rules("finance")
27
+ assert "Personal Finance Agent Rules v1.0" in text
28
+ assert "不得编造" in text
29
+ assert "储蓄率" in text
30
+
31
+
32
+ def test_unknown_agent_has_no_rules():
33
+ assert load_agent_rules("no_such_agent") == ""
34
+ assert load_agent_rules("") == ""
35
+
36
+
37
+ def test_system_prompt_order_kernel_role_rules():
38
+ out = PromptMixin.system_prompt.fget(_fake())
39
+ assert KERNEL_PROMPT in out
40
+ assert out.index("认知操作系统") < out.index("ROLEXYZ") < out.index("Personal Finance Agent Rules")
41
+
42
+
43
+ def test_rules_follow_kernel_when_role_is_short():
44
+ out = PromptMixin.system_prompt.fget(_fake(role="ROLEXYZ"))
45
+ assert "本 Agent 专业规则(必须遵守)" in out
46
+ assert "月度结余" in out
47
+
48
+
49
+ def test_switch_disables_rules(monkeypatch):
50
+ monkeypatch.setenv("FOXCLAW_AGENT_RULES", "0")
51
+ out = PromptMixin.system_prompt.fget(_fake())
52
+ assert "ROLEXYZ" in out and "Personal Finance Agent Rules" not in out
53
+
54
+
55
+ def test_switch_disables_rules_even_without_kernel(monkeypatch):
56
+ monkeypatch.setenv("FOXCLAW_KERNEL_PROMPT", "0")
57
+ monkeypatch.setenv("FOXCLAW_AGENT_RULES", "0")
58
+ monkeypatch.setenv("FOXCLAW_BEHAVIOR", "0")
59
+ assert PromptMixin.system_prompt.fget(_fake(role="ROLEXYZ")) == "ROLEXYZ"
60
+
61
+
62
+ def test_rules_survive_kernel_off(monkeypatch):
63
+ monkeypatch.setenv("FOXCLAW_KERNEL_PROMPT", "0")
64
+ out = PromptMixin.system_prompt.fget(_fake(role="ROLEXYZ"))
65
+ assert KERNEL_PROMPT not in out
66
+ assert "Personal Finance Agent Rules" in out
67
+ # 行为规范是硬边界,不随内核前缀一起关掉
68
+ assert out.index("[行为规范") < out.index("ROLEXYZ")
69
+
70
+
71
+ def test_agent_without_id_gets_no_rules():
72
+ fake = types.SimpleNamespace(_role_prompt="ROLEXYZ")
73
+ out = PromptMixin.system_prompt.fget(fake)
74
+ assert "ROLEXYZ" in out and "Personal Finance Agent Rules" not in out
75
+
76
+
77
+ def test_rules_dir_lives_inside_package():
78
+ """规则文件必须在 foxclaw 包内:仓库根的 skills/ 就没被打进 wheel,别重演。"""
79
+ pkg = Path(RULES_DIR).resolve()
80
+ assert pkg.name == "rules" and pkg.parent.name == "agents"
81
+ assert (pkg / "finance" / "RULES.md").is_file()
@@ -33,7 +33,6 @@ def check(cond, label):
33
33
  print(f" ✗ {label}")
34
34
 
35
35
 
36
- # ═══════════════ [1]-[3] P2-2 compose pending ═══════════════
37
36
  from foxclaw.skills import SkillRegistry, SkillSpec, SkillAdapter
38
37
  from foxclaw.skills.pipeline import compose, plan, AHSPipeline
39
38
 
@@ -58,7 +57,7 @@ def _make_pending_registry(counter: dict):
58
57
  reg.register(SkillSpec(
59
58
  id="send_email", name="发邮件", description="发邮件",
60
59
  kind="local", triggers=["发送", "邮件"], side_effect="send",
61
- destructive=True, # ← 未确认 → needs_confirmation
60
+ destructive=True,
62
61
  handler=lambda to="": {"sent": to}, params={"to": "str"}))
63
62
  return reg
64
63
 
@@ -92,8 +91,6 @@ def test_compose_pending():
92
91
 
93
92
  def test_compose_failed_still_rolls_back():
94
93
  print("\n【1b】对照:P2-2 后真失败(not_connected)仍触发回滚")
95
- # confirmed=True:write/send 都过权限;第二步 handler=None → dispatch not_connected
96
- # → 真失败 → 回滚第一步(write 副作用,有 compensate)。若误走 pending 则不回滚。
97
94
  reg = SkillRegistry()
98
95
  rolled = []
99
96
  reg.register(SkillSpec(
@@ -104,7 +101,7 @@ def test_compose_failed_still_rolls_back():
104
101
  reg.register(SkillSpec(
105
102
  id="send_email", name="发邮件", description="发邮件",
106
103
  kind="local", triggers=["发送", "邮件"], side_effect="send",
107
- handler=None, # ← 无 handler → not_connected(非 pending)
104
+ handler=None,
108
105
  params={"to": "str"}))
109
106
  adp = SkillAdapter(reg)
110
107
  res = compose(plan("创建日程 然后 发送邮件", reg), reg, adp, confirmed=True)
@@ -151,7 +148,6 @@ def test_ahs_pipeline_pending():
151
148
  "composition 字典带 pending_confirmation/completed_steps,未回滚")
152
149
 
153
150
 
154
- # ═══════════════ [4] P2-1 run_async ═══════════════
155
151
  def test_run_async():
156
152
  print("\n【4】P2-1:run_async 异步入口")
157
153
  reg = SkillRegistry()
@@ -171,24 +167,19 @@ def test_run_async():
171
167
  pipe = AHSPipeline(reg, SkillAdapter(reg))
172
168
 
173
169
  async def _main():
174
- # a) 返回 PipelineResult
175
170
  r = await pipe.run_async("计算 1+1")
176
171
  check(r.task == "计算 1+1" and (r.success or r.is_pending),
177
172
  "run_async 返回 PipelineResult")
178
- # b) 不阻塞事件循环:两个 0.5s 慢任务并发,若串行 ≥1.0s,并发 ≈0.5s
179
- # (阈值 0.9s:串行/并发区分度大,且留足调度抖动余量——0.3/0.5 的窄距实测会偶抖)
180
173
  t0 = time.perf_counter()
181
174
  await asyncio.gather(pipe.run_async("慢 1+1"), pipe.run_async("慢 2+2"))
182
175
  elapsed = time.perf_counter() - t0
183
176
  check(elapsed < 0.9,
184
177
  f"并发两个慢 run_async 耗时 {elapsed:.2f}s < 0.9s(未阻塞事件循环)")
185
178
  asyncio.run(_main())
186
- # c) 同步 run 向后兼容
187
179
  r_sync = pipe.run("计算 1+1")
188
180
  check(r_sync.task == "计算 1+1", "同步 run 仍可用(向后兼容)")
189
181
 
190
182
 
191
- # ═══════════════ [5]-[7] P2-3 idempotency ═══════════════
192
183
  from foxclaw.core.idempotency import IdempotencyGuard
193
184
 
194
185
 
@@ -204,7 +195,7 @@ def test_idem_append_log():
204
195
  print("\n【5】P2-3:追加日志(O(1) 写)+ compact 合并 + 重启回放")
205
196
  d = tempfile.mkdtemp(prefix="idem_a_")
206
197
  path = os.path.join(d, "idem.json")
207
- g = IdempotencyGuard(persist_path=path, compact_every=1000) # 不触发 compact
198
+ g = IdempotencyGuard(persist_path=path, compact_every=1000)
208
199
  g.mark("k1"); g.mark("k2"); g.mark("k3")
209
200
  log_path = path + ".log"
210
201
  check(os.path.exists(log_path), "mark 产生追加日志")
@@ -213,7 +204,6 @@ def test_idem_append_log():
213
204
  check(len(lines) == 3, f"3 次 mark = 3 行日志(实为 {len(lines)}),非全量写")
214
205
  check(not os.path.exists(path), "未达阈值不写全量快照")
215
206
 
216
- # compact 合并:compact_every=5,第 5 次 mark 触发(轮转日志→快照;新 append 开新文件)
217
207
  d2 = tempfile.mkdtemp(prefix="idem_b_")
218
208
  path2 = os.path.join(d2, "idem.json")
219
209
  g2 = IdempotencyGuard(persist_path=path2, compact_every=5)
@@ -225,13 +215,11 @@ def test_idem_append_log():
225
215
  with open(path2, encoding="utf-8") as f:
226
216
  data = json.load(f)
227
217
  check(len(data) == 5, f"快照含全部 5 条(实为 {len(data)})")
228
- # compact 之后的新 mark 进新日志;重启 = 读快照(5) + 回放新日志(1) = 6
229
218
  g2.mark("k5")
230
219
  g2b = IdempotencyGuard(persist_path=path2, compact_every=10000)
231
220
  check(all(g2b.seen(f"k{i}") for i in range(6)),
232
221
  "compact 后继续 mark:重启回放 = 快照5 + 新日志1 = 6 全可见")
233
222
 
234
- # 重启回放:g3 不写快照(阈值大),g4 启动 = 读快照(无)+ 回放日志
235
223
  d3 = tempfile.mkdtemp(prefix="idem_c_")
236
224
  path3 = os.path.join(d3, "idem.json")
237
225
  g3 = IdempotencyGuard(persist_path=path3, compact_every=10000)
@@ -243,7 +231,6 @@ def test_idem_append_log():
243
231
 
244
232
  def test_idem_compat_and_eviction():
245
233
  print("\n【6】P2-3:旧快照兼容 + 容量淘汰发 delete(重启不复活)")
246
- # 旧格式:纯全量快照,无 .log → 直接可读
247
234
  d = tempfile.mkdtemp(prefix="idem_d_")
248
235
  path = os.path.join(d, "idem.json")
249
236
  with open(path, "w", encoding="utf-8") as f:
@@ -252,7 +239,6 @@ def test_idem_compat_and_eviction():
252
239
  check(g.seen("legacy_k") and g.get_result("legacy_k") == {"x": 1},
253
240
  "旧版纯快照格式兼容(无日志,回放为空)")
254
241
 
255
- # 容量淘汰必须落 delete:否则重启回放 mark 日志会"复活"被淘汰的 key
256
242
  d2 = tempfile.mkdtemp(prefix="idem_e_")
257
243
  path2 = os.path.join(d2, "idem.json")
258
244
  g2 = IdempotencyGuard(persist_path=path2, max_size=2, compact_every=10000)
@@ -277,7 +263,7 @@ def test_idem_persist_failure():
277
263
  return orig_open(*a, **kw)
278
264
  builtins.open = _bad_open
279
265
  try:
280
- g.mark("k1") # 追加日志失败 → 计数 + 事件;主流程不炸
266
+ g.mark("k1")
281
267
  finally:
282
268
  builtins.open = orig_open
283
269
  check(g.seen("k1"), "持久化失败不影响内存主流程")
@@ -289,7 +275,6 @@ def test_idem_persist_failure():
289
275
  "stats() 含新字段(无 persist_path 也一样)")
290
276
 
291
277
 
292
- # ═══════════════ [8]-[9] P2-4 recovery ═══════════════
293
278
  from foxclaw.core.recovery import RestartRecovery, RecoveryPlan
294
279
 
295
280
 
@@ -313,7 +298,6 @@ def test_recovery_plan():
313
298
  "details 逐任务带 action")
314
299
  evts = [e for e in spine.events if e[0] == "recovery.plan"]
315
300
  check(len(evts) == 3, f"每个分类决定发 recovery.plan 事件(实为 {len(evts)})")
316
- # scan_interrupted 返回类型不变(兼容)
317
301
  check(isinstance(r2.scan_interrupted(), list), "scan_interrupted 仍返回 list(兼容)")
318
302
 
319
303
 
@@ -330,7 +314,6 @@ def test_recovery_details():
330
314
  check(records["t1"].status == "in_progress" and records["t1"].description == "second",
331
315
  "重复 start 保留最后一次")
332
316
 
333
- # compact_log:started 很早、finished 刚刚 → 旧逻辑(只看 started)会丢,新逻辑保留
334
317
  d2 = tempfile.mkdtemp(prefix="recv_c_")
335
318
  log2 = os.path.join(d2, "log.jsonl")
336
319
  old = time.time() - 30 * 86400
@@ -345,20 +328,17 @@ def test_recovery_details():
345
328
  "compact 用 max(started,finished):started 30 天前但刚完成的记录不丢")
346
329
  check(r2._replay()["t_old"].finished > 0, "compact 保留 finished 时间戳")
347
330
 
348
- # fsync=False 可用(高频场景开关)
349
331
  d3 = tempfile.mkdtemp(prefix="recv_d_")
350
332
  log3 = os.path.join(d3, "log.jsonl")
351
333
  r3 = RestartRecovery(log3, boot_id="boot1", fsync=False)
352
334
  r3.task_started("t1", "x")
353
335
  r3.task_finished("t1")
354
336
  check(r3._replay()["t1"].status == "done", "fsync=False 正常读写(开关兼容)")
355
- # 空中断 → 空计划
356
337
  r4 = RestartRecovery(os.path.join(d3, "empty.jsonl"), boot_id="b")
357
338
  p4 = r4.plan_recovery([])
358
339
  check(p4.details == [] and p4.retryable == [], "无中断 → 空计划(不炸)")
359
340
 
360
341
 
361
- # ═══════════════ [10] P3-2 /api/ahs/run ═══════════════
362
342
  def test_api_ahs_run():
363
343
  print("\n【10】P3-2:/api/ahs/run 走 run_async + 暴露 pending 字段")
364
344
  os.environ["FOXCLAW_SHADOW_ROUNDS"] = "6"
@@ -377,7 +357,6 @@ def test_api_ahs_run():
377
357
  check("is_pending" in body and "pending_confirmation" in body,
378
358
  "响应暴露 is_pending/pending_confirmation 字段")
379
359
  check(body.get("success") in (True, False), "success 字段正常返回")
380
- # resume_state 参数被接受(闭环 P2-2 续跑;空断点现场 → 正常全跑)
381
360
  r2 = c.post("/api/ahs/run", json={
382
361
  "task": "计算 3*3", "confirmed": True,
383
362
  "resume_state": {"completed_steps": [], "resume_at": None}})
@@ -64,17 +64,15 @@ def _plan_tool():
64
64
  "required": ["flow"]}}}
65
65
 
66
66
 
67
- # ── 1. 格式转换 ───────────────────────────────────────────────────────────────
68
67
  def test_to_anthropic_tools():
69
68
  out = ModelRouter._to_anthropic_tools([
70
69
  _plan_tool(),
71
- {"name": "direct", "description": "", "input_schema": {}}, # 已是 anthropic 格式
70
+ {"name": "direct", "description": "", "input_schema": {}},
72
71
  ])
73
72
  assert len(out) == 2
74
73
  assert out[0]["name"] == "submit_plan"
75
74
  assert out[0]["input_schema"]["required"] == ["flow"]
76
75
  assert out[1]["name"] == "direct"
77
- # 非法项(空名/非 dict)跳过
78
76
  assert ModelRouter._to_anthropic_tools([{"type": "function", "function": {"name": ""}}]) == []
79
77
  assert ModelRouter._to_anthropic_tools(None) == []
80
78
 
@@ -88,18 +86,15 @@ def test_to_anthropic_tool_choice():
88
86
  {"type": "function", "function": {"name": "x"}}) == {"type": "tool", "name": "x"}
89
87
 
90
88
 
91
- # ── 2. supports_tools ──────────────────────────────────────────────────────────
92
89
  def test_supports_tools_anthropic():
93
90
  r = _router(_fake_client([[]]))
94
91
  assert r.is_stub is False
95
92
  assert r.supports_tools() is True
96
- # 无 client(stub)→ 不支持
97
93
  r2 = ModelRouter.__new__(ModelRouter)
98
94
  r2.provider = "anthropic"; r2._client = None
99
95
  assert r2.supports_tools() is False
100
96
 
101
97
 
102
- # ── 3. call_tools anthropic 分支 ──────────────────────────────────────────────
103
98
  def test_call_tools_anthropic_tool_use():
104
99
  client = _fake_client([[
105
100
  _block(type="tool_use", id="toolu_1", name="submit_plan",
@@ -113,13 +108,11 @@ def test_call_tools_anthropic_tool_use():
113
108
  "args": {"flow": "sequential",
114
109
  "steps": [{"name": "查支出", "agent": "finance"}]},
115
110
  "args_parse_failed": False}]
116
- # 入参:tools 转 anthropic 格式 + tool_choice 转 auto + system 单独传 + 运行时注入
117
111
  kw = client.calls[0]
118
112
  assert kw["system"].startswith("【运行时信息") and "sys" in kw["system"]
119
113
  assert kw["tools"][0]["name"] == "submit_plan"
120
114
  assert kw["tool_choice"] == {"type": "auto"}
121
115
  assert all(m["role"] != "system" for m in kw["messages"]), "anthropic 不允许 system 角色入 messages"
122
- # assistant_msg 是 anthropic 原生 blocks
123
116
  assert res["assistant_msg"]["role"] == "assistant"
124
117
  assert res["assistant_msg"]["content"][0]["type"] == "tool_use"
125
118
 
@@ -144,7 +137,6 @@ def test_call_tools_anthropic_tool_choice_none():
144
137
  assert kw["tools"][0]["name"] == "submit_plan", "历史含 tool_use 时必须仍传 tools"
145
138
 
146
139
 
147
- # ── 4. format_tool_result ─────────────────────────────────────────────────────
148
140
  def test_format_tool_result_anthropic():
149
141
  r = _router(_fake_client([[]]))
150
142
  out = r.format_tool_result({"id": "toolu_9", "name": "web_search"}, "结果")
@@ -152,7 +144,6 @@ def test_format_tool_result_anthropic():
152
144
  {"type": "tool_result", "tool_use_id": "toolu_9", "content": "结果"}]}
153
145
 
154
146
 
155
- # ── 5. fail-safe ──────────────────────────────────────────────────────────────
156
147
  def test_call_tools_anthropic_failsafe():
157
148
  def create(**kw):
158
149
  raise RuntimeError("boom")
@@ -162,19 +153,15 @@ def test_call_tools_anthropic_failsafe():
162
153
  res = r.call_tools("sys", [{"role": "user", "content": "t"}], [])
163
154
  assert res["supported"] is False and res.get("error")
164
155
 
165
- # stub(无 client)→ supported=False
166
156
  r2 = _router(None)
167
157
  res2 = r2.call_tools("sys", [{"role": "user", "content": "t"}], [])
168
158
  assert res2["supported"] is False
169
159
 
170
160
 
171
- # ── 6. 回灌闭环(_reply_with_tools 在 anthropic 下)────────────────────────────
172
161
  def test_reply_with_tools_anthropic_loop():
173
162
  from foxclaw.agents.base import CognitiveAgent
174
163
  client = _fake_client([
175
- # 第1轮:模型要求调 recall_memory
176
164
  [_block(type="tool_use", id="c1", name="recall_memory", input={"query": "偏好"})],
177
- # 第2轮:拿到结果后给最终答案
178
165
  [_block(type="text", text="根据记忆,你喜欢灰色。")],
179
166
  ])
180
167
  r = _router(client)
@@ -187,7 +174,6 @@ def test_reply_with_tools_anthropic_loop():
187
174
  out = CognitiveAgent._reply_with_tools(
188
175
  agent, "我喜欢什么颜色", [{"role": "user", "content": "我喜欢什么颜色"}])
189
176
  assert out == "根据记忆,你喜欢灰色。"
190
- # 第2轮的 messages 必须含 anthropic 格式 assistant(tool_use) + user(tool_result)
191
177
  kw2 = client.calls[1]
192
178
  has_tool_result = any(
193
179
  isinstance(m.get("content"), list)
@@ -21,9 +21,8 @@ def show(title, data, limit=None):
21
21
 
22
22
 
23
23
  def test_api():
24
- # 环境变量须在 import foxclaw.api 前设好(模块加载时按环境变量建网关/agent)
25
24
  os.environ["FOXCLAW_SHADOW_ROUNDS"] = "6"
26
- os.environ["FOXCLAW_DATA_DIR"] = tempfile.mkdtemp(prefix="foxclaw_test_") # 不污染仓库根
25
+ os.environ["FOXCLAW_DATA_DIR"] = tempfile.mkdtemp(prefix="foxclaw_test_")
27
26
  from foxclaw.api import app
28
27
 
29
28
  with TestClient(app) as c:
@@ -31,7 +30,6 @@ def test_api():
31
30
  print("FOXCLAW · API 端到端验证 (ASGI TestClient)")
32
31
  print("="*64)
33
32
 
34
- # 1. 系统状态
35
33
  r = c.get("/api/status")
36
34
  print(f"\n[GET /api/status] HTTP {r.status_code}")
37
35
  data = r.json()
@@ -39,7 +37,6 @@ def test_api():
39
37
  tc = s["thought_chain"]
40
38
  print(f" {aid:<12} 节点={tc['total_nodes']:<3} shadow={s['harness']['shadow_mode']}")
41
39
 
42
- # 2. 发任务(真实经过 ThoughtChain + Harness)
43
40
  print("\n[POST /api/task] 连发 7 个财务任务触发校准")
44
41
  for i, t in enumerate(["分析支出","制定预算","评估风险","季度报告","追踪消费","明年规划","同期对比"], 1):
45
42
  r = c.post("/api/task", json={"task": t, "agent_id": "finance"})
@@ -47,11 +44,9 @@ def test_api():
47
44
  err = d.get("error", "ok")
48
45
  print(f" [{i}] {t} → HTTP {r.status_code} · {err}")
49
46
 
50
- # 3. Harness 状态(校准后)
51
47
  r = c.get("/api/harness/finance/status")
52
48
  show("GET /api/harness/finance/status", r.json())
53
49
 
54
- # 4. 推理链
55
50
  r = c.get("/api/thoughtchain/finance/chain")
56
51
  nodes = r.json()["nodes"]
57
52
  print(f"\n[GET /api/thoughtchain/finance/chain] HTTP {r.status_code} · {len(nodes)} 节点")
@@ -59,25 +54,40 @@ def test_api():
59
54
  print(f" {n['type']:<16} {n['harness_status']:<9} "
60
55
  f"conf={n['conf']:.2f} Δ={n.get('harness_delta') or 0:.3f}")
61
56
 
62
- # 5. 漂移趋势
63
57
  r = c.get("/api/thoughtchain/finance/trend?limit=10")
64
58
  trend = r.json()["trend"]
65
59
  print(f"\n[GET .../trend] {len(trend)} 数据点 · Δ 序列:")
66
60
  print(" " + " ".join(f"{t['delta']:.2f}" for t in trend[:10]))
67
61
 
68
- # 6. Policy
69
62
  r = c.get("/api/harness/finance/policy")
70
63
  pol = r.json()
71
64
  print(f"\n[GET /api/harness/finance/policy] v{pol['version']} · "
72
65
  f"{len(pol.get('rules',[]))} 规则")
73
66
 
74
- # 7. 偏好管理端点(F3):GET 列出结构;DELETE 不存在 id 优雅返回 ok=False(不崩)
75
67
  r = c.get("/api/preferences")
76
68
  assert r.status_code == 200 and "preferences" in r.json(), r.text
77
69
  r = c.delete("/api/preferences/nonexistent_id")
78
70
  assert r.status_code == 200 and r.json().get("ok") is False, r.text
79
71
  print("\n[GET/DELETE /api/preferences] 列出+删除通路 ok")
80
72
 
73
+ r = c.put("/api/profile", json={"nickname": " snow "})
74
+ assert r.status_code == 200 and r.json().get("nickname") == "snow", r.text
75
+ r = c.get("/api/profile")
76
+ assert r.json().get("nickname") == "snow", r.text
77
+ c.put("/api/profile", json={"nickname": "snow2"})
78
+ r = c.get("/api/preferences")
79
+ nicks = [p["content"] for p in r.json()["preferences"] if "称呼我为" in p["content"]]
80
+ assert nicks == ["用户偏好:称呼我为 snow2"], nicks
81
+ print("\n[GET/PUT /api/profile] 称呼写入/读回/替换 ok")
82
+
83
+ r = c.get("/api/system/info")
84
+ info = r.json()
85
+ assert r.status_code == 200 and "update" in info, r.text
86
+ up = info["update"]
87
+ assert {"enabled", "current", "latest", "update_available"} <= set(up), up
88
+ assert up["enabled"] is False and up["update_available"] is False, up
89
+ print("\n[GET /api/system/info] update 字段存在,默认关闭")
90
+
81
91
  print("\n" + "="*64)
82
92
  print("✓ API 端到端验证完成 · 所有端点返回真实数据")
83
93
  print("="*64)
@@ -10,12 +10,10 @@ import os
10
10
  import sys
11
11
  from pathlib import Path
12
12
  sys.path.insert(0, str(Path(__file__).parent.parent))
13
- # 本测试要精确控制 FOXCLAW_API_KEY 的有/无;若 .env 带真实 key 会污染判定
14
- # (见 foxclaw/api.py 中 FOXCLAW_SKIP_DOTENV 的说明),故关掉 .env。
15
13
  os.environ["FOXCLAW_SKIP_DOTENV"] = "1"
16
14
 
17
15
  import tempfile
18
- os.environ["FOXCLAW_DATA_DIR"] = tempfile.mkdtemp(prefix="foxclaw_test_") # 不污染仓库根
16
+ os.environ["FOXCLAW_DATA_DIR"] = tempfile.mkdtemp(prefix="foxclaw_test_")
19
17
  from fastapi.testclient import TestClient
20
18
  from foxclaw.api import app
21
19
 
@@ -44,7 +42,6 @@ def main():
44
42
  "正确 key → 200")
45
43
 
46
44
  print("\n【3】WebSocket 鉴权(端点内 ?key=,http 中间件管不到 ws)")
47
- # 无 key → 握手被拒(端点 close 1008)
48
45
  rejected = False
49
46
  try:
50
47
  with c.websocket_connect("/ws/harness"):
@@ -52,7 +49,6 @@ def main():
52
49
  except Exception:
53
50
  rejected = True
54
51
  check(rejected, "WS 无 key → 握手被拒")
55
- # 带 ?key= → 连上
56
52
  connected = False
57
53
  try:
58
54
  with c.websocket_connect("/ws/harness?key=secret-123"):