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
@@ -20,7 +20,7 @@ from pathlib import Path
20
20
  sys.path.insert(0, str(Path(__file__).parent.parent))
21
21
 
22
22
  import foxclaw.core.web_search as ws
23
- ws.needs_search = lambda q: False # 关联网:离线测试不碰网络
23
+ ws.needs_search = lambda q: False
24
24
 
25
25
  from foxclaw import Foxclaw
26
26
  from foxclaw.core.thought_chain import ThoughtNode
@@ -43,7 +43,7 @@ def _slow(system, messages, max_tokens=2048, agent_id=""):
43
43
 
44
44
  def _build(data_dir=None):
45
45
  os_ = Foxclaw(data_dir=data_dir or tempfile.mkdtemp())
46
- os_.router.complete = _slow # 注入固定延迟,让并发/串行差异可测
46
+ os_.router.complete = _slow
47
47
  return os_
48
48
 
49
49
 
@@ -55,12 +55,10 @@ def os_(tmp_path_factory):
55
55
 
56
56
  @pytest.mark.xfail(
57
57
  reason="pre-existing 失败:HKC_EMBEDDING=stub 下 LLM 全部本地 stub 调用极快,并行线程/await 开销反而把并行拖慢于顺序,断言 par<seq*0.75 在 stub 环境不可靠。test_hkc_index_build_manager 等 P1 测试仍覆盖冷构建核心。",
58
- strict=False, # 若日后通过/在某些环境通过,不强制转 fail,允许绿
58
+ strict=False,
59
59
  )
60
60
  def test_fork_join(os_):
61
61
  print("\n【1】并行 fork-join vs 顺序")
62
- # 深审硬化:register_subagents 已在 setup 期预建 _scheduler/_steering_queue,
63
- # 并发任务到达前属性就绪 → 消除懒建 check-then-act race
64
62
  check(hasattr(os_.supervisor, "_scheduler") and hasattr(os_.supervisor, "_steering_queue"),
65
63
  "主管 setup 期预建 scheduler/steering_queue(消除并发懒建 race)")
66
64
  r = asyncio.run(os_.supervisor.run_task(PAR_TASK))
@@ -83,7 +81,7 @@ def test_fork_join(os_):
83
81
  def test_fault_isolation(os_):
84
82
  print("\n【2】故障隔离:分支超时不拖垮整批")
85
83
  r = asyncio.run(os_.supervisor.run_task(
86
- PAR_TASK, context={"parallel_timeout": 0.05})) # 远小于 0.4s 的 stub
84
+ PAR_TASK, context={"parallel_timeout": 0.05}))
87
85
  res = r["result"]
88
86
  check(res["dispatched_to"] == "parallel", "仍走并行")
89
87
  steps = res["parallel_steps"]
@@ -126,7 +124,6 @@ def test_concurrency_safety(os_):
126
124
  check(cnt >= N * M, f"ThoughtChain 并发写不丢({cnt} ≥ {N*M})")
127
125
  st = pm.stats()
128
126
  check(st["by_state"].get("running", 0) == 0, "ProcessManager 无残留 running(状态机并发一致)")
129
- # os_ 为多个子测试共享,故用 ≥(本测试贡献 N*M 个 completed,叠加此前的)
130
127
  check(st["by_state"].get("completed", 0) >= N * M, f"≥{N*M} 进程 completed(并发流转一致)")
131
128
  check(len(seen) == N * M, f"SystemBus 事件回调零丢失({len(seen)}/{N*M})")
132
129
 
@@ -152,7 +149,6 @@ def test_memory_isolation_concurrent(os_):
152
149
  for t in ts: t.start()
153
150
  for t in ts: t.join()
154
151
 
155
- # 从 office 视角召回"保密草稿":只能拿到自己的 + 共享,绝不能泄露 finance/admin 私有
156
152
  res = hmr.recall(query="保密草稿", agent_id="office", top_k=10)
157
153
  def foreign_private(r):
158
154
  tags = r.get("tags", [])
@@ -174,9 +170,6 @@ def test_gatekeeper_no_race(os_):
174
170
 
175
171
  def fire():
176
172
  try:
177
- # 金额取 5000:**远离**任何阈值边界。本测试验的是"并发裁决无竞态",
178
- # 与阈值定多少无关 —— 原先写 500 恰好卡在 large_purchase_check 边界上
179
- # (规则是 > 500,500 不大于 500),调整默认阈值就把它误伤了。
180
173
  req = CrossAgentRequest(
181
174
  from_agent="admin", to_agent="finance", request_type="budget_request",
182
175
  payload={"amount": 5000, "category": "travel"}, reason="差旅")
@@ -192,7 +185,6 @@ def test_gatekeeper_no_race(os_):
192
185
  for t in ts: t.join()
