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
@@ -15,6 +15,7 @@ import inspect
15
15
  import os
16
16
  from dataclasses import dataclass, field
17
17
  from typing import Any, Callable
18
+ from .stage_meta import named_stage
18
19
 
19
20
 
20
21
  @dataclass
@@ -27,32 +28,27 @@ class PipelineContext:
27
28
  反对的是"跨任务/并发共享可变状态"(改一半被读到不一致);ctx 不属于该反模式,可变安全。
28
29
  并发隔离由 test_pipeline.py::test_concurrent_ctx_isolation 锁定(24 并发任务零串)。
29
30
  (会话指针 _session_id/_parent_node_id 另经 contextvar 隔离,见 base.py。)"""
30
- agent: Any # 执行本任务的 Agent(CognitiveAgent)
31
+ agent: Any
31
32
  task: str
32
33
  context: dict = field(default_factory=dict)
33
34
  session_id: str = ""
34
- # 各 stage 逐步填充的产物
35
- perceived: dict = field(default_factory=dict) # perceive stage 的输入理解
35
+ perceived: dict = field(default_factory=dict)
36
36
  memories: list = field(default_factory=list)
37
- plan: dict = field(default_factory=dict) # plan stage 的任务结构(多步?并行?)
38
- route: dict = field(default_factory=dict) # route stage 的路由决策(target)
37
+ plan: dict = field(default_factory=dict)
38
+ route: dict = field(default_factory=dict)
39
39
  result: Any = None
40
40
  extra: dict = field(default_factory=dict)
41
- verified: dict = field(default_factory=dict) # verify stage 汇总的 Harness 校验结果
42
- reflect: dict = field(default_factory=dict) # reflect stage 的决策(accept/retry + 次数)
43
- trace: list = field(default_factory=list) # 逐 stage 轨迹(后续步骤 emit 用)
44
- # ── 协作式中断(P3,opt-in)──────────────────────────────
45
- stop_event: Any = None # 置位则在下一 stage 边界 / reflect 重试迭代前优雅停止;默认 None=永不停
46
- interrupted: bool = False # 被 stop_event 提前中断(供调用方识别"部分完成、未跑完全部 stage")
47
- # ── 【P1-1】主管路由专用字段(领域 Agent 不用,默认空)──────────
48
- # 主管 pipeline(supervisor_stages.SUPERVISOR_STAGES)在 schedule/route/plan/dispatch
49
- # 各 stage 间经这些字段传递路由决策;领域 Agent 的 DEFAULT_STAGES 从不读它们。
50
- sched: Any = None # schedule stage 的 ScheduleDecision
51
- target: str = "" # route stage 的分诊目标(子 Agent id)
52
- plan_obj: Any = None # plan stage 的 planner 拆解结果(多步计划)
53
- step_routes: list = field(default_factory=list) # [(子任务文本, 目标 Agent)]
54
- multi_agent: bool = False # 是否跨多 Agent(顺序链式 / 并行 fork-join)
55
- dispatch_result: dict = field(default_factory=dict) # dispatch stage 的原始分发返回
41
+ verified: dict = field(default_factory=dict)
42
+ reflect: dict = field(default_factory=dict)
43
+ trace: list = field(default_factory=list)
44
+ stop_event: Any = None
45
+ interrupted: bool = False
46
+ sched: Any = None
47
+ target: str = ""
48
+ plan_obj: Any = None
49
+ step_routes: list = field(default_factory=list)
50
+ multi_agent: bool = False
51
+ dispatch_result: dict = field(default_factory=dict)
56
52
 
57
53
 
58
54
  def _emit_stage(ctx: "PipelineContext", stage_name: str) -> None:
@@ -95,16 +91,10 @@ class CognitivePipeline:
95
91
  回调异常被吞(fail-safe):流式反馈绝不拖垮任务执行。"""
96
92
  from foxclaw.core.tracing import span as _span
97
93
  for stage in self.stages:
98
- # 协作式中断点(P3):stage **边界**检查 stop_event。opt-in——默认 stop_event=None 恒不触发,
99
- # 与改前逐字一致。置位时优雅停:标记 interrupted、保留已完成 stage 的 ctx 产物、不跑后续 stage。
100
- # 粒度=stage 边界:一般无法中止已在执行的单个 stage;但**流式路径(run_task_stream,D1)**下
101
- # act 的 LLM 生成经 `_act_stream` 的**同一** stop_event 串到 `router.stream`,可 **token 级**
102
- # 即时停 → 断连时二者协同:停后续 stage(此处)+ 停 act 生成(base._call_llm),省 token。
103
94
  if _stop_requested(ctx):
104
95
  ctx.interrupted = True
105
96
  break
106
97
  name = getattr(stage, "stage_name", getattr(stage, "__name__", "stage"))
107
- # 单一 stage 循环 → 一处埋点覆盖全部 stage(链路里能看到 perceive→…→reflect 各自耗时)
108
98
  with _span(f"stage.{name}", agent=getattr(ctx.agent, "AGENT_ID", "")):
