foxclaw 0.1.0 → 0.1.2

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 (495) hide show
  1. package/README.md +54 -29
  2. package/bin/foxclaw.cjs +51 -0
  3. package/index.js +11 -10
  4. package/package.json +46 -38
  5. package/python/.dockerignore +35 -0
  6. package/python/.env.example +165 -0
  7. package/python/.gitattributes +46 -0
  8. package/python/.github/workflows/ci.yml +122 -0
  9. package/python/.github/workflows/release.yml +183 -0
  10. package/python/Dockerfile +64 -0
  11. package/python/LICENSE +21 -0
  12. package/python/MANIFEST.in +14 -0
  13. package/python/README.md +192 -0
  14. package/python/README_CN.md +324 -0
  15. package/python/bench/bm25_audit.py +199 -0
  16. package/python/bench/bm25_regression.py +178 -0
  17. package/python/bench/bm25_stress.py +159 -0
  18. package/python/bench/cd_audit.py +238 -0
  19. package/python/bench/chaos.py +142 -0
  20. package/python/bench/concurrency.py +103 -0
  21. package/python/bench/e2e_real_models.py +95 -0
  22. package/python/bench/llm_retry_test.py +248 -0
  23. package/python/bench/load_test.py +196 -0
  24. package/python/bench/vector_ivf_upgrade.py +134 -0
  25. package/python/docs/CHANNELS.md +195 -0
  26. package/python/docs/GOVERNANCE.md +164 -0
  27. package/python/docs/RELEASE.md +109 -0
  28. package/python/docs/RUN.md +75 -0
  29. package/python/docs/RUN_CN.md +121 -0
  30. package/python/docs//351/205/215/347/275/256/346/214/207/345/215/227.md +152 -0
  31. package/python/evals/__init__.py +0 -0
  32. package/python/evals/governance/README.md +61 -0
  33. package/python/evals/governance/__init__.py +0 -0
  34. package/python/evals/governance/cases.py +152 -0
  35. package/python/evals/governance/compare_embeddings.py +94 -0
  36. package/python/evals/governance/convergence.svg +11 -0
  37. package/python/evals/governance/report.json +371 -0
  38. package/python/evals/governance/run_evals.py +400 -0
  39. package/python/examples/governance_middleware_demo.py +77 -0
  40. package/python/examples/langgraph_governance_demo.py +108 -0
  41. package/python/examples/sample_itinerary.csv +6 -0
  42. package/python/examples/sample_knowledge.csv +9 -0
  43. package/python/examples/sample_transactions.csv +14 -0
  44. package/python/foxclaw/__init__.py +526 -0
  45. package/python/foxclaw/agents/__init__.py +12 -0
  46. package/python/foxclaw/agents/agents.py +35 -0
  47. package/python/foxclaw/agents/base.py +865 -0
  48. package/python/foxclaw/agents/impl/__init__.py +1 -0
  49. package/python/foxclaw/agents/impl/_helpers.py +78 -0
  50. package/python/foxclaw/agents/impl/finance.py +248 -0
  51. package/python/foxclaw/agents/impl/office.py +367 -0
  52. package/python/foxclaw/agents/impl/supervisor.py +659 -0
  53. package/python/foxclaw/agents/mixins/_context.py +151 -0
  54. package/python/foxclaw/agents/mixins/augment.py +149 -0
  55. package/python/foxclaw/agents/mixins/llm.py +66 -0
  56. package/python/foxclaw/agents/mixins/memory.py +35 -0
  57. package/python/foxclaw/agents/mixins/preference.py +81 -0
  58. package/python/foxclaw/agents/mixins/prompt.py +77 -0
  59. package/python/foxclaw/agents/mixins/skillbox.py +58 -0
  60. package/python/foxclaw/agents/mixins/tool_exec.py +443 -0
  61. package/python/foxclaw/api/__init__.py +283 -0
  62. package/python/foxclaw/api/app.py +123 -0
  63. package/python/foxclaw/api/middleware.py +317 -0
  64. package/python/foxclaw/api/routes/ahs.py +417 -0
  65. package/python/foxclaw/api/routes/attachments.py +117 -0
  66. package/python/foxclaw/api/routes/browser.py +39 -0
  67. package/python/foxclaw/api/routes/domain.py +314 -0
  68. package/python/foxclaw/api/routes/knowledge.py +163 -0
  69. package/python/foxclaw/api/routes/llm_admin.py +461 -0
  70. package/python/foxclaw/api/routes/memory.py +89 -0
  71. package/python/foxclaw/api/routes/orchestration.py +41 -0
  72. package/python/foxclaw/api/routes/recovery.py +43 -0
  73. package/python/foxclaw/api/routes/review.py +275 -0
  74. package/python/foxclaw/api/routes/search.py +37 -0
  75. package/python/foxclaw/api/routes/system.py +327 -0
  76. package/python/foxclaw/api/routes/task.py +281 -0
  77. package/python/foxclaw/api/routes/ws.py +68 -0
  78. package/python/foxclaw/api/spine.py +55 -0
  79. package/python/foxclaw/browser/__init__.py +13 -0
  80. package/python/foxclaw/browser/action/__init__.py +1 -0
  81. package/python/foxclaw/browser/action/action_executor.py +54 -0
  82. package/python/foxclaw/browser/action/action_types.py +45 -0
  83. package/python/foxclaw/browser/action/action_verifier.py +68 -0
  84. package/python/foxclaw/browser/action/url_policy.py +147 -0
  85. package/python/foxclaw/browser/adapter/__init__.py +1 -0
  86. package/python/foxclaw/browser/adapter/base_adapter.py +23 -0
  87. package/python/foxclaw/browser/adapter/web_adapter.py +46 -0
  88. package/python/foxclaw/browser/driver/__init__.py +1 -0
  89. package/python/foxclaw/browser/driver/base_driver.py +61 -0
  90. package/python/foxclaw/browser/driver/playwright_driver.py +166 -0
  91. package/python/foxclaw/browser/perception/__init__.py +1 -0
  92. package/python/foxclaw/browser/perception/dom_extractor.py +43 -0
  93. package/python/foxclaw/browser/perception/element_indexer.py +57 -0
  94. package/python/foxclaw/browser/perception/semantic_reducer.py +33 -0
  95. package/python/foxclaw/browser/perception/snapshot.py +29 -0
  96. package/python/foxclaw/browser/service.py +148 -0
  97. package/python/foxclaw/browser/session/__init__.py +1 -0
  98. package/python/foxclaw/browser/session/session_manager.py +57 -0
  99. package/python/foxclaw/capability/__init__.py +12 -0
  100. package/python/foxclaw/capability/embedding_service.py +67 -0
  101. package/python/foxclaw/channels/__init__.py +140 -0
  102. package/python/foxclaw/channels/discord.py +184 -0
  103. package/python/foxclaw/channels/dispatcher.py +204 -0
  104. package/python/foxclaw/channels/feishu.py +225 -0
  105. package/python/foxclaw/channels/openclaw_bridge.py +156 -0
  106. package/python/foxclaw/channels/signing.py +187 -0
  107. package/python/foxclaw/channels/telegram.py +121 -0
  108. package/python/foxclaw/channels/weixin.py +225 -0
  109. package/python/foxclaw/cli.py +154 -0
  110. package/python/foxclaw/cognitive/__init__.py +4 -0
  111. package/python/foxclaw/cognitive/reasoner.py +82 -0
  112. package/python/foxclaw/cognitive/state.py +147 -0
  113. package/python/foxclaw/core/__init__.py +9 -0
  114. package/python/foxclaw/core/audit_chain.py +133 -0
  115. package/python/foxclaw/core/audit_sink.py +100 -0
  116. package/python/foxclaw/core/compaction.py +283 -0
  117. package/python/foxclaw/core/config.py +65 -0
  118. package/python/foxclaw/core/confirm_nonce.py +157 -0
  119. package/python/foxclaw/core/dispatch.py +27 -0
  120. package/python/foxclaw/core/envfile.py +48 -0
  121. package/python/foxclaw/core/failure_policy.py +151 -0
  122. package/python/foxclaw/core/gateway.py +85 -0
  123. package/python/foxclaw/core/governance_audit.py +175 -0
  124. package/python/foxclaw/core/idempotency.py +289 -0
  125. package/python/foxclaw/core/injection_guard.py +221 -0
  126. package/python/foxclaw/core/memory_adapter.py +516 -0
  127. package/python/foxclaw/core/metrics.py +89 -0
  128. package/python/foxclaw/core/model_router.py +1097 -0
  129. package/python/foxclaw/core/paths.py +103 -0
  130. package/python/foxclaw/core/recovery.py +362 -0
  131. package/python/foxclaw/core/scheduler.py +144 -0
  132. package/python/foxclaw/core/secrets.py +134 -0
  133. package/python/foxclaw/core/sessions.py +157 -0
  134. package/python/foxclaw/core/settings.py +430 -0
  135. package/python/foxclaw/core/shared_write.py +198 -0
  136. package/python/foxclaw/core/steering_queue.py +153 -0
  137. package/python/foxclaw/core/system_bus.py +84 -0
  138. package/python/foxclaw/core/thought_chain.py +318 -0
  139. package/python/foxclaw/core/token_meter.py +235 -0
  140. package/python/foxclaw/core/tools.py +172 -0
  141. package/python/foxclaw/core/tracing.py +390 -0
  142. package/python/foxclaw/core/truth_pipeline.py +114 -0
  143. package/python/foxclaw/core/vec_recall_adapter.py +503 -0
  144. package/python/foxclaw/core/verify.py +181 -0
  145. package/python/foxclaw/core/web_search.py +242 -0
  146. package/python/foxclaw/finance/__init__.py +15 -0
  147. package/python/foxclaw/finance/evidence.py +153 -0
  148. package/python/foxclaw/finance/fx.py +100 -0
  149. package/python/foxclaw/finance/graph.py +188 -0
  150. package/python/foxclaw/finance/models.py +150 -0
  151. package/python/foxclaw/finance/nlparse.py +575 -0
  152. package/python/foxclaw/finance/policy.py +329 -0
  153. package/python/foxclaw/finance/reasoner.py +129 -0
  154. package/python/foxclaw/finance/reasoning.py +340 -0
  155. package/python/foxclaw/finance/reconcile.py +180 -0
  156. package/python/foxclaw/finance/runtime.py +527 -0
  157. package/python/foxclaw/governance/__init__.py +24 -0
  158. package/python/foxclaw/governance/middleware.py +184 -0
  159. package/python/foxclaw/harness/__init__.py +17 -0
  160. package/python/foxclaw/harness/_base.py +72 -0
  161. package/python/foxclaw/harness/_policy.py +359 -0
  162. package/python/foxclaw/harness/drift.py +89 -0
  163. package/python/foxclaw/harness/event_bus.py +31 -0
  164. package/python/foxclaw/harness/harness.py +446 -0
  165. package/python/foxclaw/harness/loop_detection.py +152 -0
  166. package/python/foxclaw/itinerary/__init__.py +10 -0
  167. package/python/foxclaw/itinerary/amap_client.py +117 -0
  168. package/python/foxclaw/itinerary/models.py +80 -0
  169. package/python/foxclaw/itinerary/reasoner.py +101 -0
  170. package/python/foxclaw/itinerary/reasoning.py +174 -0
  171. package/python/foxclaw/itinerary/runtime.py +148 -0
  172. package/python/foxclaw/kernel/__init__.py +13 -0
  173. package/python/foxclaw/kernel/capabilities.py +73 -0
  174. package/python/foxclaw/kernel/policy_gate.py +114 -0
  175. package/python/foxclaw/kernel/process_manager.py +205 -0
  176. package/python/foxclaw/kernel/scheduler.py +131 -0
  177. package/python/foxclaw/kernel/sentinel.py +453 -0
  178. package/python/foxclaw/kernel/sentinel_client.py +246 -0
  179. package/python/foxclaw/kernel/spine.py +97 -0
  180. package/python/foxclaw/knowledge/__init__.py +376 -0
  181. package/python/foxclaw/knowledge/truth_crystallizer.py +92 -0
  182. package/python/foxclaw/learning/__init__.py +12 -0
  183. package/python/foxclaw/learning/knowledge_graph.py +149 -0
  184. package/python/foxclaw/learning/models.py +93 -0
  185. package/python/foxclaw/learning/reasoner.py +70 -0
  186. package/python/foxclaw/learning/runtime.py +363 -0
  187. package/python/foxclaw/learning/spaced_repetition.py +96 -0
  188. package/python/foxclaw/mcp_server.py +189 -0
  189. package/python/foxclaw/orchestration/__init__.py +184 -0
  190. package/python/foxclaw/pipeline/__init__.py +23 -0
  191. package/python/foxclaw/pipeline/cognitive_pipeline.py +481 -0
  192. package/python/foxclaw/pipeline/supervisor_stages.py +230 -0
  193. package/python/foxclaw/planning/__init__.py +17 -0
  194. package/python/foxclaw/planning/llm_planner.py +232 -0
  195. package/python/foxclaw/planning/planner.py +99 -0
  196. package/python/foxclaw/search/__init__.py +24 -0
  197. package/python/foxclaw/search/agent_port.py +194 -0
  198. package/python/foxclaw/search/evidence.py +113 -0
  199. package/python/foxclaw/search/external_search.py +50 -0
  200. package/python/foxclaw/search/hkc_read_port.py +60 -0
  201. package/python/foxclaw/search/internal_search.py +73 -0
  202. package/python/foxclaw/search/models.py +129 -0
  203. package/python/foxclaw/search/planner.py +44 -0
  204. package/python/foxclaw/search/retriever.py +39 -0
  205. package/python/foxclaw/search/runtime.py +134 -0
  206. package/python/foxclaw/search/strategy.py +44 -0
  207. package/python/foxclaw/skills/__init__.py +613 -0
  208. package/python/foxclaw/skills/hooks_spec.py +73 -0
  209. package/python/foxclaw/skills/lifecycle.py +278 -0
  210. package/python/foxclaw/skills/markdown_skill.py +174 -0
  211. package/python/foxclaw/skills/markdown_store.py +39 -0
  212. package/python/foxclaw/skills/mcp_client.py +190 -0
  213. package/python/foxclaw/skills/mcp_store.py +43 -0
  214. package/python/foxclaw/skills/permission.py +180 -0
  215. package/python/foxclaw/skills/pipeline.py +386 -0
  216. package/python/foxclaw/skills/policy.py +80 -0
  217. package/python/foxclaw/skills/review.py +72 -0
  218. package/python/foxclaw/skills/store.py +57 -0
  219. package/python/frontend/assets/core_bg.jpg +0 -0
  220. package/python/frontend/deck/auth.js +32 -0
  221. package/python/frontend/deck/chat_model_picker.js +537 -0
  222. package/python/frontend/deck/domain.js +296 -0
  223. package/python/frontend/deck/main.js +1950 -0
  224. package/python/frontend/deck/markdown.js +48 -0
  225. package/python/frontend/deck/settings.js +324 -0
  226. package/python/frontend/deck/state.js +6 -0
  227. package/python/frontend/deck/util.js +6 -0
  228. package/python/frontend/deck/visuals.js +212 -0
  229. package/python/frontend/deck.css +855 -0
  230. package/python/frontend/deck.html +686 -0
  231. package/python/frontend/deck_v2.html +341 -0
  232. package/python/frontend/planet/index.html +266 -0
  233. package/python/frontend/planet/planet.css +579 -0
  234. package/python/frontend/planet/planet.js +1637 -0
  235. package/python/frontend/planet/vendor/three.core.min.js +6 -0
  236. package/python/frontend/planet/vendor/three.module.min.js +6 -0
  237. package/python/frontend/v3/assets/index-BD7VPbMV.js +33 -0
  238. package/python/frontend/v3/assets/index-CAvnYWGH.css +1 -0
  239. package/python/frontend/v3/index.html +15 -0
  240. package/python/frontend/widget_runtime.js +295 -0
  241. package/python/frontend/widgets.js +696 -0
  242. package/python/frontend_src/index.html +14 -0
  243. package/python/frontend_src/package-lock.json +1376 -0
  244. package/python/frontend_src/package.json +19 -0
  245. package/python/frontend_src/src/App.vue +62 -0
  246. package/python/frontend_src/src/api.js +89 -0
  247. package/python/frontend_src/src/components/ChatDock.vue +377 -0
  248. package/python/frontend_src/src/components/ComposerMenu.vue +75 -0
  249. package/python/frontend_src/src/components/DocViewer.vue +171 -0
  250. package/python/frontend_src/src/components/Inspector.vue +241 -0
  251. package/python/frontend_src/src/components/LeftRail.vue +394 -0
  252. package/python/frontend_src/src/components/MenuPopover.vue +130 -0
  253. package/python/frontend_src/src/components/MessageStream.vue +401 -0
  254. package/python/frontend_src/src/components/ModelPicker.vue +355 -0
  255. package/python/frontend_src/src/components/QuickIngest.vue +118 -0
  256. package/python/frontend_src/src/components/Stage.vue +226 -0
  257. package/python/frontend_src/src/components/ToolMenu.vue +144 -0
  258. package/python/frontend_src/src/components/TopBar.vue +90 -0
  259. package/python/frontend_src/src/components/panels/ActivityPanel.vue +84 -0
  260. package/python/frontend_src/src/components/panels/AgentsPanel.vue +86 -0
  261. package/python/frontend_src/src/components/panels/KnowledgePanel.vue +441 -0
  262. package/python/frontend_src/src/components/panels/MemoryPanel.vue +118 -0
  263. package/python/frontend_src/src/components/panels/PanelBox.vue +51 -0
  264. package/python/frontend_src/src/components/panels/ReviewPanel.vue +308 -0
  265. package/python/frontend_src/src/components/panels/SettingsPanel.vue +254 -0
  266. package/python/frontend_src/src/components/panels/TasksPanel.vue +74 -0
  267. package/python/frontend_src/src/components/panels/ToolsPanel.vue +640 -0
  268. package/python/frontend_src/src/components/panels/UsagePanel.vue +105 -0
  269. package/python/frontend_src/src/lib/fmt.js +48 -0
  270. package/python/frontend_src/src/lib/md.js +204 -0
  271. package/python/frontend_src/src/main.js +7 -0
  272. package/python/frontend_src/src/stores/app.js +106 -0
  273. package/python/frontend_src/src/stores/chat.js +484 -0
  274. package/python/frontend_src/src/stores/model.js +104 -0
  275. package/python/frontend_src/src/stores/panels.js +330 -0
  276. package/python/frontend_src/src/styles/base.css +138 -0
  277. package/python/frontend_src/src/styles/panel.css +117 -0
  278. package/python/frontend_src/src/styles/tokens.css +78 -0
  279. package/python/frontend_src/vite.config.js +46 -0
  280. package/python/pyproject.toml +265 -0
  281. package/python/skills/README.md +123 -0
  282. package/python/skills/execution-contract/SKILL.md +30 -0
  283. package/python/skills/general-assistant/SKILL.md +34 -0
  284. package/python/skills/planner/SKILL.md +33 -0
  285. package/python/skills/schedule-itinerary/SKILL.md +35 -0
  286. package/python/skills/skill-composition/SKILL.md +31 -0
  287. package/python/skills/skill-guard/SKILL.md +67 -0
  288. package/python/skills/skill-memory/SKILL.md +50 -0
  289. package/python/skills/skill-router/SKILL.md +35 -0
  290. package/python/tests/conftest.py +76 -0
  291. package/python/tests/run_all.py +263 -0
  292. package/python/tests/test_agent_lifecycle.py +360 -0
  293. package/python/tests/test_ahs_p2.py +414 -0
  294. package/python/tests/test_anthropic_tools.py +206 -0
  295. package/python/tests/test_api.py +87 -0
  296. package/python/tests/test_api_auth.py +83 -0
  297. package/python/tests/test_api_host_guard.py +69 -0
  298. package/python/tests/test_attachments.py +167 -0
  299. package/python/tests/test_audit_chain.py +121 -0
  300. package/python/tests/test_audit_sink.py +136 -0
  301. package/python/tests/test_browser.py +153 -0
  302. package/python/tests/test_browser_request_guard.py +184 -0
  303. package/python/tests/test_capabilities.py +158 -0
  304. package/python/tests/test_channel_signing.py +192 -0
  305. package/python/tests/test_channel_trust.py +109 -0
  306. package/python/tests/test_channels.py +184 -0
  307. package/python/tests/test_cli.py +46 -0
  308. package/python/tests/test_compaction_toolpair.py +119 -0
  309. package/python/tests/test_config_layering.py +65 -0
  310. package/python/tests/test_confirm_nonce.py +197 -0
  311. package/python/tests/test_context_compaction_wiring.py +130 -0
  312. package/python/tests/test_dispatch.py +95 -0
  313. package/python/tests/test_domain_evals.py +403 -0
  314. package/python/tests/test_domain_import.py +78 -0
  315. package/python/tests/test_domain_reasoners.py +148 -0
  316. package/python/tests/test_e2e.py +178 -0
  317. package/python/tests/test_emb_cache.py +107 -0
  318. package/python/tests/test_embedding_singleton.py +89 -0
  319. package/python/tests/test_envfile.py +83 -0
  320. package/python/tests/test_evals_smoke.py +114 -0
  321. package/python/tests/test_event_spine.py +182 -0
  322. package/python/tests/test_evidence_semantic.py +101 -0
  323. package/python/tests/test_evidence_sse.py +97 -0
  324. package/python/tests/test_file_ingest.py +146 -0
  325. package/python/tests/test_finance.py +153 -0
  326. package/python/tests/test_finance_ledger.py +252 -0
  327. package/python/tests/test_finance_persist.py +178 -0
  328. package/python/tests/test_gateway.py +144 -0
  329. package/python/tests/test_governance.py +468 -0
  330. package/python/tests/test_governance_audit.py +359 -0
  331. package/python/tests/test_governance_middleware.py +143 -0
  332. package/python/tests/test_hkc.py +286 -0
  333. package/python/tests/test_hkc_index_build_manager.py +339 -0
  334. package/python/tests/test_hkc_manifest_reuse.py +317 -0
  335. package/python/tests/test_hkc_search_event_sync.py +533 -0
  336. package/python/tests/test_import_path_guard.py +161 -0
  337. package/python/tests/test_injection_guard.py +211 -0
  338. package/python/tests/test_integration.py +141 -0
  339. package/python/tests/test_itinerary.py +103 -0
  340. package/python/tests/test_kee_semantic.py +89 -0
  341. package/python/tests/test_kernel.py +222 -0
  342. package/python/tests/test_learning.py +110 -0
  343. package/python/tests/test_llm_admin.py +160 -0
  344. package/python/tests/test_llm_planner.py +366 -0
  345. package/python/tests/test_markdown_skills.py +73 -0
  346. package/python/tests/test_markdown_skills_openclaw.py +71 -0
  347. package/python/tests/test_mcp_connectors.py +109 -0
  348. package/python/tests/test_mcp_guardian_discovery.py +91 -0
  349. package/python/tests/test_mcp_no_memory.py +43 -0
  350. package/python/tests/test_mcp_policy_gate.py +77 -0
  351. package/python/tests/test_mcp_server.py +95 -0
  352. package/python/tests/test_memory_recall.py +95 -0
  353. package/python/tests/test_metrics.py +66 -0
  354. package/python/tests/test_multi_chain.py +102 -0
  355. package/python/tests/test_multimodal.py +135 -0
  356. package/python/tests/test_negotiation.py +146 -0
  357. package/python/tests/test_orchestration.py +119 -0
  358. package/python/tests/test_orchestration_decoupled.py +109 -0
  359. package/python/tests/test_p3_p5_hardening.py +338 -0
  360. package/python/tests/test_parallel_dispatch.py +215 -0
  361. package/python/tests/test_pipeline.py +533 -0
  362. package/python/tests/test_plan_visible.py +86 -0
  363. package/python/tests/test_planner.py +95 -0
  364. package/python/tests/test_policy_gate.py +109 -0
  365. package/python/tests/test_reasoner.py +185 -0
  366. package/python/tests/test_release_check.py +263 -0
  367. package/python/tests/test_resume_checkpoint.py +156 -0
  368. package/python/tests/test_routing.py +95 -0
  369. package/python/tests/test_scheduler.py +141 -0
  370. package/python/tests/test_search.py +204 -0
  371. package/python/tests/test_secrets.py +159 -0
  372. package/python/tests/test_semantic.py +78 -0
  373. package/python/tests/test_sessions.py +91 -0
  374. package/python/tests/test_settings.py +281 -0
  375. package/python/tests/test_shadow_learning.py +330 -0
  376. package/python/tests/test_shared_write.py +207 -0
  377. package/python/tests/test_skill_taint_gate.py +154 -0
  378. package/python/tests/test_skills.py +133 -0
  379. package/python/tests/test_stats_and_events.py +165 -0
  380. package/python/tests/test_streaming.py +60 -0
  381. package/python/tests/test_supervisor_pipeline.py +177 -0
  382. package/python/tests/test_supervisor_triage.py +108 -0
  383. package/python/tests/test_taint.py +117 -0
  384. package/python/tests/test_task_stream.py +85 -0
  385. package/python/tests/test_thinking_sse.py +67 -0
  386. package/python/tests/test_thinking_stream.py +76 -0
  387. package/python/tests/test_tool_center.py +129 -0
  388. package/python/tests/test_tool_parallel.py +113 -0
  389. package/python/tests/test_tool_params.py +103 -0
  390. package/python/tests/test_tool_use.py +242 -0
  391. package/python/tests/test_toplevel_concurrency.py +117 -0
  392. package/python/tests/test_tracing.py +290 -0
  393. package/python/tests/test_truth_crystallizer.py +127 -0
  394. package/python/tests/test_truth_flow.py +93 -0
  395. package/python/tests/test_url_policy.py +53 -0
  396. package/python/tests/test_verify_evidence.py +225 -0
  397. package/python/tests/test_version_sync.py +71 -0
  398. package/python/tests/test_web_trigger.py +99 -0
  399. package/python/tools/check_faiss_type.py +31 -0
  400. package/python/tools/check_log.ps1 +46 -0
  401. package/python/tools/ivf_status.py +72 -0
  402. package/python/tools/mypy_baseline.txt +5 -0
  403. package/python/tools/mypy_ratchet.py +97 -0
  404. package/python/tools/pack.py +178 -0
  405. package/python/tools/release_check.py +349 -0
  406. package/python/tools/restart_foxclaw.ps1 +74 -0
  407. package/python/tools/shared_write_audit.py +204 -0
  408. package/python/tools/size_baseline.txt +28 -0
  409. package/python/tools/size_check.py +102 -0
  410. package/python/tools/verify_env.bat +2 -0
  411. package/python/tools/verify_env.py +25 -0
  412. package/python/vendor/HMR_LICENSE +37 -0
  413. package/python/vendor/HMR_README.md +65 -0
  414. package/python/vendor/hkc/LICENSE +37 -0
  415. package/python/vendor/hkc/hkc_ace/__init__.py +0 -0
  416. package/python/vendor/hkc/hkc_ace/ace.py +457 -0
  417. package/python/vendor/hkc/hkc_ace/skill_taxonomy.json +465 -0
  418. package/python/vendor/hkc/hkc_api/__init__.py +0 -0
  419. package/python/vendor/hkc/hkc_api/container.py +416 -0
  420. package/python/vendor/hkc/hkc_api/ingest_jobs.py +103 -0
  421. package/python/vendor/hkc/hkc_api/main.py +157 -0
  422. package/python/vendor/hkc/hkc_api/routes/__init__.py +0 -0
  423. package/python/vendor/hkc/hkc_api/routes/ability.py +151 -0
  424. package/python/vendor/hkc/hkc_api/routes/config.py +51 -0
  425. package/python/vendor/hkc/hkc_api/routes/knowledge.py +630 -0
  426. package/python/vendor/hkc/hkc_api/routes/search.py +88 -0
  427. package/python/vendor/hkc/hkc_api/schemas.py +153 -0
  428. package/python/vendor/hkc/hkc_api/synthesis.py +179 -0
  429. package/python/vendor/hkc/hkc_core/__init__.py +0 -0
  430. package/python/vendor/hkc/hkc_core/graph/__init__.py +0 -0
  431. package/python/vendor/hkc/hkc_core/graph/base.py +188 -0
  432. package/python/vendor/hkc/hkc_core/graph/sqlite_store.py +789 -0
  433. package/python/vendor/hkc/hkc_core/models/__init__.py +0 -0
  434. package/python/vendor/hkc/hkc_core/models/enums.py +65 -0
  435. package/python/vendor/hkc/hkc_core/models/ku.py +307 -0
  436. package/python/vendor/hkc/hkc_core/utils/__init__.py +0 -0
  437. package/python/vendor/hkc/hkc_core/utils/id_gen.py +113 -0
  438. package/python/vendor/hkc/hkc_crystallizer/__init__.py +44 -0
  439. package/python/vendor/hkc/hkc_crystallizer/adapters.py +94 -0
  440. package/python/vendor/hkc/hkc_crystallizer/candidate.py +145 -0
  441. package/python/vendor/hkc/hkc_crystallizer/crystallizer.py +179 -0
  442. package/python/vendor/hkc/hkc_crystallizer/ingress.py +90 -0
  443. package/python/vendor/hkc/hkc_kde/__init__.py +0 -0
  444. package/python/vendor/hkc/hkc_kde/chunker.py +197 -0
  445. package/python/vendor/hkc/hkc_kde/extractor.py +623 -0
  446. package/python/vendor/hkc/hkc_kde/kde.py +298 -0
  447. package/python/vendor/hkc/hkc_kde/loaders/__init__.py +0 -0
  448. package/python/vendor/hkc/hkc_kde/loaders/ebook_loader.py +153 -0
  449. package/python/vendor/hkc/hkc_kde/loaders/office_loader.py +194 -0
  450. package/python/vendor/hkc/hkc_kde/loaders/pdf_loader.py +162 -0
  451. package/python/vendor/hkc/hkc_kde/loaders/text_loader.py +157 -0
  452. package/python/vendor/hkc/hkc_kde/loaders/web_loader.py +91 -0
  453. package/python/vendor/hkc/hkc_kde/models.py +74 -0
  454. package/python/vendor/hkc/hkc_kde/packager.py +329 -0
  455. package/python/vendor/hkc/hkc_kde/relation_disc.py +151 -0
  456. package/python/vendor/hkc/hkc_kee/__init__.py +0 -0
  457. package/python/vendor/hkc/hkc_kee/dedup.py +227 -0
  458. package/python/vendor/hkc/hkc_kee/fingerprint.py +106 -0
  459. package/python/vendor/hkc/hkc_kee/kee.py +541 -0
  460. package/python/vendor/hkc/hkc_kep/__init__.py +0 -0
  461. package/python/vendor/hkc/hkc_kep/event_bus.py +205 -0
  462. package/python/vendor/hkc/hkc_sdk/__init__.py +63 -0
  463. package/python/vendor/hkc/hkc_sdk/base.py +151 -0
  464. package/python/vendor/hkc/hkc_sdk/direct_client.py +153 -0
  465. package/python/vendor/hkc/hkc_sdk/exceptions.py +39 -0
  466. package/python/vendor/hkc/hkc_sdk/http_client.py +166 -0
  467. package/python/vendor/hkc/hkc_sdk/models.py +171 -0
  468. package/python/vendor/hkc/hkc_search/__init__.py +0 -0
  469. package/python/vendor/hkc/hkc_search/bm25.py +325 -0
  470. package/python/vendor/hkc/hkc_search/embedding_backends.py +374 -0
  471. package/python/vendor/hkc/hkc_search/graph_search.py +132 -0
  472. package/python/vendor/hkc/hkc_search/hybrid.py +331 -0
  473. package/python/vendor/hkc/hkc_search/index_build_manager.py +378 -0
  474. package/python/vendor/hkc/hkc_search/vector_search.py +484 -0
  475. package/python/vendor/hmr/__init__.py +0 -0
  476. package/python/vendor/hmr/core/__init__.py +0 -0
  477. package/python/vendor/hmr/core/hmr.py +684 -0
  478. package/python/vendor/hmr/core/models.py +310 -0
  479. package/python/vendor/hmr/engines/__init__.py +0 -0
  480. package/python/vendor/hmr/engines/evolution.py +710 -0
  481. package/python/vendor/hmr/engines/jit_compiler.py +472 -0
  482. package/python/vendor/hmr/engines/lifecycle.py +264 -0
  483. package/python/vendor/hmr/engines/policy.py +625 -0
  484. package/python/vendor/hmr/engines/recall.py +260 -0
  485. package/python/vendor/hmr/engines/runtime_state.py +158 -0
  486. package/python/vendor/hmr/engines/scheduler.py +323 -0
  487. package/python/vendor/hmr/engines/semantic.py +150 -0
  488. package/python/vendor/hmr/engines/temporal.py +261 -0
  489. package/python/vendor/hmr/engines/thought_chain.py +635 -0
  490. package/python/vendor/hmr/graph/__init__.py +0 -0
  491. package/python/vendor/hmr/graph/cwg.py +338 -0
  492. package/python/vendor/hmr/graph/memory_graph.py +530 -0
  493. package/python/vendor/hmr/storage/__init__.py +0 -0
  494. package/python/vendor/hmr/storage/memory_fs.py +300 -0
  495. package/python/vendor/hmr/storage/vector_store.py +520 -0