193
186
 
194
187
  check(not errs, f"并发守门人裁决无异常(否则: {errs[:2]})")
195
- # 大额确定性触发 large_purchase_check → 全部一致(不写死具体阈值:那是可调项)
196
188
  check(set(results) == {"needs_confirmation"},
197
189
  f"{N} 次并发裁决结果一致(无竞态): {set(results)}")
198
190
  check(len(orch.request_log) - before == N, f"请求日志无丢失(+{N})")
@@ -32,7 +32,7 @@ class FakeAgent:
32
32
  def __init__(self, nodes=None, fail_on=None):
33
33
  self.thought_chain = _FakeChain(nodes or [])
34
34
  self.exec_count = 0
35
- self._fail_on = fail_on # 第 N 次 _execute 抛错(模拟重试期间 BLOCK/出错)
35
+ self._fail_on = fail_on
36
36
  def _recall_query(self, task): return f"q:{task}"
37
37
  def _recall_memory(self, query, top_k=5):
38
38
  self.last_recall = (query, top_k)
@@ -57,7 +57,6 @@ def test_default_pipeline():
57
57
  assert ctx.perceived.get("task_len") == 1, "perceive 记录输入特征"
58
58
  assert ctx.plan.get("multi_step") is False and ctx.route.get("target") == "self"
59
59
  assert ctx.verified.get("passed") is True, "verify 汇总校验结果到 ctx.verified"
60
- # 默认 reflect:接受,不重试(behavior-preserving) → _execute 只调 1 次
61
60
  assert ctx.reflect["decision"] == "accept" and ctx.reflect["retries"] == 0
62
61
  assert a.exec_count == 1, "默认不重试,_execute 只调一次"
63
62
  print(f" ✓ recall→plan→route→act→verify→reflect 按序;plan={ctx.plan['flow']} "
@@ -67,12 +66,10 @@ def test_default_pipeline():
67
66
  def test_plan_route_stage():
68
67
  print("\n【1c】plan/route stage:P1-2 引擎分析结构(informational,不改执行)")
69
68
  from foxclaw.pipeline import stage_plan, stage_route
70
- # 多步任务(顺序连接词)
71
69
  ctx = PipelineContext(agent=FakeAgent(), task="先分析支出 然后 给出建议")
72
70
  stage_plan(ctx); stage_route(ctx)
73
71
  assert ctx.plan["multi_step"] is True and len(ctx.plan["steps"]) == 2, ctx.plan
74
72
  assert ctx.plan["flow"] == "sequential" and ctx.route["target"] == "self"
75
- # 单步任务
76
73
  ctx2 = PipelineContext(agent=FakeAgent(), task="分析我的开支")
77
74
  stage_plan(ctx2)
78
75
  assert ctx2.plan["multi_step"] is False and ctx2.plan["steps"] == ["分析我的开支"]
@@ -94,7 +91,7 @@ def test_insertable_stage():
94
91
  print("\n【2】可插桩:插入自定义 stage 按位置执行")
95
92
  hits = []
96
93
  def stage_probe(ctx):
97
- hits.append(("probe", len(ctx.memories))) # 在 recall 之后、act 之前
94
+ hits.append(("probe", len(ctx.memories)))
98
95
  stage_probe.stage_name = "probe"
99
96
  a = FakeAgent()
100
97
  ctx = PipelineContext(agent=a, task="X")
@@ -133,14 +130,13 @@ def test_stage_events():
133
130
  print("\n【6】逐 stage 事件:每个 stage emit agent.cognition.<stage> 到脊柱")
134
131
  spine = _CapSpine()
135
132
  a = FakeAgent()
136
- a._caps = _Caps(spine) # 注入能力中枢(解析出脊柱)
133
+ a._caps = _Caps(spine)
137
134
  ctx = PipelineContext(agent=a, task="E", session_id="s3")
138
135
  asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(ctx))
139
136
  expected = ["agent.cognition." + n for n in
140
137
  ["perceive", "recall", "plan", "route", "act", "verify", "reflect"]]
141
138
  assert spine.events == expected, f"逐 stage 事件,实得 {spine.events}"
142
- # 无中枢时优雅跳过(不崩)
143
- a2 = FakeAgent() # 无 _caps
139
+ a2 = FakeAgent()
144
140
  asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(PipelineContext(agent=a2, task="X")))
145
141
  print(" ✓ 7 个 agent.cognition.* 事件按序 emit;无中枢时优雅跳过")
146
142
 
@@ -149,33 +145,25 @@ def test_route_skill_optin_inert():
149
145
  print("\n【7】A·Step5:route 默认自动选 skill(仅只读+高阈值);可关;有副作用的不自动选")