109
99
  r = stage(ctx)
110
100
  if inspect.isawaitable(r):
@@ -113,15 +103,13 @@ class CognitivePipeline:
113
103
  _emit_stage(ctx, name)
114
104
  if on_stage is not None:
115
105
  try:
116
- on_stage(name, ctx) # D1:逐 stage 吐进度给流式路径
106
+ on_stage(name, ctx)
117
107
  except Exception:
118
- pass # 流式回调故障不拖垮任务执行(fail-safe)
108
+ pass
119
109
  return ctx
120
110
 
121
111
 
122
- # ── Stages(Step 1:recall / act)──────────────────────────────
123
- # stage 是操作 ctx 的可调用;命名 stage_* 便于事件与轨迹标注。
124
-
112
+ @named_stage("perceive")
125
113
  def stage_perceive(ctx: PipelineContext) -> None:
126
114
  """感知:把原始输入框定为结构化感知(Step 5)。当前记录轻量输入特征;
127
115
  这里是未来"意图识别/输入解析"的落点。也是逐 stage 事件链的起点。"""
@@ -129,14 +117,13 @@ def stage_perceive(ctx: PipelineContext) -> None:
129
117
  "task_len": len(ctx.task or ""),
130
118
  "has_context": bool(ctx.context),
131
119
  }
132
- stage_perceive.stage_name = "perceive"
133
120
 
134
121
 
122
+ @named_stage("recall")
135
123
  async def stage_recall(ctx: PipelineContext) -> None:
136
124
  """召回相关记忆(等价 P1-1 模板的 _recall_memory(_recall_query, top_k))。"""
137
125
  a = ctx.agent
138
126
  ctx.memories = a._recall_memory(a._recall_query(ctx.task), top_k=a._recall_topk)
139
- stage_recall.stage_name = "recall"
140
127
 
141
128
 
142
129
  def _plan_text(plan: dict) -> str:
@@ -152,7 +139,7 @@ def _record_plan_node(ctx: PipelineContext) -> None:
152
139
  计划节点不参与准入裁决,也不被 act 读取(见下方「informational」边界说明)。
153
140
  """
154
141
  agent = getattr(ctx, "agent", None)
155
- think = getattr(agent, "_think", None) # 轻量替身/测试 Agent 可能没有 _think
142
+ think = getattr(agent, "_think", None)
156
143
  if think is None:
157
144
  return
158
145
  try:
@@ -162,6 +149,7 @@ def _record_plan_node(ctx: PipelineContext) -> None:
162
149
  pass
163
150
 
164
151
 
152
+ @named_stage("plan")
165
153
  def stage_plan(ctx: PipelineContext) -> None:
166
154
  """结构规划:用 **P1-2 统一规划引擎** 分析任务结构(是否多步/并行),记录到 ctx.plan。
167
155
 
@@ -187,28 +175,10 @@ def stage_plan(ctx: PipelineContext) -> None:
187
175
  steps, flow = [task], "single"
188
176
  ctx.plan = {"steps": steps, "flow": flow, "multi_step": len(steps) > 1}
189
177
  _record_plan_node(ctx)
190
- stage_plan.stage_name = "plan"
191
- # ⚠【预留·informational,不驱动执行】(2026-07-17 V3·B1 审核标注)
192
- # stage_plan 写 ctx.plan 供观测/审计,但**没有任何 _execute 读取它**,故不改变执行
193
- # (act 整段交 LLM/skill)。让 act 真按 plan 多步执行是**行为改变**,须 opt-in + 每步
194
- # confirm 挂起 + fail-safe(见 V2_ROADMAP「Skill 接入认知管线」),不在此顺手做。
195
- # (B1 自审:曾在此加 `stage_plan.informational=True` 属性,但无任何消费者=又一个死标记,
196
- # 已删——注释足以说明,不造没人读的假结构化标记。)
197
-
198
-
199
- # 默认自动派发的门槛(A·Step5,2026-07-16:从"默认关"改为"默认开但严格受限")。
200
- # 背景:此前 route/dispatch 双双默认关 → 工具中心/MCP 连接器/技能台**在正常对话里根本够不着**
201
- # (「帮我算 3+5」助手只会用 LLM 硬答,计算器技能不会被调用)。整套工具生态形同虚设。
202
- # 放量策略:只自动派发**只读**技能、且要求**高置信**;有副作用的(write/send/pay/destructive)
203
- # 一律不自动选中,仍须显式 use_skill —— 自动执行绝不越过"会改变世界"的那条线。
204
- # 阈值取 0.6 —— 这是**量出来的**,不是拍的。router 实测分值只有三档:
205
- # 0.600 强匹配(触发词直接命中,如「帮我计算…」→calculator)
206
- # 0.480 弱匹配(如「查下去公司的路线」→amap_route、「发邮件给张三」→gmail_send)
207
- # 0.000 无匹配(闲聊/写代码)
208
- # 故 (0.48, 0.60] 区间内的任何值都等价于"只要强匹配";再高(如 0.75)则**一个都选不中**、
209
- # 功能形同虚设。0.6 已是最保守的可用值 —— "比 0.6 更保守"是伪命题。
178
+
179
+
210
180
  AUTO_ROUTE_THRESHOLD = 0.6
