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,103 @@
1
+ """文件路径准入 —— 把「调用方给的路径」收敛到允许的根目录内。
2
+
3
+ 为什么需要单独一层:领域导入接口(`/api/{finance,learning,itinerary}/import`)接受
4
+ 请求体里的 ``csv_path``。如果把它原样交给 ``open()``,HTTP 面就等于开放了「读本机
5
+ 任意文件」的能力 —— 而且读到的内容会被解析进知识库/账本,再从 ``/api`` 读回来,
6
+ 构成完整的信息泄露链(``csv_path=/etc/passwd`` 这类)。
7
+
8
+ 分层原则(与项目其余部分一致):
9
+ - **HTTP 边界是信任边界**,用户可控的路径必须校验;
10
+ - **本地库调用(CLI / 测试 / 示例)不校验**:调用方代码本身已有文件权限,
11
+ 给它加限制只会破坏用法,不增加安全性。
12
+
13
+ 允许的根 = ``FOXCLAW_DATA_DIR``(默认 ``./foxclaw_data``)
14
+ + ``FOXCLAW_IMPORT_ROOTS``(逗号或分号分隔,给「从下载目录导入」留出口)。
15
+
16
+ 校验用 ``resolve(strict=True)``:先解掉符号链接再比前缀,避免造一个指向 ``/etc``
17
+ 的软链绕过检查。比较用 ``normcase``:Windows 路径大小写不敏感,直接比字符串会漏。
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import os
23
+ import re
24
+ from pathlib import Path
25
+ from typing import Iterable, Sequence
26
+
27
+ __all__ = [
28
+ "DEFAULT_IMPORT_SUFFIXES",
29
+ "allowed_import_roots",
30
+ "is_within",
31
+ "resolve_import_path",
32
+ ]
33
+
34
+ _DEFAULT_DATA_DIR = "./foxclaw_data"
35
+ DEFAULT_IMPORT_SUFFIXES: tuple[str, ...] = (".csv", ".txt")
36
+
37
+
38
+ def _split_paths(raw: str) -> list[str]:
39
+ """按 , ; 切分路径列表(Windows 的盘符冒号不能当分隔符,故不用 :)。"""
40
+ return [x.strip().strip("\"'") for x in re.split(r"[;,]", raw or "") if x.strip()]
41
+
42
+
43
+ def allowed_import_roots() -> list[Path]:
44
+ """允许作为导入来源的根目录(绝对路径、已 resolve)。"""
45
+ raw = [os.environ.get("FOXCLAW_DATA_DIR") or _DEFAULT_DATA_DIR]
46
+ raw += _split_paths(os.environ.get("FOXCLAW_IMPORT_ROOTS", ""))
47
+ roots: list[Path] = []
48
+ for item in raw:
49
+ try:
50
+ roots.append(Path(item).expanduser().resolve())
51
+ except (OSError, RuntimeError):
52
+ continue # 无效配置不值当让请求 500,忽略即可(settings check 会提示)
53
+ return roots
54
+
55
+
56
+ def is_within(path: Path | str, roots: Sequence[Path]) -> bool:
57
+ """path 是否落在 roots 的任意一个之下(已按平台规范化大小写)。"""
58
+ try:
59
+ target = Path(path).resolve()
60
+ except (OSError, RuntimeError):
61
+ return False
62
+ for root in roots:
63
+ try:
64
+ if target.is_relative_to(root):
65
+ return True
66
+ except (OSError, RuntimeError):
67
+ continue
68
+ return False
69
+
70
+
71
+ def resolve_import_path(raw: str,
72
+ suffixes: Iterable[str] = DEFAULT_IMPORT_SUFFIXES,
73
+ roots: Sequence[Path] | None = None) -> Path:
74
+ """校验并返回可安全导入的绝对路径;不合法一律抛 ``ValueError``。
75
+
76
+ 依次检查:非空 → 存在且是普通文件 → 后缀在白名单 → 落在允许根内。
77
+ """
78
+ text = (raw or "").strip().strip("\"'")
79
+ if not text:
80
+ raise ValueError("路径为空")
81
+
82
+ try:
83
+ path = Path(text).expanduser().resolve(strict=True)
84
+ except (OSError, RuntimeError) as exc:
85
+ raise ValueError(f"文件不存在或不可访问: {text}") from exc
86
+
87
+ if not path.is_file():
88
+ raise ValueError(f"不是普通文件: {path}")
89
+
90
+ allowed_suffixes = {s.lower() for s in suffixes}
91
+ if allowed_suffixes and path.suffix.lower() not in allowed_suffixes:
92
+ raise ValueError(
93
+ f"不支持的扩展名 {path.suffix or '(无)'};允许:"
94
+ f"{' '.join(sorted(allowed_suffixes))}")
95
+
96
+ bases = list(roots) if roots is not None else allowed_import_roots()
97
+ if not is_within(path, bases):
98
+ shown = ", ".join(str(b) for b in bases) or "(无)"
99
+ raise ValueError(
100
+ f"路径不在允许的导入目录内: {path}\n"
101
+ f"允许的目录: {shown}\n"
102
+ f"如需从其它目录导入,设置 FOXCLAW_IMPORT_ROOTS=<目录>(多个用逗号分隔)")
103
+ return path
@@ -0,0 +1,362 @@
1
+ """
2
+ Foxclaw Core — 崩溃/重启恢复 (Restart Recovery)
3
+
4
+ 借鉴 OpenClaw 的原理,适配 Foxclaw。核心思想:
5
+ 系统跑长任务到一半崩溃/重启,那个任务不该凭空消失。
6
+ 启动时主动识别"上次还在进行、但现在没有活跃进程持有"的任务,
7
+ 诚实标记为"被中断",而不是装无事或留下半完成的脏状态。
8
+
9
+ 这与 Foxclaw"可审计、可信"的定位高度契合——可信系统不该悄悄吞掉崩溃的任务。
10
+
11
+ 机制:
12
+ · 任务开始 → 落一条 in_progress 记录(带 pid + 启动时间)
13
+ · 任务正常结束 → 标记 done(或从活跃集移除)
14
+ · 启动时 → 扫描所有 in_progress 记录:
15
+ - 若记录的 pid 不是当前进程、且那个 pid 已不存在 → 判定被中断
16
+ - 标记为 interrupted,交给上层决定(通知用户 / 尝试恢复)
17
+
18
+ 存储:JSONL 追加写(崩溃安全:写一条是原子 append,不会损坏整个文件)。
19
+ """
20
+ from __future__ import annotations
21
+ import json
22
+ import os
23
+ import sys
24
+ import time
25
+ from dataclasses import dataclass, field, asdict
26
+
27
+
28
+ def _pid_alive(pid: int) -> bool:
29
+ """判断进程是否存活(只查询、绝不影响目标进程)。跨平台:
30
+
31
+ · POSIX:os.kill(pid, 0) —— 发 0 号信号,内核只做权限/存在性检查,不真正投递。
32
+ · Windows:**决不能**用 os.kill(pid, 0)。CPython 在 Windows 上把非 CTRL 信号
33
+ 翻译成 TerminateProcess(handle, sig),因此 os.kill(pid, 0) 会真的以退出码 0
34
+ 终止该 pid(已实测:会误杀 pid 被 OS 复用后的无辜进程,且 os.kill 正常返回、
35
+ 不抛异常,连"探测失败"都察觉不到)。改用 OpenProcess 只申请查询权限、绝不
36
+ 终止,再用 GetExitCodeProcess 看是否仍 STILL_ACTIVE(259)。
37
+
38
+ 非破坏性是硬约束:本函数只在启动恢复扫描里做诊断记录,任何情况下都不得
39
+ 对被探测进程产生副作用。
40
+ """
41
+ if pid <= 0:
42
+ return False
43
+
44
+ if sys.platform == "win32":
45
+ import ctypes
46
+ from ctypes import wintypes
47
+ PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
48
+ STILL_ACTIVE = 259
49
+ kernel32 = ctypes.windll.kernel32
50
+ handle = kernel32.OpenProcess(
51
+ PROCESS_QUERY_LIMITED_INFORMATION, False, pid)
52
+ if not handle:
53
+ return False # 打不开句柄 = 进程不存在(或无权限)→ 保守判不存活
54
+ try:
55
+ code = wintypes.DWORD()
56
+ if not kernel32.GetExitCodeProcess(handle, ctypes.byref(code)):
57
+ return False
58
+ # STILL_ACTIVE=259 表示仍在运行。极少数进程恰以 259 退出会被误判存活,
59
+ # 但这是"只读探测"的可接受代价——绝不误杀才是首要目标(psutil 同此取舍)。
60
+ return code.value == STILL_ACTIVE
61
+ finally:
62
+ kernel32.CloseHandle(handle)
63
+
64
+ # POSIX
65
+ try:
66
+ os.kill(pid, 0)
67
+ return True
68
+ except ProcessLookupError:
69
+ return False
70
+ except PermissionError:
71
+ return True # 存在但无权限 → 视为存活
72
+ except Exception:
73
+ return False
74
+
75
+
76
+ @dataclass
77
+ class TaskRecord:
78
+ task_id: str
79
+ description: str
80
+ pid: int
81
+ started: float
82
+ status: str = "in_progress" # in_progress / done / failed / interrupted
83
+ session_id: str = ""
84
+ channel: str = ""
85
+ finished: float = 0.0
86
+ boot_id: str = "" # 哪次启动产生的(防 pid 复用误判)
87
+ # 【P2-4】恢复策略字段
88
+ retryable: bool = False # 调用方标记"可安全重试"(无副作用/幂等)
89
+ start_count: int = 1 # 同一 task_id 被 start 的次数(重复 start 保留最后一次)
90
+
91
+ def to_dict(self) -> dict:
92
+ return asdict(self)
93
+
94
+
95
+ @dataclass
96
+ class RecoveryPlan:
97
+ """【P2-4】恢复计划:把中断任务分类,调用方(API 层/前端)据此决定动作。"""
98
+ retryable: list = field(default_factory=list) # 可重试的 task_id
99
+ notify_user: list = field(default_factory=list) # 需通知用户的(来自某个渠道)
100
+ needs_manual: list = field(default_factory=list) # 需人工处理的(保守默认)
101
+ details: list = field(default_factory=list) # [{task_id,description,channel,...,action}]
102
+
103
+ def to_dict(self) -> dict:
104
+ return asdict(self)
105
+
106
+
107
+ class RestartRecovery:
108
+ """任务生命周期日志 + 启动恢复扫描。"""
109
+
110
+ def __init__(self, log_path: str, current_pid: int = None, boot_id: str = None,
111
+ spine=None, fsync: bool = True):
112
+ self.log_path = log_path
113
+ self.current_pid = current_pid or os.getpid()
114
+ # boot_id:每次启动唯一。任务记录带上它,扫描时"非本次启动的 in_progress"
115
+ # 即属于上一次运行 —— 比单纯靠 pid 可靠(pid 会被 OS 复用)。
116
+ import uuid
117
+ self.boot_id = boot_id or uuid.uuid4().hex[:16]
118
+ self._active: dict[str, TaskRecord] = {} # 当前进程内进行中的任务
119
+ self._spine = spine # 【P2-4】审计脊柱(可空,None 静默)
120
+ self._fsync = fsync # 【P2-4】fsync 可配(默认开=崩溃安全;高频场景可关)
121
+ os.makedirs(os.path.dirname(log_path) or ".", exist_ok=True)
122
+
123
+ # ── 任务生命周期 ──────────────────────────────────────
124
+ def task_started(self, task_id: str, description: str,
125
+ session_id: str = "", channel: str = "",
126
+ retryable: bool = False) -> TaskRecord:
127
+ rec = TaskRecord(
128
+ task_id=task_id, description=description[:200],
129
+ pid=self.current_pid, started=time.time(),
130
+ session_id=session_id, channel=channel, boot_id=self.boot_id,
131
+ retryable=retryable)
132
+ self._active[task_id] = rec
133
+ self._append({"event": "start", **rec.to_dict()})
134
+ return rec
135
+
136
+ def task_finished(self, task_id: str, status: str = "done"):
137
+ rec = self._active.pop(task_id, None)
138
+ if rec:
139
+ rec.status = status
140
+ rec.finished = time.time()
141
+ self._append({"event": "finish", "task_id": task_id,
142
+ "status": status, "pid": self.current_pid,
143
+ "finished": time.time()})
144
+
145
+ # ── 步骤级 checkpoint(P1 断点可续)─────────────────────
146
+ def record_step(self, task_id: str, step: dict) -> None:
147
+ """记一条「已完成的步骤」。管线每完成一个 stage、工具轮每完成一个工具调用各落一条,
148
+ 崩溃/断连后 `steps_of()` 能回答「跑到哪一步」,也让续跑知道哪些步骤不必重做。
149
+
150
+ fail-safe:写日志失败绝不影响任务执行(与任务生命周期记账同一定位)。
151
+ """
152
+ if not task_id or not isinstance(step, dict):
153
+ return
154
+ self._append({"event": "step", "task_id": task_id,
155
+ "ts": time.time(), **step})
156
+
157
+ def steps_of(self, task_id: str, limit: int = 200) -> list:
158
+ """回放日志,取该 task 已记录的步骤(写入顺序;只留最近 limit 条)。
159
+
160
+ 只读、容错:文件不存在/行损坏/中途写一半都跳过,不抛异常。
161
+ """
162
+ out: list = []
163
+ if not task_id or not os.path.exists(self.log_path):
164
+ return out
165
+ try:
166
+ with open(self.log_path, encoding="utf-8") as f:
167
+ for line in f:
168
+ line = line.strip()
169
+ if not line:
170
+ continue
171
+ try:
172
+ ev = json.loads(line)
173
+ except Exception:
174
+ continue # 末行可能写一半(append 崩溃),跳过
175
+ if ev.get("event") == "step" and ev.get("task_id") == task_id:
176
+ out.append({k: v for k, v in ev.items()
177
+ if k not in ("event", "task_id")})
178
+ except Exception:
179
+ return out
180
+ return out[-limit:] if limit else out
181
+
182
+ # ── 启动恢复扫描 ──────────────────────────────────────
183
+ def scan_interrupted(self) -> list[TaskRecord]:
184
+ """启动时调用:重建日志状态,找出被中断的任务。
185
+
186
+ 判定(双重,避免 pid 复用误判):
187
+ 1. 任务仍是 in_progress
188
+ 2. 它的 boot_id 不是本次启动(即属于上一次运行)
189
+ boot_id 随机不复用,所以"非本次启动的进行中任务"必然是被中断的。
190
+ """
191
+ records = self._replay()
192
+ interrupted = []
193
+ for task_id, rec in records.items():
194
+ if rec.status != "in_progress":
195
+ continue
196
+ if rec.boot_id == self.boot_id:
197
+ continue # 本次启动自己的任务,跳过
198
+ # 非本次启动的 in_progress = 上次运行遗留 = 被中断
199
+ # (pid 是否存活作为辅助信息记录,但不作为唯一判据)
200
+ rec.status = "interrupted"
201
+ interrupted.append(rec)
202
+ self._append({"event": "interrupted", "task_id": task_id,
203
+ "pid": rec.pid, "prev_boot_id": rec.boot_id,
204
+ "pid_still_alive": _pid_alive(rec.pid),
205
+ "detected_by": self.boot_id,
206
+ "detected_at": time.time()})
207
+ return interrupted
208
+
209
+ def _replay(self) -> dict[str, TaskRecord]:
210
+ """回放日志,重建每个 task 的最终状态。"""
211
+ records: dict[str, TaskRecord] = {}
212
+ if not os.path.exists(self.log_path):
213
+ return records
214
+ try:
215
+ with open(self.log_path, encoding="utf-8") as f:
216
+ for line in f:
217
+ line = line.strip()
218
+ if not line:
219
+ continue
220
+ try:
221
+ ev = json.loads(line)
222
+ except Exception:
223
+ continue # 跳过损坏行(append 模式下末行可能写一半)
224
+ self._apply_event(records, ev)
225
+ except Exception:
226
+ pass
227
+ return records
228
+
229
+ @staticmethod
230
+ def _apply_event(records: dict, ev: dict):
231
+ tid = ev.get("task_id")
232
+ if not tid:
233
+ return
234
+ kind = ev.get("event")
235
+ if kind == "start":
236
+ # 【P2-4】同一 task_id 多次 start:保留最后一次,但累计 start_count
237
+ existing = records.get(tid)
238
+ start_count = (existing.start_count + 1) if existing else 1
239
+ records[tid] = TaskRecord(
240
+ task_id=tid, description=ev.get("description", ""),
241
+ pid=ev.get("pid", 0), started=ev.get("started", 0),
242
+ status="in_progress", session_id=ev.get("session_id", ""),
243
+ channel=ev.get("channel", ""), boot_id=ev.get("boot_id", ""),
244
+ retryable=ev.get("retryable", False),
245
+ start_count=start_count)
246
+ elif kind in ("finish", "interrupted") and tid in records:
247
+ records[tid].status = ("interrupted" if kind == "interrupted"
248
+ else ev.get("status", "done"))
249
+ if kind == "finish":
250
+ records[tid].finished = ev.get("finished", 0)
251
+
252
+ def plan_recovery(self, interrupted: list = None) -> RecoveryPlan:
253
+ """【P2-4】给中断任务分类,返回恢复计划(不改 scan_interrupted 的返回,兼容)。
254
+
255
+ 分类规则:
256
+ · retryable=True → retryable(调用方标记过可安全重试)
257
+ · channel 非空 → notify_user(任务来自某个渠道,应通知该渠道用户)
258
+ · 其他 → needs_manual(保守:人工处理)
259
+ 每个分类决定都发 recovery.plan 审计事件(有 spine 时)。
260
+ """
261
+ if interrupted is None:
262
+ interrupted = self.scan_interrupted()
263
+ plan = RecoveryPlan()
264
+ for rec in interrupted:
265
+ if rec.retryable:
266
+ plan.retryable.append(rec.task_id)
267
+ action = "retry"
268
+ elif rec.channel:
269
+ plan.notify_user.append(rec.task_id)
270
+ action = "notify_user"
271
+ else:
272
+ plan.needs_manual.append(rec.task_id)
273
+ action = "manual"
274
+ plan.details.append({
275
+ "task_id": rec.task_id,
276
+ "description": rec.description,
277
+ "channel": rec.channel,
278
+ "session_id": rec.session_id,
279
+ "start_count": rec.start_count,
280
+ "reason": "crash_or_restart",
281
+ "action": action,
282
+ })
283
+ self._emit_recovery_event(rec, action)
284
+ return plan
285
+
286
+ def _emit_recovery_event(self, rec: TaskRecord, action: str):
287
+ if self._spine is None:
288
+ return
289
+ try:
290
+ self._spine.publish("recovery.plan", {
291
+ "task_id": rec.task_id,
292
+ "action": action,
293
+ "channel": rec.channel,
294
+ "boot_id": self.boot_id,
295
+ }, source="recovery")
296
+ except Exception:
297
+ pass
298
+
299
+ def _append(self, entry: dict):
300
+ """追加一条日志(崩溃安全:单行 append)。fsync 可配:默认开(崩溃安全),
301
+ 高频场景可关(牺牲最后一条的持久性换吞吐)。"""
302
+ try:
303
+ with open(self.log_path, "a", encoding="utf-8") as f:
304
+ f.write(json.dumps(entry, ensure_ascii=False) + "\n")
305
+ f.flush()
306
+ if self._fsync:
307
+ os.fsync(f.fileno()) # 落盘,防崩溃丢最后一条
308
+ except Exception:
309
+ pass # 日志失败不影响主流程
310
+
311
+ def compact_log(self, keep_recent_days: float = 7):
312
+ """压实日志:丢弃已完成且超过 N 天的记录,防文件无限膨胀。
313
+ 【P2-4】保留判据用 max(started, finished):旧逻辑只看 started,
314
+ 会丢"开始得早但刚完成"的记录(其 finish 审计线索一并丢失)。
315
+ 步骤级 checkpoint(step 事件)属于被保留的任务 → 一并搬运,否则压实即丢断点。"""
316
+ records = self._replay()
317
+ cutoff = time.time() - keep_recent_days * 86400
318
+ keep = []
319
+ for rec in records.values():
320
+ # 保留:仍在进行的,或最近有活动(开始或完成)的
321
+ last_activity = max(rec.started, rec.finished or 0)
322
+ if rec.status == "in_progress" or last_activity >= cutoff:
323
+ keep.append({"event": "start", **rec.to_dict()})
324
+ if rec.status != "in_progress":
325
+ keep.append({"event": "finish", "task_id": rec.task_id,
326
+ "status": rec.status, "finished": rec.finished})
327
+ keep_ids = {e["task_id"] for e in keep if e.get("event") == "start"}
328
+ steps = self._raw_steps(keep_ids)
329
+ try:
330
+ tmp = self.log_path + ".tmp"
331
+ with open(tmp, "w", encoding="utf-8") as f:
332
+ for e in keep:
333
+ f.write(json.dumps(e, ensure_ascii=False) + "\n")
334
+ for e in steps: # 保留任务的 step 事件随记录一起搬走
335
+ f.write(json.dumps(e, ensure_ascii=False) + "\n")
336
+ os.replace(tmp, self.log_path)
337
+ except Exception:
338
+ pass
339
+
340
+ def _raw_steps(self, task_ids: set) -> list:
341
+ """扫原始日志取指定任务的 step 事件(compact 用;task_ids 为空直接返回)。"""
342
+ out: list = []
343
+ if not task_ids or not os.path.exists(self.log_path):
344
+ return out
345
+ try:
346
+ with open(self.log_path, encoding="utf-8") as f:
347
+ for line in f:
348
+ line = line.strip()
349
+ if not line:
350
+ continue
351
+ try:
352
+ ev = json.loads(line)
353
+ except Exception:
354
+ continue
355
+ if ev.get("event") == "step" and ev.get("task_id") in task_ids:
356
+ out.append(ev)
357
+ except Exception:
358
+ return out
359
+ return out
360
+
361
+ def active_count(self) -> int:
362
+ return len(self._active)
@@ -0,0 +1,144 @@
1
+ """
2
+ 认知调度器 (Cognitive Scheduler)
3
+
4
+ 把任务调度建模成真正的 OS 调度:特征化 → 打分 → 负载调整 → 路由 + 抢占判定。
5
+ 借鉴 Codex 版 Foxclaw 的 CognitiveScheduler 思想,适配本项目的五 Agent 架构。
6
+
7
+ 主管在分诊前先过这一层,得到:优先级、执行模式(抢占/标准)、预算、调度理由。
8
+ 让主管从"关键词分诊"升级为"特征感知 + 负载感知的 OS 式调度器"。
9
+ """
10
+ from __future__ import annotations
11
+ from dataclasses import dataclass
12
+
13
+
14
+ @dataclass
15
+ class TaskIntent:
16
+ """任务意图:粗粒度类别 + 三个亲和度提示。"""
17
+ category: str # reasoning / memory / workflow / tool
18
+ confidence: float
19
+ memory_affinity: float # 记忆依赖度
20
+ reasoning_depth: float # 推理深度提示
21
+
22
+
23
+ @dataclass
24
+ class TaskFeatures:
25
+ complexity: float # 复杂度
26
+ memory_dependency: float # 记忆依赖
27
+ tool_cost: float # 工具成本
28
+ reasoning_depth: float # 推理深度
29
+ urgency: float # 紧急度
30
+ expected_runtime: float # 预期耗时
31
+
32
+
33
+ @dataclass
34
+ class SystemContext:
35
+ """系统负载上下文。主管据此调整优先级、决定是否抢占。"""
36
+ active_agents: int = 0 # 当前活跃 Agent 数
37
+ queue_depth: int = 0 # 待处理任务队列深度
38
+ system_load: float = 0.1 # 系统负载 0-1
39
+
40
+
41
+ @dataclass
42
+ class ScheduleDecision:
43
+ priority: float # 最终优先级 0-1
44
+ route_hint: str # 路由倾向:agent / tool / workflow
45
+ execution_mode: str # preemptive(抢占) / standard(标准)
46
+ preemptible: bool # 是否可触发抢占
47
+ expected_budget: float # 预期预算(耗时×优先级修正)
48
+ intent: TaskIntent = None
49
+ features: TaskFeatures = None
50
+ preemption_reason: str = None
51
+ reason: str = ""
52
+
53
+ def to_dict(self) -> dict:
54
+ return {
55
+ "priority": self.priority, "route_hint": self.route_hint,
56
+ "execution_mode": self.execution_mode, "preemptible": self.preemptible,
57
+ "expected_budget": self.expected_budget,
58
+ "preemption_reason": self.preemption_reason, "reason": self.reason,
59
+ "intent": self.intent.category if self.intent else None,
60
+ }
61
+
62
+
63
+ class CognitiveScheduler:
64
+ """意图感知的认知调度器。"""
65
+
66
+ # 意图识别关键词(粗分类)
67
+ _REASONING = ["规划", "计划", "分析", "设计", "推理", "为什么", "怎么办", "策略", "方案"]
68
+ _MEMORY = ["记得", "记忆", "之前", "上次", "回忆", "查一下我", "我说过"]
69
+ _WORKFLOW = ["然后", "接着", "之后", "依次", "批量", "流程", "再然后", "下一步"]
70
+
71
+ def parse_intent(self, task: str) -> TaskIntent:
72
+ t = (task or "").lower()
73
+ if any(k in t for k in self._REASONING):
74
+ return TaskIntent("reasoning", 0.8, 0.6, 0.95)
75
+ if any(k in t for k in self._MEMORY):
76
+ return TaskIntent("memory", 0.85, 0.9, 0.4)
77
+ if any(k in t for k in self._WORKFLOW):
78
+ return TaskIntent("workflow", 0.75, 0.5, 0.6)
79
+ return TaskIntent("tool", 0.7, 0.2, 0.3)
80
+
81
+ def extract_features(self, task: str, intent: TaskIntent) -> TaskFeatures:
82
+ complexity, tool_cost, urgency, runtime = 0.4, 0.3, 0.6, 0.5
83
+ if intent.category == "reasoning":
84
+ complexity, tool_cost, urgency, runtime = 0.9, 0.4, 0.8, 0.9
85
+ elif intent.category == "workflow":
86
+ complexity, tool_cost, urgency, runtime = 0.7, 0.6, 0.65, 0.8
87
+ elif intent.category == "memory":
88
+ complexity, tool_cost, urgency, runtime = 0.5, 0.2, 0.55, 0.6
89
+ # 紧急词加成
90
+ if any(w in task for w in ["紧急", "立刻", "马上", "尽快", "急"]):
91
+ urgency = min(1.0, urgency + 0.3)
92
+ return TaskFeatures(complexity, intent.memory_affinity, tool_cost,
93
+ intent.reasoning_depth, urgency, runtime)
94
+
95
+ def score(self, f: TaskFeatures) -> float:
96
+ """基础分:加权特征。"""
97
+ weighted = (f.complexity * 0.3 + f.memory_dependency * 0.2
98
+ + f.tool_cost * 0.15 + f.reasoning_depth * 0.25
99
+ + f.urgency * 0.1)
100
+ return round(weighted, 3)
101
+
102
+ def adjust(self, base: float, ctx: SystemContext, intent: TaskIntent,
103
+ f: TaskFeatures) -> float:
104
+ """负载调整:负载/队列越深,优先级被压低;紧急/记忆亲和度抬升。"""
105
+ memory_affinity = max(0.25, intent.memory_affinity)
106
+ load_penalty = 1 + ctx.system_load + (ctx.queue_depth * 0.05)
107
+ urgency_boost = max(0.5, f.urgency)
108
+ final = (base * urgency_boost * memory_affinity) / load_penalty
109
+ return min(1.0, round(final, 3))
110
+
111
+ def route_hint(self, priority: float, intent: TaskIntent) -> str:
112
+ if intent.category == "workflow":
113
+ return "workflow"
114
+ if intent.category == "reasoning":
115
+ return "agent"
116
+ if intent.category == "memory" and priority < 0.3:
117
+ return "tool"
118
+ return "agent" if priority > 0.35 else "tool"
119
+
120
+ def preemption_reason(self, priority: float, ctx: SystemContext) -> str:
121
+ # 阈值匹配实际优先级范围(本调度器优先级多在 0.4 以内,
122
+ # 原 0.55/0.7 几乎不可达,会让抢占形同虚设)
123
+ if priority > 0.35 and ctx.active_agents >= 3:
124
+ return "高优先级 + Agent 压力大"
125
+ if priority > 0.4 and ctx.queue_depth >= 2:
126
+ return "高优先级 + 队列积压"
127
+ return None
128
+
129
+ def schedule(self, task: str, ctx: SystemContext = None) -> ScheduleDecision:
130
+ """主入口:给一个任务做完整调度决策。"""
131
+ ctx = ctx or SystemContext()
132
+ intent = self.parse_intent(task)
133
+ features = self.extract_features(task, intent)
134
+ base = self.score(features)
135
+ priority = self.adjust(base, ctx, intent, features)
136
+ route = self.route_hint(priority, intent)
137
+ preempt = self.preemption_reason(priority, ctx)
138
+ mode = "preemptive" if priority > 0.4 else "standard"
139
+ return ScheduleDecision(
140
+ priority=priority, route_hint=route, execution_mode=mode,
141
+ preemptible=preempt is not None,
142
+ expected_budget=round(features.expected_runtime * (1 + priority), 2),
143
+ intent=intent, features=features, preemption_reason=preempt,
144
+ reason=f"意图={intent.category} 优先级={priority} 模式={mode}")