150
146
  from foxclaw.pipeline import stage_route
151
147
  from foxclaw.skills import build_default_registry
152
- reg = build_default_registry() # 含 calculator(触发词「计算」等)
148
+ reg = build_default_registry()
153
149
 
154
- # (a) 【契约变更 2026-07-16】原为"默认关"(behavior-preserving 放量期)。
155
- # 现默认**开**:此前 route/dispatch 双关导致工具中心/MCP/技能在正常对话里根本够不着
156
- # (「帮我算 3+5」只会 LLM 硬答)。现默认自动选中**只读**技能。
157
150
  a = FakeAgent(); a.skill_registry = reg
158
151
  ctx = PipelineContext(agent=a, task="帮我计算 (12+8)*5")
159
152
  stage_route(ctx)
160
153
  assert ctx.route["target"] == "skill" and ctx.route["skill_id"] == "calculator", \
161
154
  f"默认应自动选中只读的 calculator: {ctx.route}"
162
155
 
163
- # (a2) 可关(显式 False / 环境变量)→ 回落 self,老行为随时可拿回
164
156
  ctx_off = PipelineContext(agent=a, task="帮我计算 (12+8)*5",
165
157
  context={"_route_skill": False})
166
158
  stage_route(ctx_off)
167
159
  assert ctx_off.route["target"] == "self", ctx_off.route
168
160
 
169
- # (b) 开启(显式阈值 0.5,避免耦合具体分值):计算类 → router 选中 calculator,target=skill
170
161
  ctx2 = PipelineContext(agent=a, task="帮我计算 (12+8)*5",
171
162
  context={"_route_skill": {"threshold": 0.5}})
172
163
  stage_route(ctx2)
173
164
  assert ctx2.route["target"] == "skill" and ctx2.route["skill_id"] == "calculator", ctx2.route
174
165
  assert ctx2.route["confidence"] >= 0.5, ctx2.route
175
166
 
176
- # (c) 【契约变更】原断言"act 不消费 route"(Step1 铺线期)。Step2 起 act 会真派发;
177
- # 此处验证:选中 skill 但**无 skill_adapter**(FakeAgent 没挂)→ _act_via_skill 回落
178
- # _execute,fail-safe 不崩。
179
167
  a3 = FakeAgent(); a3.skill_registry = reg
180
168
  ctx3 = PipelineContext(agent=a3, task="帮我计算 (12+8)*5",
181
169
  context={"_route_skill": {"threshold": 0.5}})
@@ -184,13 +172,11 @@ def test_route_skill_optin_inert():
184
172
  assert ctx3.result == "done:帮我计算 (12+8)*5#1" and a3.exec_count == 1, \
185
173
  "无 skill_adapter → 回落 _execute(fail-safe)"
186
174
 
187
- # (d) 无 skill_registry 的 Agent(finance/admin/office)→ 恒 self(fail-safe 回落)
188
175
  a4 = FakeAgent()
189
176
  ctx4 = PipelineContext(agent=a4, task="计算 1+1", context={"_route_skill": True})
190
177
  stage_route(ctx4)
191
178
  assert ctx4.route["target"] == "self", "无 skill_registry → 回落 self"
192
179
 
193
- # (e) 高阈值(>1)→ 实际不选,回落 self(阈值可调证明)
194
180
  ctx5 = PipelineContext(agent=a, task="帮我计算 1+1",
195
181
  context={"_route_skill": {"threshold": 1.01}})
196
182
  stage_route(ctx5)
@@ -209,7 +195,6 @@ def test_act_skill_dispatch_step2():
209
195
  a = FakeAgent(); a.skill_registry = reg; a.skill_adapter = adapter
210
196
  return a
211
197
 
212
- # (a) 开启 dispatch + 计算类 → 真正执行 calculator,结果=100,且领域 _execute(LLM) 不被调
213
198
  a = mk_agent()
214
199
  ctx = PipelineContext(agent=a, task="帮我计算 (12+8)*5",
215
200
  context={"_route_skill": {"threshold": 0.5}, "_skill_dispatch": True})
@@ -219,9 +204,6 @@ def test_act_skill_dispatch_step2():
219
204
  assert ctx.result["output"]["result"] == 100, ctx.result
220
205
  assert a.exec_count == 0, "skill 执行了 → 领域 _execute(LLM) 不再被调(首个真改执行)"
221
206
 
222
- # (b) 【安全铁律】开启 dispatch + 发邮件(destructive/send)未确认 → needs_confirmation,零副作用、不回落 LLM
223
- # 注:默认的只读闸会挡住 gmail_send(有副作用不自动选),故此处**显式** read_only=False
224
- # 表示"我就是要测有副作用的路由被 PolicyGate 拦住"。
225
207
  a2 = mk_agent()