211
- AUTO_ROUTE_READ_ONLY = True # 只自动派发只读技能(有副作用的绝不自动动手)
181
+ AUTO_ROUTE_READ_ONLY = True
212
182
 
213
183
 
214
184
  def _is_read_only(skill) -> bool:
@@ -242,6 +212,7 @@ def _route_policy(agent, context: dict) -> dict:
242
212
  "read_only": bool(p.get("read_only", AUTO_ROUTE_READ_ONLY))}
243
213
 
244
214
 
215
+ @named_stage("route")
245
216
  def stage_route(ctx: PipelineContext) -> None:
246
217
  """路由决策:记录本任务由谁处理到 ctx.route(target=self 或 skill)。
247
218
 
@@ -260,33 +231,27 @@ def stage_route(ctx: PipelineContext) -> None:
260
231
  return
261
232
  reg = getattr(ctx.agent, "skill_registry", None)
262
233
  if reg is None:
263
- return # 无工具箱(finance/admin/office)→ 保持 self
234
+ return
264
235
  try:
265
236
  from ..skills.pipeline import route as skill_route
266
237
  r = skill_route(ctx.task, reg)
267
238
  except Exception:
268
- return # 选路失败不影响主流程 → 保持 self(fail-safe)
239
+ return
269
240
  if r.primary and r.confidence >= policy["threshold"]:
270
241
  sk = reg.get(r.primary)
271
- # 只读闸:有副作用的技能(write/send/pay/destructive)**绝不自动选中**——
272
- # 自动执行不得越过"会改变世界"的线;要用仍可显式 use_skill(那时用户是知情的)。
273
242
  if policy["read_only"] and sk is not None and not _is_read_only(sk):
274
- return # 保持 self → LLM 作答,不自动动手
275
- # 外泄闸:问的是**用户自己的数据**(账本/行程/学习)→ 绝不自动发给**外部**技能。
276
- # 与 needs_search 共用同一判定(单一真源)。否则「查一下最新的本月支出」会把私人
277
- # 财务问题自动送去外部搜索——那正是 needs_search 已挡住、却从技能这条旁路漏回来的 bug。
243
+ return
278
244
  if sk is not None and getattr(sk, "external", False):
279
245
  try:
280
246
  from ..core.web_search import is_personal_query
281
247
  if is_personal_query(ctx.task):
282
- return # 私人问题不外发 → 保持 self
248
+ return
283
249
  except Exception:
284
- pass # 判定不可用 → 不因此放行外部技能之外的行为(保持原逻辑)
250
+ pass
285
251
  ctx.route = {"target": "skill", "skill_id": r.primary,
286
252
  "confidence": round(float(r.confidence), 3),
287
253
  "reason": f"router 选中 skill「{r.primary}」"
288
254
  f"(置信 {r.confidence:.2f}≥{policy['threshold']})"}
289
- stage_route.stage_name = "route"
290
255
 
291
256
 
292
257
  def _skill_dispatch_enabled(agent, context: dict) -> bool:
@@ -339,21 +304,21 @@ async def _act_via_skill(ctx: "PipelineContext"):
339
304
  reg = getattr(agent, "skill_registry", None)
340
305
  adp = getattr(agent, "skill_adapter", None)
341
306
  if reg is None or adp is None:
342
- return None # 无工具箱(finance/admin/office)→ 回落 _execute
307
+ return None
343
308
  sid = (ctx.route or {}).get("skill_id")
344
- skill = reg.get(sid) if sid else None
309
+ if not isinstance(sid, str) or not sid:
310
+ return None
311
+ skill = reg.get(sid)
345
312
  if skill is None:
346
313
  return None
347
314
  from ..skills.pipeline import _build_args
348
315
  from ..skills import set_loop_session, reset_loop_session
349
316
  args = _build_args(skill, ctx.task, {})
350
317
  confirmed = _confirmed_for(ctx.context, sid)
351
- token = set_loop_session(getattr(ctx, "session_id", None)) # 循环检测按会话分槽
318
+ token = set_loop_session(getattr(ctx, "session_id", None))
352
319
  try:
353
320
  res = await asyncio.to_thread(adp.invoke, skill, args, confirmed)
354
321
  except Exception as e:
355
- # invoke 理应自兜底(catches _dispatch 异常);万一权限评估等意外抛错 → **fail-closed**:
356
- # 结构化 error 回传,**绝不回落 LLM**(路由到发邮件却让 LLM 代答=幻觉/危险),也不崩整个任务。
357
322
  return ({"skill": sid, "status": "error", "message": f"skill dispatch 异常: {e}",
358
323
  "pending": False}, {"via": "skill", "skill_id": sid, "skill_status": "error"})
359
324
  finally:
@@ -361,12 +326,11 @@ async def _act_via_skill(ctx: "PipelineContext"):
361
326
  extra = {"via": "skill", "skill_id": sid, "skill_status": res.status}
362
327
  if res.success:
363
328
  return ({"skill": sid, "output": res.output, "status": res.status}, extra)
364
- # 非成功:无副作用已发生(权限门+confirmed=False保证)。结构化回传,绝不当完成、绝不回落 LLM
365
- # (路由到发邮件却让 LLM 代答=危险/幻觉);needs_confirmation 待用户确认后重跑(带 _confirmed)。
366
329
  return ({"skill": sid, "status": res.status, "message": res.message,
367
330
  "pending": res.status == "needs_confirmation"}, extra)
368
331
 
369
332
 
333
+ @named_stage("act")
370
334
  async def stage_act(ctx: PipelineContext) -> None:
371
335
  """领域执行步骤。默认:领域 `_execute`(→ LLM),等价 P1-1 模板。