@@ -0,0 +1,865 @@
1
+ """
2
+ CognitiveAgent — 认知增强 Agent 基类
3
+ 内建 HMR + ThoughtChain + Harness,不需要外部配置
4
+ 每个 Agent 实例拥有:
5
+ - 独立的 HMR Workspace(记忆隔离)
6
+ - 独立的 ThoughtChain(推理轨迹)
7
+ - 独立的 Harness 实例(策略边界)
8
+ - 共享的 Anthropic LLM 接口
9
+ """
10
+
11
+ import threading
12
+ import uuid
13
+
14
+ from ..core.thought_chain import ThoughtChain, ThoughtNode
15
+ from ..harness.harness import Harness, HarnessResult
16
+
17
+ # 工具并行执行后,多个 worker 线程可能同时走 _think(web_search 命中 / 记忆召回节点),
18
+ # 而 _parent_node_id 是共享可变指针。用模块级 RLock 串行化「建链 + 挪指针」两步,
19
+ # 保证 thought 链仍是一条合法链(节点本身顺序由完成时刻决定,语义可接受)。
20
+ _THINK_LOCK = threading.RLock()
21
+
22
+ # ── S3 步骤 2:按职责拆分到 mixin(纯搬移、零行为变更)────────────────────────
23
+ # 会话状态基础设施(contextvar + 污点 + HarnessBlockedError)搬至 mixins/_context.py;
24
+ # 各职责方法搬至 mixins/{llm,augment,memory,preference,prompt,tool_exec}.py;
25
+ # 主循环(会话管理 / _think / stream_reply / run_task 生命周期)留在本文件。
26
+ from .mixins._context import (
27
+ _agent_ctx_get,
28
+ _agent_ctx_set,
29
+ _taint_get,
30
+ _taint_raise,
31
+ _taint_reset,
32
+ _untrusted_set,
33
+ _evidence_reset,
34
+ _evidence_take,
35
+ _suppress_memory,
36
+ _act_stream,
37
+ _reasoning_inject,
38
+ _resume_steps,
39
+ HarnessBlockedError,
40
+ )
41
+ from .mixins.augment import AugmentMixin
42
+ from .mixins.llm import LlmMixin
43
+ from .mixins.memory import MemoryMixin
44
+ from .mixins.skillbox import SkillBoxMixin
45
+ from .mixins.preference import PreferenceMixin
46
+ from .mixins.prompt import PromptMixin, KERNEL_PROMPT
47
+ from .mixins.tool_exec import ToolExecMixin
48
+
49
+ # 历史导入兼容:`from foxclaw.agents.base import X` 的 X 在此 re-export(测试/外部模块仍按此路径取符号)。
50
+ __all__ = [
51
+ "CognitiveAgent", "HarnessBlockedError", "KERNEL_PROMPT",
52
+ "_suppress_memory", "_act_stream", "_taint_get", "_taint_raise", "_taint_reset",
53
+ ]
54
+
55
+
56
+ def _plan_payload(ctx) -> dict | None:
57
+ """从管线 ctx 提取计划负载:{steps, flow, multi_step};没有计划 → None。
58
+
59
+ 领域管线把计划写 `ctx.plan`(pipeline.stage_plan),主管管线写 `ctx.plan_obj`
60
+ (pipeline.supervisor_stages.sv_plan),两种形态都兼容 → 两条链路都能回显计划。
61
+ 纯只读、不抛异常(坏了就当没有计划,不拖垮任务)。
62
+ """
63
+ for attr in ("plan", "plan_obj"):
64
+ p = getattr(ctx, attr, None)
65
+ if p is None:
66
+ continue
67
+ try:
68
+ steps = p.get("steps") if isinstance(p, dict) else getattr(p, "steps", None)
69
+ if not steps:
70
+ continue
71
+ steps = [str(s) for s in steps]
72
+ flow = (p.get("flow", "single") if isinstance(p, dict)
73
+ else getattr(p, "flow", "single")) or "single"
74
+ multi = (p.get("multi_step") if isinstance(p, dict)
75
+ else getattr(p, "multi_step", False))
76
+ return {"steps": steps, "flow": flow,
77
+ "multi_step": bool(multi) or len(steps) > 1}
78
+ except Exception:
79
+ continue
80
+ return None
81
+
82
+
83
+ class CognitiveAgent(AugmentMixin, LlmMixin, MemoryMixin,
84
+ PreferenceMixin, PromptMixin, ToolExecMixin, SkillBoxMixin):
85
+ """
86
+ 所有 Foxclaw Agent 的基类
87
+ 子类只需实现:_role_prompt、allowed_tools、run_task
88
+ (system_prompt 由基类统一 = KERNEL_PROMPT 内核前缀 + 子类 _role_prompt 角色职责)
89
+ """
90
+
91
+ AGENT_ID: str = "base"
92
+ AGENT_NAME: str = "Base Agent"
93
+ ALLOWED_TOOL_CATEGORIES: set[str] = {"read"}
94
+
95
+ def __init__(
96
+ self,
97
+ hmr, # HMR 实例(从外部注入)
98
+ thought_chain: ThoughtChain,
99
+ client=None,
100
+ data_dir: str = "./foxclaw_data",
101
+ shadow_rounds: int = 20,
102
+ router=None, # ModelRouter(多模型路由,从外部注入)
103
+ spine=None, # 【P0】全局 EventSpine(harness 事件汇入脊柱→统一 WS 投影)
104
+ caps=None, # 【P1-3】CapabilityRegistry(按名解析同伴,不再直接互持)
105
+ ):
106
+ self.hmr = hmr
107
+ self.thought_chain = thought_chain
108
+ self.client = client
109
+ # 多模型路由:优先用注入的 router;否则惰性创建(自动探测可用模型)
110
+ if router is None:
111
+ from ..core.model_router import ModelRouter
112
+ router = ModelRouter()
113
+ self.router = router
114
+ self.data_dir = data_dir
115
+
116
+ # 独立 Harness
117
+ self.harness = Harness(
118
+ agent_id=self.AGENT_ID,
119
+ thought_chain=thought_chain,
120
+ policy_path=f"{data_dir}/policy_{self.AGENT_ID}.yaml",
121
+ shadow_rounds=shadow_rounds,
122
+ spine=spine,
123
+ )
124
+ # 调度层(主管)豁免语义漂移拦截:接收任意输入是其本职,不算异常。
125
+ # 领域 Agent 保持语义拦截:执行业务时跑题(如财务去写诗)才该拦。
126
+ self.harness.block_on_semantic = not getattr(self, "IS_ROUTER", False)
127
+
128
+ # 获取或创建 HMR Workspace
129
+ self.workspace = hmr.get_workspace(self.AGENT_ID)
130
+
131
+ # 每任务会话状态改由 contextvars 承载(见文件顶部)。这里只需一个 per-实例 token
132
+ # 作为分槽 key;_session_id / _parent_node_id 是路由到 contextvar 的 property,
133
+ # 故 start_session / _think 里既有的读写代码完全无需改动。
134
+ self._ctx_token = uuid.uuid4().hex
135
+
136
+ # 【P1-3】能力注册中枢:Agent 不再直接持有 search_runtime,而是按名惰性解析
137
+ # (打断 agents⇄search_runtime⇄agent_port⇄agents 的直接互持环)。
138
+ self._caps = caps
139
+
140
+ def search_internal(self, query: str, top_k: int = 5):
141
+ """Agent 反向调用内部搜索(HMR记忆 + HKC知识 + Agent态势),只查内部、不联网。
142
+ 这是「串连 Agent」双向连通的 Agent→搜索 方向。经注册中枢惰性解析 search_runtime。"""
143
+ caps = getattr(self, "_caps", None)
144
+ rt = caps.get("search_runtime") if caps is not None else None
145
+ if rt is None:
146
+ return None
147
+ return rt.search(query, agent_id=self.AGENT_ID, internal_only=True)
148
+
149
+ def available_abilities(self, keys: list = None) -> list:
150
+ """【HKC 第2档 Step2·能力自省】证据驱动地查 HKC 的 ACE 能力:哪些的知识覆盖已达标
151
+ (可编译 sufficient=True)、哪些还差(sufficient=False + missing_skills)。
152
+
153
+ **只读、informational——不改任何执行**。HKC 未启用/未 vendor → []。coverage 是确定性
154
+ (无 LLM),但对每个能力算一遍图覆盖,故**按需调用**(非热路径)。注:HKC 知识是全局共享
155
+ 的(非按 Agent 分区),故各 Agent 看到的能力覆盖一致——这是"当前知识能支撑哪些能力"的
156
+ 系统级自省,挂在 Agent 上是"我能取用哪些能力"的语义。"""
157
+ caps = getattr(self, "_caps", None)
158
+ hkc = caps.get("hkc") if caps is not None else None
159
+ if hkc is None or not getattr(hkc, "available", False):
160
+ return []
161
+ client = hkc.client
162
+ try:
163
+ all_keys = list(keys) if keys else list(client.list_abilities())
164
+ except Exception:
165
+ return []
166
+ out = []
167
+ for key in all_keys:
168
+ try:
169
+ ab = client.compile_ability(key)
170
+ out.append({"ability_key": ab.ability_key, "display_name": ab.display_name,
171
+ "domain": ab.domain, "sufficient": True, "coverage": ab.coverage})
172
+ except Exception as e:
173
+ if hasattr(e, "missing_skills"): # 覆盖不足=证据 gate(非错误)
174
+ out.append({"ability_key": key, "sufficient": False,
175
+ "coverage": getattr(e, "coverage", {}),
176
+ "missing_skills": getattr(e, "missing_skills", [])})
177
+ # 其它异常(未知 key/图错误)→ 跳过该能力,不污染结果
178
+ return out
179
+
180
+ # ── 每任务会话指针(contextvar 支撑,property 透明路由)──────
181
+ @property
182
+ def _session_id(self):
183
+ return _agent_ctx_get(self._ctx_token).get("session_id")
184
+
185
+ @_session_id.setter
186
+ def _session_id(self, value):
187
+ _agent_ctx_set(self._ctx_token, session_id=value)
188
+
189
+ @property
190
+ def _parent_node_id(self):
191
+ return _agent_ctx_get(self._ctx_token).get("parent_node_id")
192
+
193
+ @_parent_node_id.setter
194
+ def _parent_node_id(self, value):
195
+ _agent_ctx_set(self._ctx_token, parent_node_id=value)
196
+
197
+ # ── 会话管理 ───────────────────────────────────────────
198
+
199
+ def start_session(self, goal: str, entities: list[str] = None) -> str:
200
+ """开始一个新任务会话,注册目标基线"""
201
+ self._session_id = f"sess_{uuid.uuid4().hex[:10]}"
202
+ self._parent_node_id = None
203
+ _taint_reset(self) # 新会话从无污点起算(数据敏感度清零)
204
+ _evidence_reset(self) # 新会话不继承上一个任务的执行证据(否则证据串台)
205
+
206
+ # 注册 Harness 目标基线
207
+ self.harness.register_goal(
208
+ goal_text=goal,
209
+ allowed_tool_categories=self.ALLOWED_TOOL_CATEGORIES,
210
+ baseline_entities=set(entities or []),
211
+ )
212
+
213
+ # 在 ThoughtChain 里记录目标基线节点
214
+ goal_node = ThoughtNode.create(
215
+ session_id=self._session_id,
216
+ agent_id=self.AGENT_ID,
217
+ type="goal",
218
+ content=goal,
219
+ conf=1.0,
220
+ entities=entities or [],
221
+ )
222
+ self.thought_chain.add_node(goal_node)
223
+ self.thought_chain.update_harness(goal_node.id, "BASELINE")
224
+ self._parent_node_id = goal_node.id
225
+
226
+ # 【深审修复·移除惰性共享写】原先 `self.workspace.active_goal = goal` 把每任务目标
227
+ # 写到 per-Agent **共享**的 HMR workspace 上,同一 Agent 实例并发多任务会互相覆盖。
228
+ # 审计确认此写**完全惰性**:foxclaw 召回走显式 query(见 _recall_memory,从不读
229
+ # workspace.active_goal),save_workspace 从未被调用(不持久化),无测试依赖它——
230
+ # 故 race 零可观测影响。移除该误导性共享写:**每任务目标的权威来源**是会话隔离的
231
+ # ThoughtChain(goal 节点,session_id 经 contextvar 隔离)+ 事件驱动的 CognitiveState
232
+ # (execution.completed 的 active_goal)。共享单值 workspace 本就无法忠实承载 N 个并发目标。
233
+ return self._session_id
234
+
235
+ # ── ThoughtChain + Harness 集成推理步骤 ───────────────
236
+
237
+ def _reasoning_result(self, node):
238
+ """经统一 PolicyGate 做推理准入,返回 HarnessResult(与直接 harness.check 逐字一致)。
239
+ gate 经能力中枢惰性解析并缓存;无 gate(standalone/治理场景)则直接 harness.check。"""
240
+ gate = getattr(self, "_gate_cache", "unset")
241
+ if gate == "unset":
242
+ caps = getattr(self, "_caps", None)
243
+ gate = caps.get("policy_gate") if caps is not None else None
244
+ self._gate_cache = gate
245
+ if gate is not None:
246
+ return gate.check_reasoning(self.harness, node).raw
247
+ return self.harness.check(node)
248
+
249
+ def _think(
250
+ self,
251
+ content: str,
252
+ node_type="reasoning",
253
+ conf: float = 0.9,
254
+ mem_refs: list[str] = None,
255
+ tool_name: str = None,
256
+ entities: list[str] = None,
257
+ metadata: dict = None,
258
+ ) -> tuple[ThoughtNode, HarnessResult]:
259
+ """
260
+ 记录一个推理步骤,经过 Harness 校验
261
+ 如果 BLOCK,抛出 HarnessBlockedError
262
+ """
263
+ node = ThoughtNode.create(
264
+ session_id=self._session_id or "no_session",
265
+ agent_id=self.AGENT_ID,
266
+ type=node_type,
267
+ content=content,
268
+ conf=conf,
269
+ mem_refs=mem_refs or [],
270
+ parent_ids=[self._parent_node_id] if self._parent_node_id else [],
271
+ tool_name=tool_name,
272
+ entities=entities or [],
273
+ metadata=metadata,
274
+ )
275
+ # 锁只护「链插入 + 父指针挪动」这两步共享状态;Harness 判定在锁外,避免钩子阻塞并发工具。
276
+ with _THINK_LOCK:
277
+ self.thought_chain.add_node(node)
278
+
279
+ # Harness 校验(经统一 PolicyGate 做推理准入;拿到的仍是同一 HarnessResult,行为一致)
280
+ result = self._reasoning_result(node)
281
+
282
+ if result.is_blocked:
283
+ raise HarnessBlockedError(result, node)
284
+
285
+ # WARN:降低置信度但继续
286
+ if result.is_warned:
287
+ node.conf = max(node.conf - 0.15, 0.1)
288
+
289
+ with _THINK_LOCK:
290
+ self._parent_node_id = node.id
291
+ return node, result
292
+
293
+ @staticmethod
294
+ def _sanitize_history(history, max_turns: int = 12):
295
+ """清洗前端传来的多轮历史 → 合法 messages:只留 {role∈user/assistant, content:非空str},
296
+ 截到最近 max_turns 条。防注入非法角色/超长上下文。"""
297
+ out = []
298
+ for m in (history or [])[-max_turns:]:
299
+ if not isinstance(m, dict):
300
+ continue
301
+ role, content = m.get("role"), m.get("content")
302
+ if role in ("user", "assistant") and isinstance(content, str) and content.strip():
303
+ out.append({"role": role, "content": content})
304
+ return out
305
+
306
+ def _compact_context(self, messages: list, stage: str = "") -> list:
307
+ """LLM 调用前的上下文预算闸:超阈值则摘要/截断,任何失败都原样返回。
308
+
309
+ 历史:`core/compaction.py` 的 Compactor 早已实现(含 tool_calls 原子组切分、
310
+ LLM 摘要→部分摘要→截断三级降级),但**没有任何生产调用点**,实际生效的
311
+ 只有 `_sanitize_history` 的「按条数砍到 12 条」——长对话、多工具轮会
312
+ 直接把 provider 上下文撑爆。这里补上真实接线。
313
+
314
+ 预算由 FOXCLAW_CONTEXT_MAX_TOKENS 控制(默认 6000);置 0 可整体关闭。
315
+ """
316
+ import os as _os
317
+ try:
318
+ limit = int(_os.environ.get("FOXCLAW_CONTEXT_MAX_TOKENS", "6000") or 6000)
319
+ except Exception:
320
+ limit = 6000
321
+ if limit <= 0:
322
+ return messages
323
+ try:
324
+ from ..core.compaction import Compactor
325
+ compactor = Compactor(router=getattr(self, "router", None), max_tokens=limit)
326
+ compacted, res = compactor.maybe_compact(messages)
327
+ if res is not None:
328
+ try:
329
+ self._think(
330
+ content=(f"上下文压缩[{stage or 'llm'}] {res.method}: "
331
+ f"{res.original_count}→{res.compacted_count} 条, "
332
+ f"{res.original_tokens}→{res.compacted_tokens} token"
333
+ + (f"; {res.note}" if res.note else "")),
334
+ node_type="reflection", conf=0.7)
335
+ except Exception:
336
+ pass
337
+ return compacted
338
+ except Exception:
339
+ return messages
340
+
341
+ @staticmethod
342
+ def _attachments_context(attachments) -> str:
343
+ """把聊天附件拼成一段给 LLM 的上下文:文本类贴正文、其它降级为「文件名 + 元数据」提示。
344
+
345
+ 附件正文已在路由层 _sanitize_attachments 清洗(限长/限量);这里再按**不可信数据**隔离:
346
+ 附件可能是别人转发的文件,与用户当面对话不是同一信任级别,而手写中文定界符可被正文
347
+ 伪造。统一走 wrap_untrusted(随机 nonce 边界 + 破界 defang + 注入特征告警)。"""
348
+ if not attachments:
349
+ return ""
350
+ from ..core.injection_guard import wrap_untrusted
351
+ parts = []
352
+ for a in attachments:
353
+ if not isinstance(a, dict):
354
+ continue
355
+ name = a.get("name") or "文件"
356
+ text = (a.get("text") or "").strip()
357
+ if text:
358
+ block = wrap_untrusted(text, source="attachment")
359
+ parts.append(f"【附件「{name}」内容如下】\n{block}\n【附件「{name}」结束】")
360
+ else:
361
+ mime = a.get("mime") or "未知类型"
362
+ size = a.get("size") or 0
363
+ parts.append(f"【附件「{name}」({mime}, {size} 字节)】用户上传了此文件,"
364
+ f"但无法提取为可读文本;若需要其内容,请说明并提示用户改用文本/PDF 等可读格式。")
365
+ return "\n\n" + "\n\n".join(parts) if parts else ""
366
+
367
+ def stream_reply(self, task: str, stop_event=None, images: list = None, history: list = None,
368
+ attachments: list = None, on_thinking=None, on_evidence=None):
369
+ """流式直答路径(方案A):对话/问答/分析,不做多步编排(那走 run_task)。同步生成器,
370
+ 由端点在线程池迭代成 SSE。stop_event 置位(客户端断连)则提前终止,省 token。
371
+ images(可选)= 多模态图片,随问题发给视觉模型(模型无关,非视觉模型 router 层降级)。
372
+ history(可选)= 前端带来的最近几轮对话 → 拼在当前问题前,让多轮连贯。
373
+ attachments(可选)= 聊天附件 [{name,size,mime,url,text}]:text 为提取正文,拼进问题末尾
374
+ 供 LLM 阅读;text 为空的(二进制/图片)降级为「文件名 + 元数据」提示。
375
+ on_thinking(可选)= 推理增量回调。模型的原生 reasoning 与正文内嵌 <think> 块都经此
376
+ 出去,由端点作为独立 SSE `thinking` 事件送前端折叠显示;正文 (yield) 仍走净化。
377
+ on_evidence(可选)= 执行证据回调。工具轮的每步核验结论(verified/unverified/failed)
378
+ + 可核对凭据(来源链接、真实存在的文件)在本轮结束时一次性送出,由端点作为
379
+ SSE `evidence` 事件给前端展示 —— 「做成了」这句话从此可以点开看凭什么。
380
+
381
+ 两条路径(单一决策源之争,见 TOOL_USE_PLAN.md):
382
+ ① FOXCLAW_TOOL_USE=1 且 provider 支持 tools 且无图 → **LLM 自主工具循环**(_reply_with_tools),
383
+ 最终答案 chunk 输出;联网/召回由模型自己决策,不再靠关键词表。
384
+ ② 否则(默认 / 不支持 tools / 带图)→ **现有关键词增强链 + 真流式**(fallback,行为不变)。"""
385
+ from ..core import tools as _tools
386
+ from ..core import injection_guard as _ig
387
+ from ..core.model_router import _chunk_text
388
+ task = str(task or "")
389
+ _evidence_reset(self) # 每轮从零开账:上一轮的证据不能冒充这一轮
390
+ if attachments:
391
+ ctx = self._attachments_context(attachments)
392
+ if ctx:
393
+ task = task + ctx
394
+ hist = self._sanitize_history(history)
395
+ messages = hist + [{"role": "user", "content": task}]
396
+ parts = []
397
+ used_tools = False
398
+ guarded_out = False # 输出护栏是否触发(触发的污染回复不落库,防回流)
399
+
400
+ def _emit_thinking(delta: str) -> None:
401
+ """推理内容的最后一道关:思考也不许带密钥/系统提示原文(与正文同等级净化)。"""
402
+ if not delta or on_thinking is None:
403
+ return
404
+ try:
405
+ if _ig.scan_output(delta):
406
+ on_thinking("(推理内容已被安全护栏过滤)")
407
+ return
408
+ delta = _ig.redact_secrets(delta)
409
+ except Exception:
410
+ pass
411
+ try:
412
+ on_thinking(delta)
413
+ except Exception:
414
+ pass # 前端断连/回调异常不得影响正文生成
415
+
416
+ if _tools.tool_use_enabled() and self.router.supports_tools() and not images:
417
+ # 与 fallback 一致地注入偏好/技能/能力(否则开工具化会静默丢掉"总是注入偏好"的保证、
418
+ # 丢掉 SKILL.md 与能力知识)。**唯独不做 web_augment**——联网交给 LLM 自主调 web_search 工具。
419
+ try:
420
+ tmsgs = self._ability_augment(task, messages)
421
+ tmsgs = self._skill_augment(task, tmsgs)
422
+ tmsgs = self._pref_augment(tmsgs)
423
+ except Exception:
424
+ tmsgs = messages
425
+ _compact = getattr(self, "_compact_context", None) # 轻量替身对象无此方法
426
+ if _compact is not None:
427
+ tmsgs = _compact(tmsgs, "tools")
428
+ try:
429
+ final = self._reply_with_tools(task, tmsgs, stop_event=stop_event)
430
+ except Exception:
431
+ final = None
432
+ if final is not None: # None = 端点不支持 → 落到 fallback
433
+ # 工具路径有完整文本 → 流出前兜底(密钥脱敏/拦系统提示 dump);传 kernel 用逐字串比对更准
434
+ final, _oiss = _ig.guard_output(final, kernel=KERNEL_PROMPT)
435
+ guarded_out = bool(_oiss)
436
+ used_tools = True
437
+ for chunk in _chunk_text(final):
438
+ if stop_event is not None and stop_event.is_set():
439
+ break
440
+ parts.append(chunk)
441
+ yield chunk
442
+ if not used_tools: # fallback:关键词增强链 + 真流式(行为逐字不变)
443
+ try:
444
+ messages = self._web_augment(task, messages) # 增强都合并进"最后一条 user",历史不受影响
445
+ messages = self._ability_augment(task, messages)
446
+ messages = self._skill_augment(task, messages)
447
+ messages = self._pref_augment(messages)
448
+ except Exception:
449
+ messages = hist + [{"role": "user", "content": task}]
450
+ _compact = getattr(self, "_compact_context", None) # 轻量替身对象无此方法
451
+ if _compact is not None:
452
+ messages = _compact(messages, "chat")
453
+ _tail = "" # 滑窗尾缓冲:跨 chunk 边界的泄露检测只扫最近 _WIN 字,不再整段重扫(消 O(n²))
454
+ _WIN = 256 # 覆盖最长敏感串(密钥)跨 chunk 边界的情况
455
+ for chunk in self.router.stream(self.system_prompt, messages,
456
+ agent_id=self.AGENT_ID, stop_event=stop_event,
457
+ images=images,
458
+ on_thinking=_emit_thinking if on_thinking else None):
459
+ # 流出前检测(而非"yield 后才检测"):密钥/泄露所在 chunk 不再已发给用户才中断。
460
+ # 只扫"尾缓冲 + 新 chunk"(增量滑窗),命中则本 chunk 不发、整段 dump 止于此。
461
+ _probe = _tail + chunk
462
+ if _ig.scan_output(_probe):
463
+ guarded_out = True
464
+ break
465
+ _tail = _probe[-_WIN:]
466
+ parts.append(chunk)
467
+ yield chunk
468
+ if guarded_out:
469
+ _notice = "\n\n(⚠ 已触发输出安全护栏,中断后续内容。)"
470
+ parts.append(_notice)
471
+ yield _notice
472
+ reply = "".join(parts)
473
+ stopped = stop_event is not None and stop_event.is_set()
474
+ # 执行证据在正文之后一次性上报(晚于 delta、早于 done):前端拿到时答案已完整,
475
+ # 可以挂在答案下方供人核对;没有工具步的纯对话不产生任何证据事件。
476
+ if on_evidence is not None:
477
+ try:
478
+ _evs = _evidence_take(self)
479
+ if _evs:
480
+ on_evidence(_evs)
481
+ except Exception:
482
+ pass # 证据上报失败不得影响已生成的回答
483
+ # 落库前最终兜底(仅 fallback 路径需要):fallback 增量检查(无 kernel、只抓明显 dump)可能漏过
484
+ # 逐字复现的系统提示,这里用 kernel 逐字串比对再判一次,命中则不落库(防污染回流),不影响已流给
485
+ # 用户的内容。工具路径在流出前已 guard_output(kernel) 完整防护,无需重复扫(消冗余)。
486
+ if reply and not guarded_out and not used_tools and _ig.scan_output(reply, kernel=KERNEL_PROMPT):
487
+ guarded_out = True
488
+ try:
489
+ self._think(content=("流式直答:" + reply)[:500], node_type="reflection", conf=0.8)
490
+ except Exception:
491
+ pass
492
+ # 活动可观测:对话路径此前**不发任何事件** → /bus/history 恒空、前端"活动"页永远没内容
493
+ # (run_task 走 _emit_execution 会发,但直答/流式对话不经那条路)。这里补一条同源事件。
494
+ try:
495
+ _caps = getattr(self, "_caps", None)
496
+ _spine = _caps.get("event_spine") if _caps is not None else None
497
+ if _spine is not None and reply:
498
+ _spine.publish("chat.completed", {
499
+ "agent_id": self.AGENT_ID,
500
+ "task": (task or "")[:120],
501
+ "reply_chars": len(reply),
502
+ "guarded": bool(guarded_out),
503
+ }, source=self.AGENT_ID)
504
+ except Exception:
505
+ pass
506
+ # 写入记忆(与 /api/task 一致;MCP 场景由 _suppress_memory 兜住);断连中止/触发输出护栏则不写
507
+ #
508
+ # A 类修复:故障降级回复**禁止写入长期记忆** —— 把「模型失败/未执行」当成
509
+ # 「对话结论」沉淀,会让 HMR 后续召回持续复读故障文案(一次故障 → 长期污染)。
510
+ # 判据用 take_call_degraded()(per-call)而非全局 is_stub:is_stub=True 是
511
+ # 「从未配置模型」的离线态,那个场景下记录任务/占位仍有诊断价值,不该一刀切。
512
+ _degraded = False
513
+ try:
514
+ _degraded = bool(self.router.take_call_degraded())
515
+ except Exception:
516
+ _degraded = False
517
+ if reply and not stopped and not guarded_out and not _degraded:
518
+ try:
519
+ safe_reply = _ig.redact_secrets(reply) # 落库同样脱敏,防密钥沉进长期记忆
520
+ self._store_memory(f"对话:{task} → {safe_reply}"[:800], mem_type="conversation")
521
+ except Exception:
522
+ pass
523
+ self._maybe_store_preference(task) # 对话里的偏好表达 → 持久记(preference 类型)
524
+ elif _degraded and reply and not stopped:
525
+ # 降级:只留事件痕迹(可观测、可归因),不写记忆内容
526
+ try:
527
+ _spine = _caps.get("event_spine") if _caps is not None else None
528
+ if _spine is not None:
529
+ _spine.publish("chat.degraded", {
530
+ "agent_id": self.AGENT_ID,
531
+ "task": (task or "")[:120],
532
+ "reason": getattr(self.router, "degraded_reason", "")[:200],
533
+ }, source=self.AGENT_ID)
534
+ except Exception:
535
+ pass
536
+
537
+ # ── 统一任务生命周期(模板方法,P1:5 个 run_task 收敛为 1)──────
538
+ # 领域 Agent 不再各自重写 run_task,只实现 _execute()(召回后的领域步骤)。
539
+ # 会话开启、记忆召回、返回体结构、Harness 拦截处理都在此**唯一**定义,
540
+ # 保证所有 Agent 生命周期一致(修复"每 Agent 各写一份、行为漂移")。
541
+
542
+ _recall_topk: int = 5 # 召回条数(office 覆盖为 8)
543
+
544
+ def _task_entities(self, task: str) -> list:
545
+ """本任务的基线实体(用于 Harness 目标基线)。领域 Agent 覆盖。"""
546
+ return []
547
+
548
+ def _recall_query(self, task: str) -> str:
549
+ """记忆召回查询(领域 Agent 可加前缀,如财务/行程)。默认用原任务。"""
550
+ return task
551
+
552
+ # ── 【P1-1】任务生命周期钩子 ────────────────────────────────
553
+ # 崩溃/重启恢复记账(RestartRecovery)。会话开启后、管线执行前触发 started;任务终结时
554
+ # 触发 settled(done=正常终态含 Harness 兜底/中断;failed=未兜底异常)。run_task 与
555
+ # run_task_stream 共用 _run_task_core,故两条路径都会触发。
556
+ # 实现放在基类(而不是只让主管覆盖):P1-1 时只有主管接了线,领域 Agent 跑一半崩掉
557
+ # 在恢复扫描里根本不可见。没有 _recovery 的 standalone/测试 Agent → 直接跳过。
558
+ def _on_session_started(self, session_id: str, task: str, context: dict) -> None:
559
+ """会话已开、管线未跑:落 task_started(崩溃后才认得出"上次跑到一半")。"""
560
+ rec = getattr(self, "_recovery", None)
561
+ if rec is None:
562
+ return
563
+ try:
564
+ rec.task_started(task_id=session_id, description=task,
565
+ session_id=session_id,
566
+ channel=(context or {}).get("channel", ""))
567
+ except Exception:
568
+ pass
569
+
570
+ def _on_task_settled(self, session_id: str, status: str) -> None:
571
+ """任务终结(done/failed):落 task_finished,把该 task 从"进行中"摘掉。"""
572
+ rec = getattr(self, "_recovery", None)
573
+ if rec is None:
574
+ return
575
+ try:
576
+ rec.task_finished(session_id, status=status)
577
+ except Exception:
578
+ pass
579
+
580
+ def _record_stage(self, session_id: str, stage: str) -> None:
581
+ """管线 stage 级 checkpoint(fail-safe):断连/崩溃后能回答"跑到哪一步"。"""
582
+ rec = getattr(self, "_recovery", None)
583
+ if rec is None or not session_id:
584
+ return
585
+ try:
586
+ rec.record_step(session_id, {"kind": "stage", "name": stage})
587
+ except Exception:
588
+ pass
589
+
590
+ def _load_resume_cache(self, task_id: str) -> dict:
591
+ """装载上次运行已完成的工具步骤 → {步骤指纹: {"name", "result"}}。
592
+
593
+ 只收「成功 + 未被截断」的步:失败步复用了等于把失败当完成,截断结果复用了等于
594
+ 喂给模型半份事实 —— 宁可不复用(重跑),也不给错的上下文。
595
+ """
596
+ rec = getattr(self, "_recovery", None)
597
+ if rec is None or not task_id:
598
+ return {}
599
+ cache: dict = {}
600
+ try:
601
+ for st in rec.steps_of(task_id):
602
+ if st.get("kind") != "tool" or not st.get("key") or st.get("truncated"):
603
+ continue
604
+ res = st.get("result")
605
+ if isinstance(res, str) and res.strip():
606
+ cache[st["key"]] = {"name": st.get("name", ""), "result": res}
607
+ except Exception:
608
+ return {}
609
+ return cache
610
+
611
+ def _cognitive_pipeline(self):
612
+ """本 Agent 的认知管线(v2 ②)。默认 recall→act 两个 stage;领域 Agent 可覆盖以
613
+ 插入/替换 stage(后续步骤加 verify/reflect/plan/route/perceive)。缓存一份即可
614
+ (stage 无状态,只操作传入的 ctx)。"""
615
+ pipe = getattr(self, "_pipeline_cache", None)
616
+ if pipe is None:
617
+ from ..pipeline import CognitivePipeline, DEFAULT_STAGES
618
+ pipe = self._pipeline_cache = CognitivePipeline(DEFAULT_STAGES)
619
+ return pipe
620
+
621
+ async def run_task(self, task: str, context: dict = None, stop_event=None) -> dict:
622
+ """统一任务生命周期:开会话 → [认知管线:recall → act] → 组装返回。
623
+ Harness BLOCK 统一在此兜底为结构化错误。返回
624
+ {"result",...,"session_id","agent","thought_summary", **extra}。
625
+ v2 ②:执行体由 CognitivePipeline 驱动(Step1 抽壳,行为与 P1-1 模板逐字一致)。
626
+
627
+ stop_event(P3,opt-in,与 stream_reply 同一约定):threading/asyncio.Event,置位则让
628
+ 认知管线在下一 stage 边界 / reflect 重试前优雅停止;默认 None → 永不停,行为逐字不变。
629
+ 中断时返回体带 `"interrupted": True`,result 为已完成 stage 的部分产物。
630
+
631
+ 断点续跑(P1,opt-in):`context={"_resume_task_id": <上次 task_id>}` → 装载上次已完成
632
+ 的工具步骤,命中即复用结果不再重复执行;返回体带 `resumed_from` / `resumed_steps`
633
+ 说明续了哪一次、复用了多少步。"""
634
+ return await self._run_task_core(task, context, stop_event=stop_event, on_stage=None)
635
+
636
+ async def _run_task_core(self, task: str, context: dict = None,
637
+ stop_event=None, on_stage=None) -> dict:
638
+ """run_task 的执行核心 —— 供同步 `run_task` 与流式 `run_task_stream` **共用**,避免两份
639
+ 生命周期逻辑漂移。`on_stage`(D1):逐 stage 进度回调,透传给管线;None=同步路径不回调,
640
+ 与改前 run_task 行为逐字一致。"""
641
+ from ..pipeline import PipelineContext
642
+ context = context or {}
643
+ session_id = self.start_session(goal=task, entities=self._task_entities(task))
644
+ # 来源可信性:渠道入口会带 untrusted_source=True(见 channels/dispatcher)。本任务里的
645
+ # 写/发送/外发动作据此强制 HITL;本地 HTTP 对话不带此键 → 行为逐字不变。
646
+ _untrusted_set(self, bool(context.get("untrusted_source")))
647
+ # 【P1-1】生命周期钩子:会话已开、管线未跑。默认 no-op;主管借此做崩溃/重启
648
+ # 恢复记账(task_started)——恢复记账需先于执行落日志,故必须在管线之前。
649
+ self._on_session_started(session_id, task, context)
650
+ # E2:pre_reasoning(agent_start)钩子——推理开始前触发,可注入 additionalContext(前插进 act
651
+ # 的 LLM 上下文,见 _call_llm)。默认无钩子/无注入 → 行为逐字不变。hooks 由 Foxclaw 装配注入;
652
+ # standalone/测试 Agent 无 hooks 属性 → getattr None → 整个钩子逻辑跳过。
653
+ _hooks = getattr(self, "hooks", None)
654
+ _inject_parts = []
655
+ if _hooks is not None:
656
+ _hr = _hooks.fire("agent_start", "", {"task": task, "agent_id": self.AGENT_ID})
657
+ if _hr.additional_context:
658
+ _inject_parts.append(_hr.additional_context)
659
+ _pref = self._prefs_context() # 偏好优先注入(独立于语义 query → 每次推理总是注入)
660
+ if _pref:
661
+ _inject_parts.append(_pref)
662
+ # 偏好 + agent_start 钩子上下文合并进同一 _reasoning_inject → _call_llm 前插(复用 E2 机制)
663
+ _inject_tok = _reasoning_inject.set("\n\n".join(_inject_parts)) if _inject_parts else None
664
+ # 【P1 断点续跑】context={"_resume_task_id": <上次的 task_id/session_id>} 时,把上次
665
+ # 已完成的工具步骤装进 contextvar(root_key→结果):工具轮命中同指纹步骤直接复用结果、
666
+ # 不再重复执行。opt-in——不带该键 → 不装载、行为逐字不变。
667
+ _resume_tid = str(context.get("_resume_task_id") or "").strip()
668
+ _resume_cache = self._load_resume_cache(_resume_tid) if _resume_tid else {}
669
+ _resume_tok = _resume_steps.set(_resume_cache) if _resume_cache else None
670
+ ctx = PipelineContext(agent=self, task=task, context=context,
671
+ session_id=session_id, stop_event=stop_event)
672
+
673
+ def _stage_hook(name, _ctx):
674
+ """stage 边界:记一条 checkpoint(跑到哪一步),再转发给调用方的流式回调。"""
675
+ self._record_stage(session_id, name)
676
+ if on_stage is not None:
677
+ on_stage(name, _ctx)
678
+
679
+ try:
680
+ await self._cognitive_pipeline().run(ctx, on_stage=_stage_hook)
681
+ resp = {
682
+ "result": ctx.result,
683
+ "session_id": session_id,
684
+ "agent": self.AGENT_ID,
685
+ "thought_summary": self.thought_chain.get_summary(self.AGENT_ID),
686
+ **(ctx.extra or {}),
687
+ }
688
+ _plan = _plan_payload(ctx) # 计划可见:阻塞路径也回传,客户端无需再猜步骤
689
+ if _plan:
690
+ resp["plan"] = _plan
691
+ if _resume_cache: # 续跑透明化:告诉调用方"用了几个已完成步骤、从哪续的"
692
+ resp["resumed_from"] = _resume_tid
693
+ resp["resumed_steps"] = len(_resume_cache)
694
+ if ctx.interrupted: # P3:被 stop_event 提前中断 → 让调用方识别"部分完成"
695
+ resp["interrupted"] = True
696
+ # 【A·Step3·确认闸提升】act 若经 skill dispatch 且未成功(needs_confirmation/blocked/
697
+ # circuit_open/error),把该态提升为 run_task **顶层 status**,让调用方无需深挖 result
698
+ # 即可识别"待确认/未完成动作";needs_confirmation 附 pending_action(供确认后带
699
+ # _confirmed 重跑恢复执行)。**未完成 ⇒ 不作为已完成真值沉淀**(blocked=True,呼应
700
+ # "被拦截任务不沉淀记忆");成功/纯 LLM 路径行为逐字不变。
701
+ skill_status = (ctx.extra or {}).get("skill_status")
702
+ skill_incomplete = bool(skill_status) and skill_status != "done"
703
+ if skill_incomplete:
704
+ resp["status"] = skill_status
705
+ if skill_status == "needs_confirmation":
706
+ resp["pending_action"] = {
707
+ "skill_id": (ctx.extra or {}).get("skill_id"),
708
+ "task": task,
709
+ "message": (ctx.result or {}).get("message", "")
710
+ if isinstance(ctx.result, dict) else "",
711
+ "hint": "确认后带 context={'_confirmed': <skill_id>, ...} 重跑本任务以执行"
712
+ "(绑定到 skill_id 更安全;_confirmed=True 为概括确认)",
713
+ }
714
+ # 「未完成 ⇒ 不作为已完成真值沉淀」的 blocked 判定,须同时覆盖两类未完成:
715
+ # ① skill 未成功(needs_confirmation/blocked/circuit_open/error);
716
+ # ② 被 stop_event **中断**(B3 深审 G2):流式任务客户端断连 → 中断,ctx.result 只是
717
+ # 部分/空产物,绝不能当"已完成真值"沉淀进 HMR 共享记忆(呼应 P5/Principle07)。
718
+ # P3 引入 interrupted 时未联动此处;B3 的 /api/task/stream 是首个真正会 set stop_event
719
+ # 的生产调用方,才使这条路径可达。非中断路径 blocked 值与改前逐字一致。
720
+ blocked = skill_incomplete or ctx.interrupted
721
+ self._emit_execution(task, ctx.result, blocked=blocked, context=context)
722
+ self._maybe_store_preference(task) # 用户偏好表达 → 持久记(preference 类型,下次总是注入)
723
+ if _hooks is not None: # E2:task_complete(agent_end)——任务完成后只读观测/上报(对应范式 Stop)
724
+ _hooks.fire("agent_end", "", {"task": task, "agent_id": self.AGENT_ID,
725
+ "result": resp.get("result"),
726
+ "interrupted": bool(ctx.interrupted)})
727
+ self._on_task_settled(session_id, "done") # 【P1-1】正常终态(含中断/被拦提升)
728
+ return resp
729
+ except HarnessBlockedError as e:
730
+ self._emit_execution(
731
+ task, {"error": "harness_blocked", "reason": e.result.reason},
732
+ blocked=True, context=context)
733
+ if _hooks is not None: # task_complete 也覆盖被拦截结束(带 error 供上报)
734
+ _hooks.fire("agent_end", "", {"task": task, "agent_id": self.AGENT_ID,
735
+ "error": "harness_blocked"})
736
+ self._on_task_settled(session_id, "done") # 被拦截是已兜底的正常终态(非崩溃)
737
+ return {"error": "harness_blocked", "reason": e.result.reason,
738
+ "session_id": session_id}
739
+ except Exception:
740
+ self._on_task_settled(session_id, "failed") # 【P1-1】未兜底异常 → failed(供恢复)
741
+ raise
742
+ finally:
743
+ if _inject_tok is not None:
744
+ _reasoning_inject.reset(_inject_tok) # 复位注入 contextvar,不泄漏到后续任务
745
+ if _resume_tok is not None:
746
+ _resume_steps.reset(_resume_tok) # 续跑缓存不泄漏到后续任务
747
+
748
+ def run_task_stream(self, task: str, context: dict = None, stop_event=None):
749
+ """流式任务执行(D1):**同步 generator**,逐 stage 吐进度 + act 逐 token 出字 + 最终结果。
750
+ 产出事件(dict,逐条给 SSE):
751
+ {"stage": name} 每完成一个认知 stage(perceive→…→reflect)
752
+ {"plan": {...}} 紧跟 plan stage:显式计划 {steps, flow, multi_step}(计划可见)
753
+ {"delta": text} act 的 LLM 逐 token(仅纯 LLM 路径;skill dispatch 路径无 token)
754
+ {"result": {...}} 最终 run_task 返回体(含 interrupted/status/pending_action/plan 等)
755
+ {"error": msg} HarnessBlock 之外的异常(HarnessBlock 已被核心兜底进 result)
756
+
757
+ 与 `stream_reply` 同构(同步 generator,由 SSE 端点在后台线程迭代),复用共享的
758
+ `_run_task_core`,故生命周期/返回体与非流式 `run_task` **完全一致**——只是多了逐 stage /
759
+ 逐 token 的即时反馈。`stop_event`(客户端断连)一处置位、两处生效:停管线 stage(P3)+
760
+ 停 act 的 LLM 生成(省 token)。opt-in:不走此方法的 `run_task` 路径完全不受影响。
761
+
762
+ 实现:worker 线程跑 `asyncio.run(_run_task_core(..., on_stage=…))`(与 /api/task 一样在
763
+ 工作线程内起独立事件循环),stage 回调 + token 回调把事件塞进线程安全 `queue.Queue`,
764
+ 本 generator 主体 drain 该队列并 yield。`_act_stream` contextvar 在 worker 内设置,随
765
+ asyncio Task / to_thread 的上下文复制传播到 `_call_llm` → 触发 act 流式。"""
766
+ import queue as _queue, threading as _threading, asyncio as _asyncio
767
+ bridge: "_queue.Queue" = _queue.Queue()
768
+ _SENTINEL = object()
769
+
770
+ def _on_stage(name, _ctx):
771
+ bridge.put({"stage": name})
772
+ if name == "plan": # 计划显式化:steps 随事件送给前端/调用方
773
+ _plan = _plan_payload(_ctx)
774
+ if _plan:
775
+ bridge.put({"plan": _plan})
776
+
777
+ def _on_token(chunk):
778
+ bridge.put({"delta": chunk})
779
+
780
+ def _worker():
781
+ token = _act_stream.set({"on_token": _on_token, "stop_event": stop_event})
782
+ try:
783
+ resp = _asyncio.run(self._run_task_core(
784
+ task, context, stop_event=stop_event, on_stage=_on_stage))
785
+ bridge.put({"result": resp})
786
+ except Exception as e: # 核心已兜底 HarnessBlock;此处兜其余意外
787
+ bridge.put({"error": str(e)})
788
+ finally:
789
+ _act_stream.reset(token)
790
+ bridge.put(_SENTINEL) # 结束哨兵:保证 generator 必定收尾
791
+
792
+ _threading.Thread(target=_worker, daemon=True, name="task-stream").start()
793
+ while True:
794
+ item = bridge.get()
795
+ if item is _SENTINEL:
796
+ break
797
+ yield item
798
+
799
+ def _emit_execution(self, task, result, blocked: bool, context: dict):
800
+ """【P2·全路径触发】把本 Agent 的任务执行发到事件脊柱 → TruthPipeline(共享真值记录)
801
+ + CognitiveState/WS 可观测。修复"共享真值记录只在主管分发路径产生"的不一致:
802
+ 现在直连领域 Agent 的任务也统一走这条真值链。
803
+
804
+ 被主管分发/路由调用时(context['_no_emit']=True)跳过——由主管任务级别的统一
805
+ `_emit_execution` 承载(P1-1 后主管也走本模板),避免同一子任务双发。
806
+ 经能力中枢解析事件脊柱;无中枢(standalone/治理场景)则静默跳过。"""
807
+ if context.get("_no_emit"):
808
+ return
809
+ caps = getattr(self, "_caps", None)
810
+ spine = caps.get("event_spine") if caps is not None else None
811
+ if spine is None:
812
+ return
813
+ try:
814
+ spine.publish("execution.completed", {
815
+ "agent_id": self.AGENT_ID, "task": task,
816
+ "result": result, "blocked": blocked}, source=self.AGENT_ID)
817
+ except Exception:
818
+ pass
819
+
820
+ async def _execute(self, task: str, context: dict, session_id: str,
821
+ memories: list) -> tuple:
822
+ """领域执行步骤(召回之后 → 返回之前)。返回 (result, extra_dict)。
823
+ 领域 Agent 必须实现。SupervisorAgent 例外:它的 pipeline 不含 act stage
824
+ (路由分发由 supervisor_stages 的 dispatch stage 承担),不会走到本钩子。"""
825
+ raise NotImplementedError(f"{self.AGENT_ID} 未实现 _execute()")
826
+
827
+ # ── 编排协作接口(P3:领域知识归属领域 Agent,编排层只做通用聚合/路由)──────
828
+ def situation(self) -> dict:
829
+ """本 Agent 的领域态势:声明自己的 domain + 状态 + 告警。默认无领域态势。
830
+ 领域 Agent 覆盖。编排层据此**通用聚合**,不需懂任何领域内部 API。
831
+ 返回 {} 或 {"domain": str, "status": dict, "alerts": [ {domain,severity,msg} ]}。"""
832
+ return {}
833
+
834
+ def concede(self, req, verdict: str):
835
+ """【协商闭环】本 Agent 作为**发起方**被拒后,能否让步?
836
+
837
+ 返回修订后的 payload(dict)→ 编排层拿去再裁决一轮;返回 None → 让无可让,认输。
838
+ 默认 None:**不让步**,行为与协商前逐字一致 —— 所以给所有未实现本钩子的 Agent
839
+ 接上协商协议是零风险的(只有实现了 concede 的领域 Agent 才会真的多跑回合)。
840
+
841
+ 让步逻辑必须留在**发起方领域**(编排层不懂"改高铁/换酒店"是什么意思)。
842
+ """
843
+ return None
844
+
845
+ def handle_cross_request(self, req) -> None:
846
+ """处理跨 Agent 请求,用**本领域守门人**裁决(就地写 req.status / req.verdict)。
847
+ 默认:无特殊审批,放行。领域 Agent 覆盖。req 为鸭子类型(读 request_type/payload,
848
+ 写 status/verdict),故不与 orchestration 形成 import 依赖。"""
849
+ req.status = "approved"
850
+ req.verdict = "无需特殊审批"
851
+
852
+ # ── 通用状态查询 ───────────────────────────────────────
853
+
854
+ def get_status(self) -> dict:
855
+ return {
856
+ "agent_id": self.AGENT_ID,
857
+ "agent_name": self.AGENT_NAME,
858
+ "session_id": self._session_id,
859
+ "harness": {
860
+ "shadow_mode": self.harness.is_shadow_mode,
861
+ "thresholds": self.harness.thresholds,
862
+ "policy": self.harness.get_policy_summary(),
863
+ },
864
+ "thought_chain": self.thought_chain.get_summary(self.AGENT_ID),
865
+ }