226
208
  ctx2 = PipelineContext(agent=a2, task="帮我发送邮件给客户",
227
209
  context={"_route_skill": {"threshold": 0.5, "read_only": False},
@@ -231,37 +213,31 @@ def test_act_skill_dispatch_step2():
231
213
  assert ctx2.result["status"] == "needs_confirmation" and ctx2.result["pending"] is True, ctx2.result
232
214
  assert a2.exec_count == 0, "未确认的危险 skill → 不执行、不回落 LLM(绝不 fail-open)"
233
215
 
234
- # (c) 显式关 dispatch(只 route)→ 即便 target=skill,act 仍走 _execute(两道门相互独立)
235
- # 注:dispatch 现在**默认开**,故此处显式 False 才能验证"关掉"这条路径。
236
216
  a3 = mk_agent()
237
217
  ctx3 = PipelineContext(agent=a3, task="帮我计算 (12+8)*5",
238
218
  context={"_route_skill": {"threshold": 0.5}, "_skill_dispatch": False})
239
219
  asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(ctx3))
240
220
  assert ctx3.route["target"] == "skill" and a3.exec_count == 1, "dispatch 关 → 回落 _execute"
241
221
 
242
- # (d) 开 dispatch 但 Agent 无 skill_adapter → 回落 _execute(fail-safe)
243
- a4 = FakeAgent(); a4.skill_registry = reg # 有 registry 无 adapter
222
+ a4 = FakeAgent(); a4.skill_registry = reg
244
223
  ctx4 = PipelineContext(agent=a4, task="帮我计算 1+1",
245
224
  context={"_route_skill": {"threshold": 0.5}, "_skill_dispatch": True})
246
225
  asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(ctx4))
247
226
  assert a4.exec_count == 1, "无 adapter → 回落 _execute"
248
227
 
249
- # (b2) 【新安全闸·A·Step5】默认(只读闸开)下,发邮件这类有副作用的技能**根本不会被自动选中**
250
- # —— 自动执行不得越过"会改变世界"的线;要用仍可显式 use_skill(那时用户是知情的)。
251
228
  a2b = mk_agent()
252
- ctx2b = PipelineContext(agent=a2b, task="帮我发送邮件给客户") # 全默认
229
+ ctx2b = PipelineContext(agent=a2b, task="帮我发送邮件给客户")
253
230
  asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(ctx2b))
254
231
  assert ctx2b.route["target"] == "self", f"有副作用的技能不该被自动选中: {ctx2b.route}"
255
232
  assert a2b.exec_count == 1, "未自动动手 → 由 LLM 作答"
256
233
 
257
- # (e) 【fail-closed 加固】invoke 意外抛错 → 结构化 error,不崩、不回落 LLM
258
234
  class _BoomAdapter:
259
235
  def invoke(self, skill, args, confirmed=False, mode="strict"):
260
236
  raise RuntimeError("dispatch 内部炸了")
261
237
  a5 = FakeAgent(); a5.skill_registry = reg; a5.skill_adapter = _BoomAdapter()
262
238
  ctx5 = PipelineContext(agent=a5, task="帮我计算 1+1",
263
239
  context={"_route_skill": {"threshold": 0.5}, "_skill_dispatch": True})
264
- asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(ctx5)) # 不抛异常
240
+ asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(ctx5))
265
241
  assert ctx5.result["status"] == "error" and "异常" in ctx5.result["message"], ctx5.result
266
242
  assert a5.exec_count == 0, "dispatch 异常 → fail-closed 结构化 error,绝不回落 LLM"