372
336
 
@@ -381,9 +345,9 @@ async def stage_act(ctx: PipelineContext) -> None:
381
345
  return
382
346
  ctx.result, ctx.extra = await ctx.agent._execute(
383
347
  ctx.task, ctx.context, ctx.session_id, ctx.memories)
384
- stage_act.stage_name = "act"
385
348
 
386
349
 
350
+ @named_stage("verify")
387
351
  def stage_verify(ctx: PipelineContext) -> None:
388
352
  """把本次执行的 Harness 校验**结果**显式化到 ctx.verified(Step 2)。
389
353
 
@@ -399,19 +363,15 @@ def stage_verify(ctx: PipelineContext) -> None:
399
363
  except Exception:
400
364
  nodes = []
401
365
  warns = sum(1 for n in nodes if getattr(n, "harness_status", "") == "WARN")
402
- deltas = [n.harness_delta for n in nodes # get_chain 按 timestamp 升序
366
+ deltas = [n.harness_delta for n in nodes
403
367
  if getattr(n, "harness_delta", None) is not None]
404
368
  ctx.verified = {
405
- "passed": True, # 未被 BLOCK
369
+ "passed": True,
406
370
  "nodes": len(nodes),
407
371
  "warns": warns,
408
- "max_delta": round(max(deltas), 3) if deltas else 0.0, # 全 session 峰值(信息量)
409
- # last_delta = 最近一次推理的漂移(本次尝试的最终状态)。reflect 用它作重试信号——
410
- # 深审修复:max_delta 是 max over 累积历史(单调不减),重试追加节点后仍是旧峰值,
411
- # 会让重试永远到上限、无法因"重试改善"提前退出;last_delta 反映当前尝试,可正确早退。
372
+ "max_delta": round(max(deltas), 3) if deltas else 0.0,
412
373
  "last_delta": round(deltas[-1], 3) if deltas else 0.0,
413
374
  }
414
- stage_verify.stage_name = "verify"
415
375
 
416
376
 
417
377
  def _reflect_policy(agent, context: dict) -> dict:
@@ -426,10 +386,11 @@ def _reflect_policy(agent, context: dict) -> dict:
426
386
  if p is True:
427
387
  p = {}
428
388
  return {"enabled": True,
429
- "max_retries": int(p.get("max_retries", 1)), # 硬上限,防成环
389
+ "max_retries": int(p.get("max_retries", 1)),
430
390
  "drift_threshold": float(p.get("drift_threshold", 0.5))}
431
391
 
432
392
 
393
+ @named_stage("reflect")
433
394
  async def stage_reflect(ctx: PipelineContext) -> None:
434
395
  """反思:读 verify 信号(max_delta),决定接受还是**有上限地**重试(Step 3)。
435
396
 
@@ -440,42 +401,29 @@ async def stage_reflect(ctx: PipelineContext) -> None:
440
401
  policy = _reflect_policy(ctx.agent, ctx.context)
441
402
  decision, retries = "accept", 0
442
403
  if policy["enabled"]:
443
- # 用 last_delta(当前尝试的最终漂移)作重试信号,而非 max_delta(累积峰值,单调不减)——
444
- # 否则重试改善了也无法早退。重试若把漂移降到阈值内,下一轮 last_delta 达标即停止。
445
404
  while (ctx.verified.get("last_delta", 0.0) > policy["drift_threshold"]
446
405
  and retries < policy["max_retries"]):
447
- # 协作式中断点(P3):每次重试都是一次 `_execute`(LLM 调用)——这里是本管线里最接近
448
- # 范式"agentic loop 每轮检查中断"的地方。置位则停在下一轮 LLM 调用**之前**(省 token),
449
- # 保留已有最好结果。opt-in:stop_event=None 恒不触发,重试行为逐字不变。
450
406
  if _stop_requested(ctx):
