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
@@ -5,9 +5,13 @@
5
5
  import os
6
6
 
7
7
  from ._context import _taint_raise
8
+ from typing import Any, Callable
8
9
 
9
10
 
10
11
  class AugmentMixin:
12
+ _think: Callable[..., Any]
13
+ available_abilities: Any
14
+ _call_llm: Callable[..., Any]
11
15
  def _web_augment(self, query: str, messages: list[dict]) -> list[dict]:
12
16
  """若问题含时效性,联网搜索并把结果注入 messages(放在用户问题前)。
13
17
  DeepSeek/OpenAI 等基础 API 不自带联网,这里用百度 AI 搜索补上。失败则原样返回。"""
@@ -19,9 +23,6 @@ class AugmentMixin:
19
23
  payload = _ws.search_answer(query)
20
24
  if not payload or (not payload.get("answer") and not payload.get("references")):
21
25
  print("[联网搜索] ⚠ 无结果(未配 BAIDU_SEARCH_KEY 或网络不通)")
22
- # 需要联网但没搜到(未配 key / 网络不通)→ 注入明确指令逼 LLM 如实说,绝不编造。
23
- # 否则 LLM 被 system_prompt 告知"已接入联网"却拿不到结果,会凭训练数据瞎编版本号/
24
- # 新闻/价格(用户实测踩到)。合并进最后一条 user 消息(与下方注入同法,不新增 user 消息)。
25
26
  note = ("【系统提示:本次问题需要实时/最新信息,但联网搜索未返回任何结果"
26
27
  "(可能未配置搜索服务或网络不可用)。请如实告知用户你暂时无法查询实时信息,"
27
28
  "**切勿编造或凭记忆给出可能过时的答案**(如版本号、新闻、模型列表、价格等)。】")
@@ -39,9 +40,6 @@ class AugmentMixin:
39
40
  content=f"联网搜索「{query}」命中,参考 {n} 条",
40
41
  node_type="tool_use", conf=0.9,
41
42
  )
42
- # 合并式注入:把联网上下文并进「最后一条 user 消息」,而不是前插一条独立
43
- # user 消息 —— 后者会产生连续两条 user,某些 OpenAI-compatible 端点会拒绝,
44
- # 与「模型无关」铁律冲突。拷贝后改,不动原 messages。
45
43
  merged = list(messages)
46
44
  for i in range(len(merged) - 1, -1, -1):
47
45
  if merged[i].get("role") == "user":
@@ -49,14 +47,11 @@ class AugmentMixin:
49
47
  m["content"] = ctx + "\n\n" + (m.get("content") or "")
50
48
  merged[i] = m
51
49
  return merged
52
- return [{"role": "user", "content": ctx}] + merged # 无 user 消息则退回前插
50
+ return [{"role": "user", "content": ctx}] + merged
53
51
  except Exception as e:
54
52
  print(f"[联网搜索] 异常: {e}")
55
53
  return messages
56
54
 
57
- # 注:_model_augment 已删除 —— 模型身份注入规则化到 ModelRouter._runtime_context,
58
- # 在 LLM 唯一咽喉(complete/stream)对每次调用恒定注入真实 provider/model,
59
- # 不再靠"模型/大模型/llm"关键词命中才注入,也就没有了漏判/误判两难。
60
55
 
61
56
  def _ability_augment_enabled(self) -> bool:
62
57
  """能力知识增强是否启用。默认**关**(behavior-preserving)。
@@ -89,17 +84,16 @@ class AugmentMixin:
89
84
  if not hits:
90
85
  return messages
91
86
  lines = "\n".join(f"- {h.name}: {h.summary}" for h in hits[:5])
92
- # 知识由摄入的外部文档蒸馏而来,可能被投毒 → 包裹为不可信数据,防间接注入
93
87
  from ...core import injection_guard as _ig
94
88
  ctx = (f"【能力知识增强 · {best.get('display_name') or best['ability_key']}】"
95
89
  f"以下是知识库中与本任务相关、且该能力已达覆盖的知识,供参考:\n"
96
90
  + _ig.wrap_untrusted(lines, source="knowledge"))
97
91
  self._think(content=f"能力增强「{best['ability_key']}」注入 {len(hits)} 条知识",
98
92
  node_type="tool_use", conf=0.85)
99
- _taint_raise(self, _ig.taint_level("knowledge")) # 知识库(个人数据)→ 会话污点
93
+ _taint_raise(self, _ig.taint_level("knowledge"))
100
94
  return [{"role": "user", "content": ctx}] + messages
101
95
  except Exception:
102
- return messages # fail-safe:增强失败绝不影响主流程
96
+ return messages
103
97
 
104
98
  def _skill_augment(self, query: str, messages: list[dict]) -> list[dict]:
105
99
  """按触发词匹配 SKILL.md 指令技能,把正文注入(合并进最后一条 user 消息)。"""
@@ -107,7 +101,7 @@ class AugmentMixin:
107
101
  reg = getattr(self, "md_skills", None)
108
102
  if not reg:
109
103
  return messages
110
- matched = reg.match(query)[:3] # 触发词匹配,最多3条控 token
104
+ matched = reg.match(query)[:3]
111
105
  if not matched:
112
106
  return messages
113
107
  ctx = "以下是可参照的操作指令(SKILL),请据此执行:\n\n" + \
@@ -144,6 +138,6 @@ class AugmentMixin:
144
138
  避免堵死 async 事件循环(否则百度慢/超时时整个服务、前端轮询全部冻住)。"""
145
139
  import asyncio
146
140
  aug = await asyncio.to_thread(self._web_augment, query, messages)
147
- aug = await asyncio.to_thread(self._ability_augment, query, aug) # Step3:opt-in 能力知识注入(默认关)
148
- aug = await asyncio.to_thread(self._skill_augment, query, aug) # SKILL.md 指令技能注入
141
+ aug = await asyncio.to_thread(self._ability_augment, query, aug)
142
+ aug = await asyncio.to_thread(self._skill_augment, query, aug)
149
143
  return await self._call_llm(aug, max_tokens=max_tokens)
@@ -3,9 +3,13 @@
3
3
  从 `foxclaw/agents/base.py` 搬移出来的 `_call_llm`。
4
4
  """
5
5
  from ._context import _reasoning_inject, _act_stream
6
+ from typing import Any
6
7
 
7
8
 
8
9
  class LlmMixin:
10
+ AGENT_ID: str
11
+ router: Any
12
+ system_prompt: str
9
13
  async def _call_llm(
10
14
  self,
11
15
  messages: list[dict],
@@ -28,9 +32,6 @@ class LlmMixin:
28
32
  import asyncio
29
33
  inj = _reasoning_inject.get()
30
34
  if inj:
31
- # 合并进**最后一条 user 消息**(与 _web_augment 同法),而非前插独立 user 消息——
32
- # 后者会产生连续两条 user,某些 OpenAI-compatible 端点会拒绝,违反「模型无关」铁律
33
- # (B4 审核 A3)。拷贝后改,不动原 messages。
34
35
  merged = list(messages)
35
36
  for i in range(len(merged) - 1, -1, -1):
36
37
  if merged[i].get("role") == "user":
@@ -38,7 +39,7 @@ class LlmMixin:
38
39
  merged[i] = m
39
40
  break
40
41
  else:
41
- merged = [{"role": "user", "content": str(inj)}] + merged # 无 user 消息才独立前插
42
+ merged = [{"role": "user", "content": str(inj)}] + merged
42
43
  messages = merged
43
44
  sc = _act_stream.get()
44
45
  if sc is not None:
@@ -54,13 +55,13 @@ class LlmMixin:
54
55
  try:
55
56
  on_token(chunk)
56
57
  except Exception:
57
- pass # 单个 token 回调故障不中断收集
58
+ pass
58
59
  return "".join(parts)
59
60
 
60
61
  try:
61
62
  return await asyncio.to_thread(_stream_collect)
62
63
  except Exception:
63
- pass # 流式整体故障 → 落到下方非流式 complete(fail-safe)
64
+ pass
64
65
  return await asyncio.to_thread(
65
66
  self.router.complete,
66
67
  self.system_prompt, messages, max_tokens, self.AGENT_ID)
@@ -2,10 +2,16 @@
2
2
 
3
3
  从 `foxclaw/agents/base.py` 搬移出来的记忆相关方法。
4
4
  """