267
243
  print(" ✓ 开启:calculator 真执行(=100,不调LLM);发邮件未确认→needs_confirmation零副作用;"
@@ -303,35 +279,28 @@ def test_run_task_confirm_suspend_step3():
303
279
  ag = _Mini(hmr=_FakeHMR(), thought_chain=ThoughtChain(db_path=f"{d}/tc.db"),
304
280
  data_dir=d, shadow_rounds=5)
305
281
  ag.skill_registry = reg; ag.skill_adapter = adapter
306
- # read_only=False:本测试专门验证**写入类** skill 的 confirm 挂起路径,
307
- # 而默认的只读闸不会自动选中它 —— 故显式声明(调用方自担风险的显式选择)。
308
282
  base_ctx = {"_route_skill": {"threshold": 0.4, "read_only": False},
309
283
  "_skill_dispatch": True}
310
284
 
311
- # 未确认:写 skill → run_task 顶层 status=needs_confirmation + pending_action,零副作用
312
285
  r1 = asyncio.run(ag.run_task("帮我记一下 买牛奶", dict(base_ctx)))
313
286
  assert r1.get("status") == "needs_confirmation", r1
314
287
  assert r1.get("pending_action", {}).get("skill_id") == "save_note", r1
315
288
  assert saved == [], "未确认 → handler 不执行(零副作用)"
316
289
 
317
- # 【深审加固】确认绑定错 skill_id → 仍 needs_confirmation、不执行(防确认漂移)
318
290
  r_bad = asyncio.run(ag.run_task("帮我记一下 买牛奶",
319
291
  {**base_ctx, "_confirmed": "gmail_send"}))
320
292
  assert r_bad.get("status") == "needs_confirmation", r_bad
321
293
  assert saved == [], "确认绑定到别的 skill → 不执行本 skill(绑定优先)"
322
294
 
323
- # 确认恢复:绑定到正确 skill_id 重跑 → skill 真执行,无 needs_confirmation
324
295
  r2 = asyncio.run(ag.run_task("帮我记一下 买牛奶",
325
296
  {**base_ctx, "_confirmed": "save_note"}))
326
297
  assert r2.get("status") != "needs_confirmation", r2
327
298
  assert len(saved) == 1, f"确认后 handler 执行(有副作用),saved={saved}"
328
299
 
329
- # 向后兼容:_confirmed=True 概括确认仍生效
330
300
  saved.clear()
331
301
  r2b = asyncio.run(ag.run_task("帮我记一下 买酸奶", {**base_ctx, "_confirmed": True}))
332
302
  assert r2b.get("status") != "needs_confirmation" and len(saved) == 1, (r2b, saved)
333
303
 
334
- # 纯 LLM 路径(dispatch 关)→ 无 status 字段(行为逐字不变)
335
304
  r3 = asyncio.run(ag.run_task("随便聊聊"))
336
305
  assert "status" not in r3 and r3["result"] == "llm:随便聊聊", r3
337
306
  print(f" ✓ 未确认→顶层 needs_confirmation 零副作用;_confirmed 重跑→执行(saved={saved});"
@@ -343,7 +312,7 @@ def test_run_task_confirm_suspend_step3():
343
312
  def test_reflect_retry_optin():
344
313
  print("\n【4】reflect opt-in:漂移超阈值 → 有上限重试")
345
314
  from foxclaw.pipeline import stage_verify # noqa
346
- a = FakeAgent(nodes=[_Node("WARN", 0.8)]) # verify 得 max_delta=0.8 > 阈值 0.5
315
+ a = FakeAgent(nodes=[_Node("WARN", 0.8)])
347
316
  ctx = PipelineContext(agent=a, task="V",
348
317
  context={"_reflect": {"max_retries": 2, "drift_threshold": 0.5}})
349
318
  asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(ctx))
@@ -358,8 +327,8 @@ class _DynChain:
358
327
  def __init__(self, agent): self.agent = agent
359
328
  def get_chain(self, session_id, agent_id=None):
360
329
  if self.agent.exec_count <= 1:
361
- return [_Node("WARN", 0.8)] # 初次:高漂移
362
- return [_Node("WARN", 0.8), _Node("PASS", 0.2)] # 重试后:旧高(0.8) + 新低(0.2)
330
+ return [_Node("WARN", 0.8)]
331
+ return [_Node("WARN", 0.8), _Node("PASS", 0.2)]
363
332
 
364
333
 
365
334
  def test_reflect_early_exit_on_improve():
@@ -369,10 +338,8 @@ def test_reflect_early_exit_on_improve():
369
338
  ctx = PipelineContext(agent=a, task="D",
370
339
  context={"_reflect": {"max_retries": 3, "drift_threshold": 0.5}})
371
340
  asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(ctx))
372
- # 初次 exec=1 → last_delta=0.8>0.5 → 重试1 exec=2 → last_delta=0.2<=0.5 → 早退(非到上限3)
373
341
  assert a.exec_count == 2, f"改善后应只重试 1 次(而非到上限 3),实得 {a.exec_count}"
374
342
  assert ctx.reflect["decision"] == "retry_accepted" and ctx.reflect["retries"] == 1, ctx.reflect
375
- # 关键:此时 max_delta 仍是 0.8(旧节点),若用它会错误地重试到上限;last_delta=0.2 才对
376
343
  assert ctx.verified["max_delta"] == 0.8 and ctx.verified["last_delta"] == 0.2
377
344
  print(f" ✓ 早退:exec={a.exec_count} reflect={ctx.reflect['decision']} "
378
345
  f"(max_delta={ctx.verified['max_delta']} 仍高,但 last_delta={ctx.verified['last_delta']} 达标)")
@@ -380,7 +347,7 @@ def test_reflect_early_exit_on_improve():
380
347
 
381
348
  def test_reflect_failsafe_rollback():
382
349
  print("\n【5】reflect fail-safe:重试自身出错 → 回滚到重试前结果")