451
- # 中断:decision 统一记 "interrupted",已发生的重试次数由 retries 字段承载。
452
- # (B2 审核 F1:此前分 interrupted/retry_interrupted 两值,其中 retries==0 一支仅在
453
- # 外部线程于"pre-reflect 边界检查"与"首个 while 检查"之间置位才可达——单线程测试无法
454
- # 稳定覆盖。合并为单值消除该未覆盖分支,信息量不减: retries 已表达重试了几次。)
455
407
  ctx.interrupted = True
456
408
  decision = "interrupted"
457
409
  break
458
- prev_result, prev_extra = ctx.result, ctx.extra # 重试前的最好结果
410
+ prev_result, prev_extra = ctx.result, ctx.extra
459
411
  try:
460
412
  retries += 1
461
413
  decision = "retry"
462
414
  ctx.result, ctx.extra = await ctx.agent._execute(
463
415
  ctx.task, ctx.context, ctx.session_id, ctx.memories)
464
- stage_verify(ctx) # 重新汇总校验(刷新 last_delta)
416
+ stage_verify(ctx)
465
417
  except Exception:
466
- # 重试出错(含重跑被 BLOCK)→ 回滚到上一次好结果,停止重试(不让重试变差)
467
418
  ctx.result, ctx.extra = prev_result, prev_extra
468
419
  decision = "retry_reverted"
469
420
  break
470
- # 重试后 last_delta 已达标 → 视为改善接受
471
421
  if decision == "retry" and ctx.verified.get("last_delta", 0.0) <= policy["drift_threshold"]:
472
422
  decision = "retry_accepted"
473
423
  ctx.reflect = {"decision": decision, "retries": retries,
474
424
  "final_last_delta": ctx.verified.get("last_delta", 0.0),
475
425
  "final_max_delta": ctx.verified.get("max_delta", 0.0)}
476
- stage_reflect.stage_name = "reflect"
477
426
 
478
427
 
479
- # 默认管线的 stage 序列(完整八阶段之七;persist=run_task 里的 emit + 领域 _store_memory)。
480
428
  DEFAULT_STAGES = [stage_perceive, stage_recall, stage_plan, stage_route,
481
429
  stage_act, stage_verify, stage_reflect]