5
+ from typing import Any, Callable
6
+
5
7
  from ._context import _suppress_memory
6
8
 
7
9
 
8
10
  class MemoryMixin:
11
+ AGENT_ID: str
12
+ hmr: Any
13
+ _think: Callable[..., Any]
14
+
9
15
  def _recall_memory(self, query: str, top_k: int = 5) -> list:
10
16
  """从 HMR 召回相关记忆,记录到 ThoughtChain"""
11
17
  from foxclaw.core.tracing import span as _span
@@ -24,7 +30,7 @@ class MemoryMixin:
24
30
  def _store_memory(self, content: str, mem_type: str = "concept", **kwargs):
25
31
  """存储新记忆到 HMR"""
26
32
  if _suppress_memory.get():
27
- return # MCP 外部驱动的任务:不写记忆(防污染)
33
+ return
28
34
  from foxclaw.core.tracing import span as _span
29
35
  with _span("memory.store", agent=self.AGENT_ID, type=mem_type):
30
36
  self.hmr.ingest(
@@ -3,10 +3,14 @@
3
3
  从 `foxclaw/agents/base.py` 搬移出来的偏好相关方法。
4
4
  """
5
5
  import os
6
+ from typing import Any, Callable
6
7
 
7
8
 
8
9
  class PreferenceMixin:
9
- # 注入近期偏好(不管当前 query),让"越用越懂你"真生效。默认开;FOXCLAW_PREF_CAPTURE=0 关采集。
10
+ AGENT_ID: str
11
+ hmr: Any
12
+ _think: Callable[..., Any]
13
+ _store_memory: Callable[..., Any]
10
14
  _PREF_MARKERS = ("我喜欢", "我不喜欢", "我讨厌", "我希望", "别再", "不要再", "以后都",
11
15
  "以后请", "记住", "我的偏好", "我习惯", "下次请", "下次记得", "我倾向",
12
16
  "请务必", "请一律", "总是用", "我要求")
@@ -20,16 +24,12 @@ class PreferenceMixin:
20
24
  t = (task or "").strip()
21
25
  if not t or not any(k in t for k in self._PREF_MARKERS):
22
26
  return
23
- # 偏好通道消毒(防投毒):偏好每轮以"一律以此为准"的权威口吻注入,且无法像外部数据那样
24
- # 包 <untrusted>(它本就该被遵循)。所以只能在**存入侧**拦——若这条"偏好"候选里夹带注入/
25
- # 越权特征(如用户把含"忽略以上指令/系统覆盖"的文档粘进对话),就不固化,断掉投毒源头。
26
27
  from ...core import injection_guard as _ig
27
28
  if _ig.scan(t):
28
29
  self._think(content="偏好候选含疑似注入特征,已拒绝固化(防记忆投毒)",
29
30
  node_type="reflection", conf=0.9)
30
31
  return
31
32
  try:
32
- # 用 tag 标记(不受 memory_type 白名单降级);shared=True 跨会话跨 Agent 持久
33
33
  self._store_memory(f"用户偏好:{t[:200]}", mem_type="preference",
34
34
  tags=["preference"], shared=True)
35
35
  except Exception:
@@ -44,9 +44,6 @@ class PreferenceMixin:
44
44
  return ""
45
45
  if not prefs:
46
46
  return ""
47
- # prefs 已按"新→旧"排序(recall_by_tag)。偏好会随时间改变(如"最喜欢的颜色"从蓝改灰),
48
- # 语义相似度无法区分同话题偏好(实测"颜色"与"回答风格"偏好都 ~0.9),故不能靠自动删旧;
49
- # 改由指令让 LLM 以"最新的(最靠前)/当前对话最近说法"为准,避免被已更新的旧偏好带偏。
50
47
  lines = "\n".join(f"- {p.get('content', '')}" for p in prefs)
51
48
  return ("【用户偏好(按最近→最早排列)】\n" + lines +
52
49
  "\n**注意:偏好会随时间改变。若上面几条彼此冲突,或与当前对话中用户最近的说法不一致,"
@@ -79,3 +76,56 @@ class PreferenceMixin:
79
76
  return self.hmr.delete_memory(pref_id)
80
77
  except Exception:
81
78
  return False
79
+
80
+ _NICK_PREFIXES = ("称呼我为", "叫我", "我的名字是", "我的名字叫")
81
+
82
+ def remember_preference(self, text: str) -> bool:
83
+ """写一条用户主动声明的偏好(设置面板 / 首次引导用)。返回是否落库。
84
+
85
+ 与 `_maybe_store_preference`(对话里自动抓)的区别:这条不经过注入扫描,
86
+ 因为来源是设置表单而非自由文本;但同样限长 200,避免把整段话塞进偏好。
87
+ """
88
+ t = (text or "").strip()
89
+ if not t:
90
+ return False
91
+ try:
92
+ self._store_memory(f"用户偏好:{t[:200]}", mem_type="preference",
93
+ tags=["preference"], shared=True)
94
+ return True
95
+ except Exception:
96
+ return False
97
+
98
+ @classmethod
99
+ def _nick_of(cls, content: str) -> str | None:
100
+ """从一条 preference 文本里取出称呼;不是称呼类返回 None。"""
101
+ c = (content or "").strip()
102
+ if c.startswith("用户偏好:"):
103
+ c = c[len("用户偏好:"):].strip()
104
+ for prefix in cls._NICK_PREFIXES:
105
+ if c.startswith(prefix):
106
+ return c[len(prefix):].strip(" ::,,。.")
107
+ return None
108
+
109
+ def get_nickname(self) -> str:
110
+ """当前称呼(取最近一条称呼类偏好;没设置过返回空串)。"""
111
+ for p in self.list_preferences(limit=200):
112
+ nick = self._nick_of(p.get("content", ""))
113
+ if nick:
114
+ return nick
115
+ return ""
116
+
117
+ def set_nickname(self, nick: str) -> str:
118
+ """设置称呼;传空串 = 清除。返回生效后的值。
119
+
120
+ 先把旧的称呼条目删掉再写:否则每改一次就多一条「称呼我为 X」,注入时
121
+ 几条互相矛盾(虽然 prompt 里约定「以最新为准」,但没必要让它去猜)。
122
+ """
123
+ value = (nick or "").strip()[:24]
124
+ for p in self.list_preferences(limit=200):
125
+ if self._nick_of(p.get("content", "")):
126
+ pid = p.get("id")
127
+ if pid:
128
+ self.delete_preference(pid)
129
+ if value:
130
+ self.remember_preference(f"称呼我为 {value}")
131
+ return value
@@ -7,11 +7,6 @@ import os
7
7
  from abc import ABC, abstractmethod
8
8
 
9
9
 
10
- # ── 内核系统提示词(所有 Agent 共享前缀)────────────────────────────────────────
11
- # 见 docs/internal/FOXCLAW_SYSTEM_PROMPT.md。融合"认知操作系统"身份 + 系统性原理
12
- # (真相优先 / Principle 07 / 价值记忆)+ 安全边界(指令/数据隔离、最小权限、人在回路)。
13
- # 一处定义,base.system_prompt 统一叠加到各 Agent 角色职责之前 → 一处改全体生效。
14
- # §4 的 <untrusted> 规则与 core/injection_guard.wrap_untrusted 的打标配套,缺一不可。
15
10
  KERNEL_PROMPT = """你是 Foxclaw —— 一个面向 Agent 时代的认知操作系统,不是一次性问答机器人。
16
11
  你的使命:让信息成为知识,让知识成为能力;守护用户的知识火种,保持认知的连续性。
17
12
  以下是所有 Agent 共享的内核准则;你的具体角色职责见随后的说明。
@@ -59,7 +54,6 @@ KERNEL_PROMPT = """你是 Foxclaw —— 一个面向 Agent 时代的认知操
59
54
 
60
55
 
61
56
  class PromptMixin(ABC):
62
- # ── 子类必须实现 ───────────────────────────────────────
63
57
 
64
58
  @property
65
59
  @abstractmethod
@@ -4,8 +4,12 @@
4
4
  助手降级为指令技能后,工具箱不能再绑在某个 Agent 上 —— 否则主管/财务/办公全都用不了技能,
5
5
  而"技能"恰恰是那两个下放的 Agent 的新家。故上移到 mixin,由基类统一混入。
6
6
  """
7
+ from typing import Any, Callable
8
+
7
9
 
8
10
  class SkillBoxMixin:
11
+ _session_id: str | None
12
+ _think: Callable[..., Any]
9
13
  def attach_skills(self, registry, adapter):
10
14
  """挂载 skill 接入层(注册表 + 执行适配器)。"""
11
15
  self.skill_registry = registry
@@ -16,19 +20,18 @@ class SkillBoxMixin:
16
20
  reg = getattr(self, "skill_registry", None)
17
21
  return reg.catalog() if reg else []
18
22
 
19
- def use_skill(self, task: str, args: dict = None, confirmed: bool = False) -> dict:
23
+ def use_skill(self, task: str, args: dict | None = None, confirmed: bool = False) -> dict:
20
24
  """按任务匹配 skill 并调用,结果回填 ThoughtChain,过守门人。"""
21
25
  reg = getattr(self, "skill_registry", None)
22
26
  adp = getattr(self, "skill_adapter", None)
23
27
  if not (reg and adp):
24
28
  return {"error": "skill 接入层未挂载"}
25
- # 用升级版 router(三步打分 + 否定词检测),优于老的 reg.match 纯子串匹配
26
29
  try:
27
30
  from ...skills.pipeline import route as ahs_route
28
31
  r = ahs_route(task, reg)
29
32
  skill = reg.get(r.primary) if r.primary else None
30
33
  except Exception:
31
- skill = reg.match(task) # 兜底
34
+ skill = reg.match(task)
32
35
  if not skill:
33
36
  return {"matched": None, "message": "未匹配到合适的 skill"}
34
37
  self._think(
@@ -40,9 +43,6 @@ class SkillBoxMixin:
40
43
  from ._context import _taint_get, _untrusted_get
41
44
  token = set_loop_session(self._session_id)
42
45
  try:
43
- # 会话已取回敏感数据(知识库/记忆/网页)后,技能的外发/副作用动作同样要二次确认——
44
- # 此前技能路径不带污点,permission.evaluate 的 Stage 3.5 只对工具生效。
45
- # 来源不可信(外部聊天渠道)时按同一口径抬到 TAINT_PERSONAL。
46
46
  _taint = _taint_get(self)
47
47
  if _untrusted_get(self):
48
48
  _taint = max(_taint, _ig.TAINT_PERSONAL)
@@ -15,8 +15,8 @@ import time
15
15
  from ._context import _taint_get, _taint_raise, _untrusted_get
16
16
  from ...core import settings
17
17
  from ...core.injection_guard import redact_obj, redact_secrets
18
+ from typing import Any
18
19
 
19
- #: 同轮最多并发几个工具(再多也是网络等待,收益递减;避免打爆 provider/搜索服务)
20
20
  MAX_PARALLEL = 4
21
21
 
22
22
 
@@ -46,7 +46,7 @@ class _Spawn:
46
46
  def _run():
47
47
  try:
48
48
  self.box["v"] = fn()
49
- except BaseException as e: # 统一交主线程降级成错误文本
49
+ except BaseException as e:
50
50
  self.box["e"] = e
51
51
  finally:
52
52
  self.done.set()
@@ -83,29 +83,19 @@ def _guard_tool(agent, name: str, args: dict):
83
83
  _reg = _tools.default_registry()
84
84
  if not _reg.has(name):
85
85
  return f"(未知工具 {name},未执行)", {}
86
- # ── 参数校验(用注册表里同一份 JSON Schema)────────────────────
87
- # 修复前:schema 只发给模型,执行侧不校验 → 模型给错类型/漏参数会直接流到
88
- # 下游。且下方 permission Stage 1 因 params={} 恒不触发,只靠手工 if-not 兜。
89
- # 新增校验闸后,错误**回注模型**让其自修正,而不是静默用空参数继续
90
- # (那会让模型误以为工具已执行,进而编造基于"工具结果"的结论)。
91
86
  _errs = _reg.validate_args(name, args or {})
92
87
  if _errs:
93
88
  agent._think(content=f"工具 {name} 入参校验失败: {'; '.join(_errs)[:120]}",
94
89
  node_type="tool_intent", conf=0.9)
95
90
  return (f"(工具 {name} 入参不合法: {'; '.join(_errs)}。"
96
91
  f"请按工具声明修正参数后重试。)"), {}
97
- # ── 权限管线:动作级闸门。构造 tool-spec 交四阶段裁决 ──
98
92
  cap = _tools.tool_capability(name)
99
- # 传入真实 params schema,使 permission Stage 1(必填校验)真正生效,
100
- # 不再依赖下游手工检查兜底。兼容 permission 的两种 params 格式。
101
93
  _schema = _reg.param_schema(name)
102
94
  _req = set(_schema.get("required") or [])
103
95
  _params = {k: {"required": k in _req, "type": v.get("type")}
104
96
  for k, v in (_schema.get("properties") or {}).items()
105
97
  if isinstance(v, dict)}
106
98
  spec = _types.SimpleNamespace(id=f"tool:{name}", params=_params, **cap)
107
- # 来源不可信(外部聊天渠道 / 远程驱动)→ 按「已接触敏感数据」处理:写/发送/外发强制 HITL。
108
- # 只读工具不受影响(污点分支只作用于副作用动作),所以读操作不会被这一条拖慢。
109
99
  _taint = _taint_get(agent)
110
100
  if _untrusted_get(agent):
111
101
  _taint = max(_taint, _ig.TAINT_PERSONAL)
@@ -115,7 +105,6 @@ def _guard_tool(agent, name: str, args: dict):
115
105
  if decision.decision == "deny":
116
106
  return f"(工具 {name} 被权限策略拒绝:{decision.reason},未执行)", cap
117
107
  if decision.decision == "ask":
118
- # 红线/副作用/未声明工具 → 人在回路:不自动执行,交回由用户确认(KERNEL §6 同旨)
119
108
  agent._think(content=f"工具 {name} 需用户确认({decision.reason}),已暂停执行",
120
109
  node_type="decision", conf=0.9)
121
110
  return (f"(工具 {name} 属于需确认的动作:{decision.reason}。按安全策略我不会自动执行——"
@@ -144,19 +133,17 @@ def _invoke_tool(agent, name: str, args: dict, taint_sink: list | None = None) -
144
133
  q = str((args or {}).get("query", "")).strip()
145
134
  if not q:
146
135
  return "(web_search 缺少 query 参数)"
147
- if _ws.is_personal_query(q): # 私人账本/行程/记忆 → 绝不外发,让模型用本地信息答
136
+ if _ws.is_personal_query(q):
148
137
  return "(该问题属于用户本地数据,已跳过联网;请依据本地领域信息回答)"
149
138
  payload = _ws.search_answer(q)
150
139
  if not payload or (not payload.get("answer") and not payload.get("references")):
151
140
  return ("联网搜索未返回结果(可能未配置搜索服务或网络不可用);"
152
141
  "请如实告知用户暂时无法查询实时信息,切勿编造版本号/新闻/价格等。")
153
142
  agent._think(content=f"工具 web_search「{q}」命中", node_type="tool_use", conf=0.9)
154
- _raise(_ig.taint_level("web_search")) # 外部不可控数据 → 会话污点
143
+ _raise(_ig.taint_level("web_search"))
155
144
  return _ws.format_for_prompt(q, payload)
156
145
  if name == "recall_memory":
157
146
  q = str((args or {}).get("query", "")).strip()
158
- # top_k 此前硬编码 5,模型传值无效 → 加边界校验后尊重模型意图
159
- # (schema 已声明 top_k,1-20;越界回落默认,防一次召回拉爆上下文)
160
147
  try:
161
148
  top_k = max(1, min(20, int((args or {}).get("top_k", 5))))
162
149
  except Exception:
@@ -165,8 +152,7 @@ def _invoke_tool(agent, name: str, args: dict, taint_sink: list | None = None) -
165
152
  if not res:
166
153
  return "(未召回到相关记忆)"
167
154
  lines = "\n".join(f"- {r.get('content', '')}" for r in res[:top_k])
168
- # 记忆可能被投毒(此前注入固化) → 当作不可信数据回注,防记忆投毒二次生效
169
- _raise(_ig.taint_level("memory")) # 个人记忆 → 会话污点(高敏感)
155
+ _raise(_ig.taint_level("memory"))
170
156
  return "【召回的相关记忆】\n" + _ig.wrap_untrusted(lines, source="memory")
171
157
  except Exception as e:
172
158
  return f"(工具 {name} 执行异常:{type(e).__name__})"
@@ -179,7 +165,7 @@ def _timeout_text(name: str) -> str:
179
165
 
180
166
 
181
167
  def _merge_taint(agent, sink: list) -> None:
182
- for lvl in sink: # 只在主线程动会话污点(单调取 max)
168
+ for lvl in sink:
183
169
  try:
184
170
  _taint_raise(agent, lvl)
185
171
  except Exception:
@@ -221,8 +207,6 @@ def _run_tools_batch(agent, calls: list) -> list:
221
207
  if p[2] is not None:
222
208
  out.append(p[2])
223
209
  continue
224
- # 串行走 agent._dispatch_tool(保留「实例级覆盖」这一扩展点,单测常这么替身);
225
- # 没有该方法时才直接执行。
226
210
  _disp = getattr(agent, "_dispatch_tool", None)
227
211
  out.append(_disp(p[0], p[1]) if callable(_disp)
228
212
  else _exec_guarded(agent, p[0], p[1]))
@@ -237,7 +221,7 @@ def _run_tools_batch(agent, calls: list) -> list:
237
221
  for n, sp, sink in spawns:
238
222
  left = max(0.0, timeout - (time.monotonic() - started))
239
223
  try:
240
- ok, val = sp.wait(left) # 各自独立计时:一个超时不拖累已完成的
224
+ ok, val = sp.wait(left)
241
225
  except Exception as e:
242
226
  out.append(f"(工具 {n} 执行异常:{type(e).__name__})")
243
227
  continue
@@ -249,7 +233,6 @@ def _run_tools_batch(agent, calls: list) -> list:
249
233
  return out
250
234
 
251
235
 
252
- #: checkpoint 里单步结果最多存多少字符(超出即标记 truncated,续跑时不复用、改为重跑)
253
236
  _STEP_RESULT_MAX = 4000
254
237
 
255
238
 
@@ -337,8 +320,6 @@ def _settle(agent, call: dict, result: str) -> str:
337
320
  text, kind = _apply_failure_policy(agent, call, result)
338
321
  try:
339
322
  cap = _tools.tool_capability(name)
340
- # 核验看的是**工具原始返回**,不是我们追加过提示的文本 —— 否则提示里的
341
- # "超时/未完成"字样会反过来影响判定。
342
323
  v = _verify.verify(name, args, result, cap)
343
324
  note = _verify.note_for_model(v, read_only=bool(cap.get("read_only")))
344
325
  if note:
@@ -347,12 +328,10 @@ def _settle(agent, call: dict, result: str) -> str:
347
328
  "tool": name, "kind": kind, "status": v.status,
348
329
  "reason": v.reason, "evidence": [e.to_dict() for e in v.evidence],
349
330
  })
350
- # 判定"可复用"以核验结论为准,而不是文本开头是不是 "(" ——
351
- # 失败文本追加提示后就不再以 "(" 开头,靠形态判会把它当成功存进断点。
352
331
  if v.status != _verify.FAILED and _step_ok(result):
353
332
  _record_step(agent, call, result)
354
333
  except Exception:
355
- pass # 核验/记账失败不影响工具结果本身
334
+ pass
356
335
  return text
357
336
 
358
337
 
@@ -381,8 +360,9 @@ def _run_tool_calls(agent, calls: list) -> list:
381
360
 
382
361
 
383
362
  class ToolExecMixin:
384
- # 以下四个方法均为薄包装:真实 agent 走 self.xxx;单测可用 SimpleNamespace 直接调
385
- # 模块级函数或 `CognitiveAgent._dispatch_tool(fake, ...)`(鸭子类型)。
363
+ AGENT_ID: str
364
+ router: Any
365
+ system_prompt: str
386
366
  def _guard_tool(self, name: str, args: dict):
387
367
  return _guard_tool(self, name, args)
388
368
 
@@ -414,30 +394,24 @@ class ToolExecMixin:
414
394
  return None
415
395
  conv = list(messages)
416
396
  for _ in range(max_rounds):
417
- # 上下文预算:工具轮每轮追加 assistant.tool_calls + tool 结果,不压会撑爆窗口
418
- # (Compactor 保证 tool_calls 与其结果同进同出,缺口见 core/compaction.py:100)。
419
- # getattr 兜底:本 mixin 也被只实现 router 的轻量对象复用(_compact_context
420
- # 定义在 CognitiveAgent 上),缺该方法时退化为不压缩。
421
397
  _compact = getattr(self, "_compact_context", None)
422
398
  if _compact is not None:
423
399
  conv = _compact(conv, "tool_loop")
424
400
  if stop_event is not None and stop_event.is_set():
425
- return None # 断连 → None(回退;fallback 也会因 stop 立即终止)
401
+ return None
426
402
  res = self.router.call_tools(self.system_prompt, conv, schemas,
427
403
  agent_id=self.AGENT_ID)
428
404
  if not res.get("supported"):
429
- return None # 端点实际不支持 tools → fallback
405
+ return None
430
406
  calls = res.get("tool_calls") or []
431
407
  if not calls:
432
- return res.get("text") or None # 模型直接给出最终答案;空答 → None → fallback(不再空回复不回退)
408
+ return res.get("text") or None
433
409
  conv.append(res["assistant_msg"])
434
- # 只读并发 / 写串行 / 单工具带超时 + 每步 checkpoint + 续跑复用已完成步骤
435
410
  results = _run_tool_calls(self, calls)
436
411
  for c, result in zip(calls, results):
437
412
  conv.append(self.router.format_tool_result(c, result))
438
- # 轮数超限 → 强制不再调工具、收敛出文本答案
439
413
  final = self.router.call_tools(self.system_prompt, conv, schemas,
440
414
  tool_choice="none", agent_id=self.AGENT_ID)
441
415
  if not final.get("supported"):
442
- return None # 收敛调用失败 → fallback(不再空回复不回退)
443
- return final.get("text") or None # 收敛出空答 → None → fallback
416
+ return None
417
+ return final.get("text") or None