383
- a = FakeAgent(nodes=[_Node("WARN", 0.8)], fail_on=2) # 第 2 次 _execute(首个重试)抛错
350
+ a = FakeAgent(nodes=[_Node("WARN", 0.8)], fail_on=2)
384
351
  ctx = PipelineContext(agent=a, task="X",
385
352
  context={"_reflect": {"max_retries": 2, "drift_threshold": 0.5}})
386
353
  asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(ctx))
@@ -401,10 +368,10 @@ def test_concurrent_ctx_isolation():
401
368
 
402
369
  class SlowAgent(FakeAgent):
403
370
  async def _execute(self, task, context, session_id, memories):
404
- await asyncio.sleep(0.005) # 让出控制权 → 制造交错 → 暴露串状态
371
+ await asyncio.sleep(0.005)
405
372
  return f"done:{task}", {"task_echo": task}
406
373
 
407
- a = SlowAgent() # 同一 agent 实例,被 24 个任务并发打
374
+ a = SlowAgent()
408
375
 
409
376
  async def run_one(task):
410
377
  ctx = PipelineContext(agent=a, task=task, session_id=f"s_{task}")
@@ -434,40 +401,35 @@ def test_cooperative_interrupt():
434
401
  import threading
435
402
  print("\n【7】P3 协作式中断:stop_event 在 stage 边界 / 重试迭代前优雅停")
436
403
 
437
- # (a) 无 stop_event → 跑满 7 stage、interrupted=False(behavior-preserving)
438
404
  a = FakeAgent()
439
405
  ctx = PipelineContext(agent=a, task="T")
440
406
  asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(ctx))
441
407
  assert ctx.interrupted is False and len(ctx.trace) == 7, (ctx.interrupted, ctx.trace)
442
408
 
443
- # (b) 起步即置位 → 0 个 stage 执行、interrupted=True、无结果
444
409
  ev = threading.Event(); ev.set()
445
410
  ctx2 = PipelineContext(agent=FakeAgent(), task="T", stop_event=ev)
446
411
  asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(ctx2))
447
412
  assert ctx2.interrupted is True and ctx2.trace == [] and ctx2.result is None, \
448
413
  (ctx2.interrupted, ctx2.trace, ctx2.result)
449
414
 
450
- # (c) 中途置位(自定义 stage 跑完就 set)→ 停在下一边界,后续 stage 全不跑
451
415
  ev2 = threading.Event()
452
416
  def _tripwire(c): ev2.set()
453
417
  _tripwire.stage_name = "tripwire"
454
- stages = DEFAULT_STAGES[:2] + [_tripwire] + DEFAULT_STAGES[2:] # perceive,recall,tripwire,plan...
418
+ stages = DEFAULT_STAGES[:2] + [_tripwire] + DEFAULT_STAGES[2:]
455
419
  ctx3 = PipelineContext(agent=FakeAgent(), task="T", stop_event=ev2)
456
420
  asyncio.run(CognitivePipeline(stages).run(ctx3))
457
421
  assert ctx3.trace == ["perceive", "recall", "tripwire"], ctx3.trace
458
422
  assert ctx3.interrupted is True and ctx3.result is None, (ctx3.interrupted, ctx3.result)
459
423
  print(f" ✓ 边界中断:无 stop 跑满 7;起步停 0 stage;中途停 trace={ctx3.trace}(后续未跑)")
460
424
 
461
- # (d) reflect 重试循环中断:agent 在**首次重试的 _execute** 时置位 → 下一轮迭代前停,
462
- # 证明重试在用尽 max_retries(=5)之前被截断。这是本管线最接近"agentic loop 每轮检查中断"处。
463
425
  ev3 = threading.Event()
464
426
  class _StopOnRetryAgent(FakeAgent):
465
427
  def __init__(self, e):
466
- super().__init__(nodes=[_Node("WARN", 0.8)]) # 恒高漂移 → 不中断会一直重试到上限
428
+ super().__init__(nodes=[_Node("WARN", 0.8)])
467
429
  self._e = e
468
430
  async def _execute(self, task, context, session_id, memories):
469
431
  self.exec_count += 1
470
- if self.exec_count == 2: # 第 1 次重试的 LLM 调用时请求中断
432
+ if self.exec_count == 2:
471
433
  self._e.set()
472
434
  return f"done:{task}#{self.exec_count}", {"call": self.exec_count}
473
435
  a4 = _StopOnRetryAgent(ev3)
@@ -485,18 +447,15 @@ def test_cooperative_interrupt():
485
447
  def test_on_stage_callback():
486
448
  """【8】D1:`pipeline.run(on_stage)` 逐 stage 回调(opt-in);无回调=行为不变;回调异常 fail-safe。"""