@@ -0,0 +1,21 @@
1
+ """stage 的命名元数据。
2
+
3
+ pipeline 的两个模块都按「函数 + stage_name 属性」标注阶段,读侧用 getattr 兜底
4
+ (见 cognitive_pipeline 里取 name 的那处)。属性赋值 `fn.stage_name = ...` 在类型
5
+ 检查里不成立 —— 函数对象的类型没有这个属性;setattr 是运行期真做的同一件事,
6
+ 而且让「名字」与「定义」写在同一处,不会漏标。
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from typing import Callable, TypeVar
11
+
12
+ F = TypeVar("F", bound=Callable[..., object])
13
+
14
+
15
+ def named_stage(name: str) -> Callable[[F], F]:
16
+ """给 stage 可调用挂 stage_name(stage 事件与轨迹标注用这个键)。"""
17
+ def deco(fn: F) -> F:
18
+ vars(fn)["stage_name"] = name
19
+ return fn
20
+
21
+ return deco
@@ -16,11 +16,12 @@ hooks / 流式(run_task_stream)/ stop_event 中断 / 统一 `_emit_execution` /
16
16
  import asyncio
17
17
  from datetime import datetime
18
18
 
19
+ from ..core.dispatch import dispatch_failed
19
20
  from .cognitive_pipeline import PipelineContext, stage_recall # noqa: F401 (recall 复用默认 stage)
20
- from ..core.dispatch import dispatch_failed # 谓词在 core:避免 pipeline 反向 import agents
21
+ from .stage_meta import named_stage
21
22
 
22
23
 
23
- # ── Stage 2: meta_check —— 元问题判断(你是谁/能做什么 → 主管以总管身份自答,不分派)──
24
+ @named_stage("meta_check")
24
25
  async def sv_meta_check(ctx: PipelineContext) -> None:
25
26
  agent = ctx.agent
26
27
  if not agent._is_meta(ctx.task):
@@ -30,7 +31,6 @@ async def sv_meta_check(ctx: PipelineContext) -> None:
30
31
  node_type="decision", conf=0.92,
31
32
  )
32
33
  subagents = getattr(agent, "_subagents", {})
33
- # 拓扑 5 → 3:助手/行政已下放为指令技能,不再出现在团队介绍里。
34
34
  sub_names = {
35
35
  "finance": "财务 Agent(理财、收支、预算、风控)",
36
36
  "office": "办公学习 Agent(学习、知识管理、研究、写作、日程与行程)",
@@ -57,11 +57,10 @@ async def sv_meta_check(ctx: PipelineContext) -> None:
57
57
  )
58
58
  ctx.result = {"dispatched_to": "supervisor", "sub_result": reply,
59
59
  "self_answered": True}
60
- ctx.extra["_meta_answered"] = True # 短路标记:后续 stage 跳过(aggregate 清除)
61
- sv_meta_check.stage_name = "meta_check"
60
+ ctx.extra["_meta_answered"] = True
62
61
 
63
62
 
64
- # ── Stage 3: schedule —— 认知调度(优先级/负载/抢占)────────────────
63
+ @named_stage("schedule")
65
64
  async def sv_schedule(ctx: PipelineContext) -> None:
66
65
  if ctx.extra.get("_meta_answered"):
67
66
  return
@@ -73,10 +72,9 @@ async def sv_schedule(ctx: PipelineContext) -> None:
73
72
  + (f" · 抢占({ctx.sched.preemption_reason})" if ctx.sched.preemptible else "")),
74
73
  node_type="decision", conf=0.9,
75
74
  )
76
- sv_schedule.stage_name = "schedule"
77
75
 
78
76
 
79
- # ── Stage 4: route —— 路由决策(关键词+调度意图;未命中且模型可用 → 决策主管自答)──
77
+ @named_stage("route")
80
78
  async def sv_route(ctx: PipelineContext) -> None:
81
79
  if ctx.extra.get("_meta_answered"):
82
80
  return
@@ -84,11 +82,6 @@ async def sv_route(ctx: PipelineContext) -> None:
84
82
  target = agent._route(ctx.task, ctx.sched)
85
83
  ctx.target = target
86
84
  ctx.route = {"target": target, "reason": "主管关键词+调度意图分诊"}
87
- # 无关键词命中 = 正面对话/泛化问题 → 主管**直接自答一次**,省掉"LLM 分诊 + 派发"的
88
- # 第二次串行 LLM 调用(每次约 5s 网络往返,正面对话不值当)。只有命中领域关键词
89
- # (财务/行政/办公/…)才派发给对应子 Agent。stub 下保持原路径(零额外调用、离线测试确定)。
90
- # 权衡:少数本该派给领域 Agent 的"无关键词"任务会由主管自答——用户明确选速度优先。
91
- # (本 stage 只做**决策**置标记;自答的 LLM 调用在 dispatch stage 执行。)
92
85
  if not agent._has_keyword_hit(ctx.task) and not agent.router.is_stub:
93
86
  agent._think(
94
87
  content="无关键词命中 → 主管直接自答(省掉二次分诊派发,正面对话提速)",
@@ -100,16 +93,13 @@ async def sv_route(ctx: PipelineContext) -> None:
100
93
  node_type="decision",
101
94
  conf=0.9,
102
95
  )
103
- sv_route.stage_name = "route"
104
96
 
105
97
 
106
- # ── Stage 5: plan —— planner 拆解(多步?跨 Agent?)────────────────
98
+ @named_stage("plan")
107
99
  async def sv_plan(ctx: PipelineContext) -> None:
108
100
  if ctx.extra.get("_meta_answered") or ctx.extra.get("_self_answer"):
109
101
  return
110
102
  agent = ctx.agent
111
- # P0-1:LLM 规划含同步网络调用(call_tools),放线程池避免阻塞事件循环;
112
- # 规则拆解路径是纯 CPU,放线程池开销可忽略、行为不变。
113
103
  plan_obj, step_routes = await asyncio.to_thread(agent._plan_task, ctx.task, ctx.sched)
114
104
  if plan_obj is not None:
115
105
  routes_desc = " → ".join(f"{name}[{ag}]" for name, ag in step_routes)
@@ -117,23 +107,20 @@ async def sv_plan(ctx: PipelineContext) -> None:
117
107
  content=f"planner 判定为多步任务({len(plan_obj.tasks)}步):{routes_desc}",
118
108
  node_type="decision", conf=0.85,
119
109
  )
120
- # 若子任务跨不同 Agent,标记为多 Agent 顺序执行
121
110
  distinct_agents = {ag for _, ag in step_routes}
122
111
  ctx.multi_agent = len(distinct_agents) > 1
123
112
  else:
124
113
  ctx.multi_agent = False
125
114
  ctx.plan_obj = plan_obj
126
115
  ctx.step_routes = step_routes or []
127
- sv_plan.stage_name = "plan"
128
116
 
129
117
 
130
- # ── Stage 6: dispatch —— 分发执行(自答 / 多 Agent / 单 Agent / 兜底)──
118
+ @named_stage("dispatch")
131
119
  async def sv_dispatch(ctx: PipelineContext) -> None:
132
120
  agent = ctx.agent
133
121
  if ctx.extra.get("_meta_answered"):
134
122
  return
135
123
 
136
- # 主管自答(route stage 的决策在此执行)
137
124
  if ctx.extra.get("_self_answer"):
138
125
  _msgs = [{"role": "user", "content": (
139
126
  f"你是 Foxclaw 的主管 Agent(Supervisor),统筹整个智能体团队。"
@@ -141,9 +128,6 @@ async def sv_dispatch(ctx: PipelineContext) -> None:
141
128
  f"问题作答,不要只做自我介绍、不要答非所问。若确属需要专门领域深办的任务,可"
142
129
  f"简要引导用户去对应能力,但仍先给出有用的直接回应。"
143
130
  )}]
144
- # 联网增强:新闻/实时类(_web_augment 内 needs_search 命中)走百度搜索注入真实结果
145
- # 再作答,避免主管凭空编造(系统 prompt 明令严禁模拟搜索)。纯闲聊 needs_search=False,
146
- # 不搜、不减速。阻塞 I/O 放线程池(与 base._execute 同法),不堵 async 事件循环。
147
131
  _msgs = await asyncio.to_thread(agent._web_augment, ctx.task, _msgs)
148
132
  reply = await agent._call_llm(_msgs)
149
133
  agent._store_memory(
@@ -157,7 +141,6 @@ async def sv_dispatch(ctx: PipelineContext) -> None:
157
141
 
158
142
  subagents = getattr(agent, "_subagents", {})
159
143
 
160
- # 多 Agent 执行:并行计划(无依赖)→ fork-join 并发;否则顺序执行后汇聚
161
144
  if ctx.multi_agent and ctx.step_routes:
162
145
  if getattr(ctx.plan_obj, "flow_type", "sequential") == "parallel":
163
146
  agent._think(
@@ -173,58 +156,46 @@ async def sv_dispatch(ctx: PipelineContext) -> None:
173
156
  subagents, ctx.context, ctx.memories)
174
157
  return
175
158
 
176
- # 单 Agent 分发
177
159
  if ctx.target in subagents:
178
160
  ctx.dispatch_result = await agent._dispatch_single(
179
161
  ctx.task, ctx.target, ctx.sched, ctx.session_id,
180
162
  subagents, ctx.context, ctx.memories, ctx.plan_obj)
181
163
  return
182
164
 
183
- # 子 Agent 不可用(未注册)→ 主管自处理
184
165
  ctx.dispatch_result = await agent._dispatch_fallback(ctx.task, ctx.context)
185
- sv_dispatch.stage_name = "dispatch"
186
166
 
187
167
 
188
- # ── Stage 6.5: replan —— 反思重规划(P0-1:失败步骤 → LLM 反思 → 修正计划 → 重执行)──
168
+ @named_stage("replan")
189
169
  async def sv_replan(ctx: PipelineContext) -> None:
190
170
  if ctx.extra.get("_meta_answered") or ctx.extra.get("_self_answer"):
191
171
  return
192
172
  agent = ctx.agent
193
173
  dr = ctx.dispatch_result or {}
194
174
  if not dispatch_failed(dr):
195
- return # 无失败/未完成步骤 → 无需重规划
175
+ return
196
176
  if not agent._replan_enabled():
197
- return # opt-in 关 / 不支持 tools → 保留原结果(fail-safe)
177
+ return
198
178
  subagents = getattr(agent, "_subagents", {})
199
179
  max_rounds = int((ctx.context or {}).get("replan_max_rounds", 2))
200
180
  ctx.dispatch_result = await agent._replan_and_redo(
201
181
  ctx.task, ctx.sched, ctx.session_id, subagents, ctx.context, ctx.memories,
202
182
  dr, max_rounds=max_rounds)
203
- sv_replan.stage_name = "replan"
204
183
 
205
184
 
206
- # ── Stage 7: aggregate —— 结果组装(dispatch_result → ctx.result/extra)──
185
+ @named_stage("aggregate")
207
186
  async def sv_aggregate(ctx: PipelineContext) -> None:
208
- # 清除内部短路标记,不外泄到返回体(它们会经 **ctx.extra 透传进 resp)
209
187
  ctx.extra.pop("_meta_answered", None)
210
188
  ctx.extra.pop("_self_answer", None)
211
189
  dr = ctx.dispatch_result or {}
212
190
  if not dr:
213
- return # meta 路径:result 已在 meta_check 设好
191
+ return
214
192
  ctx.result = dr.get("result", ctx.result)
215
- # 白名单透传(与原主管返回体逐字一致的顶层键);session_id/agent/thought_summary
216
- # 由 base 模板统一组装,不从 dispatch_result 取。
217
193
  for k in ("schedule", "plan", "process_id"):
218
194
  if k in dr:
219
195
  ctx.extra[k] = dr[k]
220
- # 「未完成 ⇒ 不作为已完成真值沉淀」:任一子任务被 Harness 拦截 → 提升为顶层
221
- # status=blocked(与 base 模板的 skill_status 提升同一机制),执行事件 blocked=True,
222
- # 真值链不沉淀(呼应 P3-5)。成功/自答路径不带此键,行为逐字不变。
223
196
  if isinstance(ctx.result, dict) and ctx.result.get("blocked"):
224
197
  ctx.extra["skill_status"] = "blocked"
225
- sv_aggregate.stage_name = "aggregate"
226
198
 
227
199
 
228
- # 主管 pipeline 的 stage 序列(recall 复用默认实现:与旧 `self._recall_memory(task)` 逐字等价)
229
200
  SUPERVISOR_STAGES = [stage_recall, sv_meta_check, sv_schedule, sv_route,
230
201
  sv_plan, sv_dispatch, sv_replan, sv_aggregate]
@@ -152,9 +152,9 @@ def _invoke_plan_tool(router, system: str, user_content: str, agent_catalog: dic
152
152
  return None
153
153
  calls = res.get("tool_calls") or []
154
154
  if not calls:
155
- return None # 模型未调用工具 → 不可用,回退
155
+ return None
156
156
  if calls[0].get("args_parse_failed"):
157
- return None # 参数 JSON 解析失败 → 不可信,回退
157
+ return None
158
158
  args = calls[0].get("args") or {}
159
159
  steps = args.get("steps")
160
160
  if not isinstance(steps, list):
@@ -168,7 +168,7 @@ def _invoke_plan_tool(router, system: str, user_content: str, agent_catalog: dic
168
168
  continue
169
169
  agent = str(s.get("agent") or "office").strip()
170
170
  if agent not in agent_catalog:
171
- agent = "office" # 幻觉出不存在的 Agent(含已下放的 assistant/admin)→ 兜底办公学习
171
+ agent = "office"
172
172
  dep = s.get("depends_on") or []
173
173
  if not isinstance(dep, list):
174
174
  dep = []
@@ -11,14 +11,11 @@
11
11
  from __future__ import annotations
12
12
  import re
13
13
  from dataclasses import dataclass, field
14
+ from typing import Optional
14
15
 
15
- # ── 单一真源:否定词 + 顺序/并行/列表 标记 ──────────────────────
16
16
  NEGATIONS = ["不", "别", "无需", "不要", "取消", "甭", "don't", "no need", "without"]
17
- # 顺序拆分信号(明确的顺序连接词;不用"和/并"等高风险常用字,避免误拆名词短语)
18
17
  SPLIT_MARKERS = ["然后", "接着", "之后", "再", "最后", ";", ";"]
19
- # 并行信号:出现这些"同时性"连接词 → 子任务彼此无依赖,可并行 fork
20
18
  PARALLEL_TRIGGERS = ["同时", "分别", "并行", "一并"]
21
- # 列表分隔符(仅在检出并行触发词时才当拆分点,避免误拆普通名词短语)
22
19
  LIST_SEPS = ["、", ",", ",", ";", ";"]
23
20
 
24
21
 
@@ -55,15 +52,15 @@ class ScoredCandidate:
55
52
 
56
53
  @dataclass
57
54
  class TriggerRoute:
58
- primary: str = None
55
+ primary: Optional[str] = None
59
56
  confidence: float = 0.0
60
- candidates: list = field(default_factory=list) # [(id, score)]
61
- fallback: str = None
57
+ candidates: list = field(default_factory=list)
58
+ fallback: Optional[str] = None
62
59
  needs_composition: bool = False
63
60
  reason: str = ""
64
61
 
65
62
 
66
- def trigger_route(task: str, candidates: list, fallback_id: str = None,
63
+ def trigger_route(task: str, candidates: list, fallback_id: str | None = None,
67
64
  min_conf: float = 0.15, near_gap: float = 0.1) -> TriggerRoute:
68
65
  """触发词路由三步打分:否定过滤后的触发命中(0.8)+ 描述领域匹配(0.2),确定性排序。
69
66
  这是 AHS 原 route 的算法,现作为三处共用的通用引擎。"""
@@ -75,17 +72,15 @@ def trigger_route(task: str, candidates: list, fallback_id: str = None,
75
72
  triggers = c.triggers or []
76
73
  if not triggers:
77
74
  scored.append((c.id, 0.0)); continue
78
- # 第1步:否定过滤后的触发命中数 → 命中即给强基础分,不被触发词总数稀释
79
75
  hits = keyword_hits(task, triggers, negation=True)
80
76
  hit_score = 0.0 if hits == 0 else min(0.6 + 0.15 * (hits - 1), 1.0)
81
- # 第2步:领域匹配(描述里的词出现在任务中,粗略语义近似)
82
77
  desc_words = [w for w in re.split(r"[\s,,、]", c.description or "") if len(w) >= 2]
83
78
  domain_hit = sum(1 for w in desc_words if w in task)
84
79
  domain_score = min(domain_hit / max(len(desc_words), 1), 1.0)
85
80
  score = 0.8 * hit_score + 0.2 * domain_score
86
81
  scored.append((c.id, round(score, 3)))
87
82
 
88
- scored.sort(key=lambda x: (-x[1], x[0])) # 分降序,平局按 id 字典序(确定性)
83
+ scored.sort(key=lambda x: (-x[1], x[0]))
89
84
  top_id, top_score = scored[0]
90
85
  cands = [(i, s) for i, s in scored if s > 0]
91
86