487
449
  print("\n【8】D1 on_stage 回调:逐 stage 通知 + opt-in + fail-safe")
488
- # (a) 有回调:每 stage 完成后回调一次,顺序与 trace 一致
489
450
  seen = []
490
451
  ctx = PipelineContext(agent=FakeAgent(), task="T")
491
452
  asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(
492
453
  ctx, on_stage=lambda name, c: seen.append(name)))
493
454
  assert seen == ctx.trace == ["perceive", "recall", "plan", "route", "act", "verify", "reflect"], \
494
455
  (seen, ctx.trace)
495
- # (b) 无回调:行为逐字不变(默认路径)
496
456
  ctx2 = PipelineContext(agent=FakeAgent(), task="T")
497
457
  asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(ctx2))
498
458
  assert len(ctx2.trace) == 7 and ctx2.result == "done:T#1"
499
- # (c) 回调抛异常:被吞,不影响管线跑完(fail-safe:流式反馈绝不拖垮任务)
500
459
  def _boom(name, c):
501
460
  raise RuntimeError("回调故障")
502
461
  ctx3 = PipelineContext(agent=FakeAgent(), task="T")
@@ -44,12 +44,12 @@ def test_stage_plan_records_plan_node() -> None:
44
44
 
45
45
 
46
46
  def test_record_plan_node_never_breaks_execution() -> None:
47
- _record_plan_node(_ctx("x", agent=None)) # 无 _think → 静默跳过
47
+ _record_plan_node(_ctx("x", agent=None))
48
48
 
49
49
  def _boom(**kwargs):
50
50
  raise RuntimeError("harness 挂了")
51
51
 
52
- _record_plan_node(_ctx("x", agent=types.SimpleNamespace(_think=_boom))) # 不得抛出
52
+ _record_plan_node(_ctx("x", agent=types.SimpleNamespace(_think=_boom)))
53
53
 
54
54
 
55
55
  def test_plan_payload_accepts_both_shapes() -> None:
@@ -59,7 +59,7 @@ def test_plan_payload_accepts_both_shapes() -> None:
59
59
  "multi_step": True}
60
60
  sup = types.SimpleNamespace(plan_obj=types.SimpleNamespace(steps=["x", "y"], flow="parallel"))
61
61
  assert _plan_payload(sup)["steps"] == ["x", "y"]
62
- assert _plan_payload(sup)["multi_step"] is True # 未声明 → 按步数推断
62
+ assert _plan_payload(sup)["multi_step"] is True
63
63
  one = types.SimpleNamespace(plan_obj=types.SimpleNamespace(steps=["x"]))
64
64
  assert _plan_payload(one)["multi_step"] is False
65
65
  assert _plan_payload(types.SimpleNamespace(plan=None)) is None
@@ -72,7 +72,7 @@ def test_stream_emits_plan_event_after_plan_stage() -> None:
72
72
 
73
73
  async def _core(task, context=None, stop_event=None, on_stage=None):
74
74
  on_stage("recall", ctx)
75
- on_stage("plan", ctx) # 管线在 plan stage 完成后回调
75
+ on_stage("plan", ctx)
76
76
  return {"result": "done"}
77
77
 
78
78
  fake = types.SimpleNamespace(_run_task_core=_core)
@@ -38,33 +38,26 @@ def test_trigger_route_engine():
38
38
  C("web", ["搜索", "查"], "联网 搜索 网页"),
39
39
  C("email", ["发邮件", "email"], "发送 邮件"),
40
40
  ]
41
- # 否定:"不要计算" → calculator 触发被否定 → 不选它
42
41
  r = P.trigger_route("不要计算一下", cands)
43
42
  assert r.primary != "calculator", f"否定应排除 calculator,实得 {r.primary}"
44
- # 命中:"帮我搜索北京" → web
45
43
  r2 = P.trigger_route("帮我搜索北京", cands)
46
44
  assert r2.primary == "web" and r2.confidence >= 0.15
47
- # 兜底:完全不相关 → 用最后一个候选兜底,低置信
48
45
  r3 = P.trigger_route("xyzzy 无关内容", cands)
49
46
  assert r3.primary == cands[-1].id and r3.confidence < 0.15
50
- # 确定性:空候选
51
47
  assert P.trigger_route("x", []).primary is None
52
48
  print(f" ✓ 否定排除 / 命中选中 / 兜底(={r3.primary}) / 空候选")
53
49
 
54
50
 
55
51
  def test_three_sites_delegate():
56
52
  print("\n【3】三处委托同一引擎(单一真源)")
57
- # AHS
58
53
  from foxclaw.skills import build_default_registry
59
54
  from foxclaw.skills.pipeline import route, plan
60
55
  reg = build_default_registry()
61
56
  assert route("不要计算", reg).primary != "calculator", "AHS route 否定生效"
62
57
  assert len(plan("先算 5+5 然后再算 9*9", reg).tasks) == 2, "AHS plan 拆解"
63
- # search
64
58
  from foxclaw.search.planner import QueryPlanner
65
59
  sub = QueryPlanner()._decompose("查A,查B、查C")
66
60
  assert len(sub) == 3, "search 拆解走共享原语"
67
- # supervisor(用引擎的 keyword_hits;此处直接验证原语等价)
68
61
  assert P.keyword_hits("买基金理财", ["理财", "基金", "收支"]) == 2
69
62
  print(" ✓ AHS / search / 主管 三处均由 foxclaw.planning 单一真源驱动")
70
63
 
@@ -17,7 +17,7 @@ sys.path.insert(0, str(Path(__file__).parent.parent))
17
17
  from foxclaw.kernel import PolicyGate
18
18
 
19
19
 
20
- class _HR: # 假 HarnessResult
20
+ class _HR:
21
21
  def __init__(self, blocked=False, warned=False, reason=""):
22
22
  self._b, self._w, self.reason = blocked, warned, reason
23
23
  @property
@@ -31,7 +31,7 @@ class _Harness:
31
31
  def check(self, node): self.calls += 1; return self.hr
32
32
 
33
33
 
34
- class _PD: # 假 PermissionDecision
34
+ class _PD:
35
35
  def __init__(self, decision, reason=""):
36
36
  self.decision, self.stage, self.reason, self.trust_downgrade = decision, 2, reason, False
37
37
 
@@ -81,7 +81,6 @@ def test_integration_skill_path():
81
81
  os_ = Foxclaw(data_dir=d, shadow_rounds=50)
82
82
  seen = []
83
83
  os_.event_spine.project(lambda e: seen.append(e.topic))
84
- # 发邮件是副作用 skill → 需确认 → 经 PolicyGate 发 policy.decision.confirm
85
84
  os_.supervisor.use_skill("帮我发邮件给客户",
86
85
  args={"to": "a@b.com", "subject": "x", "body": "y"})
87
86
  assert any(t.startswith("policy.decision") for t in seen), \
@@ -0,0 +1,67 @@
1
+ """称呼(user profile)存取:round-trip、改一次不留旧条目、可清空、不误删别的偏好。
2
+
3
+ 用 stub 宿主直接测 mixin,不初始化 Agent / API(那条路径由 test_api 覆盖)。
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from foxclaw.agents.mixins.preference import PreferenceMixin
9
+
10
+
11
+ class _Stub(PreferenceMixin):
12
+ AGENT_ID = "stub"
13
+
14
+ def __init__(self):
15
+ self.items: list[dict] = []
16
+ self._seq = 0
17
+
18
+ def _store_memory(self, content, mem_type="concept", **kwargs):
19
+ self._seq += 1
20
+ self.items.insert(0, {"id": f"m{self._seq}", "content": content})
21
+
22
+ def list_preferences(self, limit=50):
23
+ return self.items[:limit]
24
+
25
+ def delete_preference(self, pref_id):
26
+ before = len(self.items)
27
+ self.items = [p for p in self.items if p["id"] != pref_id]
28
+ return len(self.items) < before
29
+
30
+
31
+ def test_nickname_roundtrip():
32
+ s = _Stub()
33
+ assert s.get_nickname() == ""
34
+ assert s.set_nickname(" snow ") == "snow"
35
+ assert s.get_nickname() == "snow"
36
+
37
+
38
+ def test_set_nickname_replaces_old():
39
+ """改称呼不能留下两条互相矛盾的偏好。"""
40
+ s = _Stub()
41
+ s.set_nickname("snow")
42
+ s.set_nickname("snow2")
43
+ assert [s._nick_of(p["content"]) for p in s.items] == ["snow2"]
44
+ assert s.get_nickname() == "snow2"
45
+
46
+
47
+ def test_clear_nickname():
48
+ s = _Stub()
49
+ s.set_nickname("snow")
50
+ assert s.set_nickname("") == ""
51
+ assert s.get_nickname() == ""
52
+ assert s.items == []
53
+
54
+
55
+ def test_other_preferences_untouched():
56
+ s = _Stub()
57
+ s.remember_preference("回答尽量简短")
58
+ s.set_nickname("snow")
59
+ contents = [p["content"] for p in s.items]
60
+ assert "用户偏好:回答尽量简短" in contents
61
+ assert "用户偏好:称呼我为 snow" in contents
62
+
63
+
64
+ def test_nickname_truncated():
65
+ s = _Stub()
66
+ long = "x" * 60
67
+ assert s.set_nickname(long) == long[:24]