foxclaw 0.1.0 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (514) hide show
  1. package/README.md +54 -29
  2. package/bin/foxclaw.cjs +51 -0
  3. package/index.js +4 -10
  4. package/package.json +46 -38
  5. package/python/.dockerignore +35 -0
  6. package/python/.env.example +173 -0
  7. package/python/.gitattributes +46 -0
  8. package/python/.github/workflows/ci.yml +167 -0
  9. package/python/.github/workflows/release.yml +183 -0
  10. package/python/CHANGELOG.md +57 -0
  11. package/python/CHANGELOG_CN.md +57 -0
  12. package/python/Dockerfile +64 -0
  13. package/python/LICENSE +21 -0
  14. package/python/MANIFEST.in +14 -0
  15. package/python/README.md +201 -0
  16. package/python/README_CN.md +336 -0
  17. package/python/bench/bm25_audit.py +186 -0
  18. package/python/bench/bm25_regression.py +177 -0
  19. package/python/bench/bm25_stress.py +155 -0
  20. package/python/bench/cd_audit.py +215 -0
  21. package/python/bench/chaos.py +140 -0
  22. package/python/bench/concurrency.py +103 -0
  23. package/python/bench/e2e_real_models.py +94 -0
  24. package/python/bench/llm_retry_test.py +241 -0
  25. package/python/bench/load_test.py +193 -0
  26. package/python/bench/vector_ivf_upgrade.py +128 -0
  27. package/python/docs/CHANNELS.md +195 -0
  28. package/python/docs/GOVERNANCE.md +164 -0
  29. package/python/docs/RELEASE.md +110 -0
  30. package/python/docs/RUN.md +75 -0
  31. package/python/docs/RUN_CN.md +121 -0
  32. package/python/docs//351/205/215/347/275/256/346/214/207/345/215/227.md +152 -0
  33. package/python/evals/__init__.py +0 -0
  34. package/python/evals/governance/README.md +61 -0
  35. package/python/evals/governance/__init__.py +0 -0
  36. package/python/evals/governance/cases.py +115 -0
  37. package/python/evals/governance/compare_embeddings.py +94 -0
  38. package/python/evals/governance/convergence.svg +11 -0
  39. package/python/evals/governance/report.json +371 -0
  40. package/python/evals/governance/run_evals.py +368 -0
  41. package/python/examples/governance_middleware_demo.py +72 -0
  42. package/python/examples/langgraph_governance_demo.py +104 -0
  43. package/python/examples/sample_itinerary.csv +6 -0
  44. package/python/examples/sample_knowledge.csv +9 -0
  45. package/python/examples/sample_transactions.csv +14 -0
  46. package/python/foxclaw/__init__.py +408 -0
  47. package/python/foxclaw/agents/__init__.py +12 -0
  48. package/python/foxclaw/agents/agents.py +35 -0
  49. package/python/foxclaw/agents/base.py +794 -0
  50. package/python/foxclaw/agents/impl/__init__.py +1 -0
  51. package/python/foxclaw/agents/impl/_helpers.py +70 -0
  52. package/python/foxclaw/agents/impl/finance.py +231 -0
  53. package/python/foxclaw/agents/impl/office.py +350 -0
  54. package/python/foxclaw/agents/impl/supervisor.py +592 -0
  55. package/python/foxclaw/agents/mixins/_context.py +128 -0
  56. package/python/foxclaw/agents/mixins/augment.py +143 -0
  57. package/python/foxclaw/agents/mixins/llm.py +67 -0
  58. package/python/foxclaw/agents/mixins/memory.py +41 -0
  59. package/python/foxclaw/agents/mixins/preference.py +131 -0
  60. package/python/foxclaw/agents/mixins/prompt.py +71 -0
  61. package/python/foxclaw/agents/mixins/skillbox.py +58 -0
  62. package/python/foxclaw/agents/mixins/tool_exec.py +417 -0
  63. package/python/foxclaw/api/__init__.py +224 -0
  64. package/python/foxclaw/api/_state.py +43 -0
  65. package/python/foxclaw/api/bootstrap.py +96 -0
  66. package/python/foxclaw/api/middleware.py +294 -0
  67. package/python/foxclaw/api/routes/ahs.py +404 -0
  68. package/python/foxclaw/api/routes/attachments.py +116 -0
  69. package/python/foxclaw/api/routes/browser.py +35 -0
  70. package/python/foxclaw/api/routes/domain.py +306 -0
  71. package/python/foxclaw/api/routes/knowledge.py +145 -0
  72. package/python/foxclaw/api/routes/llm_admin.py +449 -0
  73. package/python/foxclaw/api/routes/memory.py +105 -0
  74. package/python/foxclaw/api/routes/orchestration.py +39 -0
  75. package/python/foxclaw/api/routes/recovery.py +44 -0
  76. package/python/foxclaw/api/routes/review.py +266 -0
  77. package/python/foxclaw/api/routes/search.py +33 -0
  78. package/python/foxclaw/api/routes/system.py +311 -0
  79. package/python/foxclaw/api/routes/task.py +263 -0
  80. package/python/foxclaw/api/routes/ws.py +64 -0
  81. package/python/foxclaw/api/spine.py +52 -0
  82. package/python/foxclaw/browser/__init__.py +13 -0
  83. package/python/foxclaw/browser/action/__init__.py +1 -0
  84. package/python/foxclaw/browser/action/action_executor.py +50 -0
  85. package/python/foxclaw/browser/action/action_types.py +44 -0
  86. package/python/foxclaw/browser/action/action_verifier.py +68 -0
  87. package/python/foxclaw/browser/action/url_policy.py +143 -0
  88. package/python/foxclaw/browser/adapter/__init__.py +1 -0
  89. package/python/foxclaw/browser/adapter/base_adapter.py +23 -0
  90. package/python/foxclaw/browser/adapter/web_adapter.py +46 -0
  91. package/python/foxclaw/browser/driver/__init__.py +1 -0
  92. package/python/foxclaw/browser/driver/base_driver.py +61 -0
  93. package/python/foxclaw/browser/driver/playwright_driver.py +170 -0
  94. package/python/foxclaw/browser/perception/__init__.py +1 -0
  95. package/python/foxclaw/browser/perception/dom_extractor.py +42 -0
  96. package/python/foxclaw/browser/perception/element_indexer.py +56 -0
  97. package/python/foxclaw/browser/perception/semantic_reducer.py +33 -0
  98. package/python/foxclaw/browser/perception/snapshot.py +29 -0
  99. package/python/foxclaw/browser/service.py +144 -0
  100. package/python/foxclaw/browser/session/__init__.py +1 -0
  101. package/python/foxclaw/browser/session/session_manager.py +57 -0
  102. package/python/foxclaw/capability/__init__.py +12 -0
  103. package/python/foxclaw/capability/embedding_service.py +65 -0
  104. package/python/foxclaw/channels/__init__.py +138 -0
  105. package/python/foxclaw/channels/discord.py +175 -0
  106. package/python/foxclaw/channels/dispatcher.py +179 -0
  107. package/python/foxclaw/channels/feishu.py +215 -0
  108. package/python/foxclaw/channels/openclaw_bridge.py +151 -0
  109. package/python/foxclaw/channels/signing.py +187 -0
  110. package/python/foxclaw/channels/telegram.py +120 -0
  111. package/python/foxclaw/channels/weixin.py +215 -0
  112. package/python/foxclaw/cli.py +183 -0
  113. package/python/foxclaw/cognitive/__init__.py +4 -0
  114. package/python/foxclaw/cognitive/reasoner.py +81 -0
  115. package/python/foxclaw/cognitive/state.py +141 -0
  116. package/python/foxclaw/core/__init__.py +9 -0
  117. package/python/foxclaw/core/audit_chain.py +131 -0
  118. package/python/foxclaw/core/audit_sink.py +100 -0
  119. package/python/foxclaw/core/compaction.py +265 -0
  120. package/python/foxclaw/core/config.py +65 -0
  121. package/python/foxclaw/core/confirm_nonce.py +151 -0
  122. package/python/foxclaw/core/dispatch.py +27 -0
  123. package/python/foxclaw/core/envfile.py +48 -0
  124. package/python/foxclaw/core/failure_policy.py +142 -0
  125. package/python/foxclaw/core/gateway.py +81 -0
  126. package/python/foxclaw/core/governance_audit.py +171 -0
  127. package/python/foxclaw/core/idempotency.py +282 -0
  128. package/python/foxclaw/core/injection_guard.py +202 -0
  129. package/python/foxclaw/core/memory_adapter.py +489 -0
  130. package/python/foxclaw/core/metrics.py +88 -0
  131. package/python/foxclaw/core/model_router.py +985 -0
  132. package/python/foxclaw/core/paths.py +103 -0
  133. package/python/foxclaw/core/recovery.py +349 -0
  134. package/python/foxclaw/core/scheduler.py +141 -0
  135. package/python/foxclaw/core/secrets.py +131 -0
  136. package/python/foxclaw/core/sessions.py +155 -0
  137. package/python/foxclaw/core/settings.py +414 -0
  138. package/python/foxclaw/core/shared_write.py +186 -0
  139. package/python/foxclaw/core/steering_queue.py +154 -0
  140. package/python/foxclaw/core/system_bus.py +84 -0
  141. package/python/foxclaw/core/thought_chain.py +316 -0
  142. package/python/foxclaw/core/token_meter.py +231 -0
  143. package/python/foxclaw/core/tools.py +164 -0
  144. package/python/foxclaw/core/tracing.py +375 -0
  145. package/python/foxclaw/core/truth_pipeline.py +110 -0
  146. package/python/foxclaw/core/update_check.py +149 -0
  147. package/python/foxclaw/core/vec_recall_adapter.py +477 -0
  148. package/python/foxclaw/core/verify.py +173 -0
  149. package/python/foxclaw/core/web_search.py +218 -0
  150. package/python/foxclaw/finance/__init__.py +15 -0
  151. package/python/foxclaw/finance/evidence.py +149 -0
  152. package/python/foxclaw/finance/fx.py +98 -0
  153. package/python/foxclaw/finance/graph.py +179 -0
  154. package/python/foxclaw/finance/models.py +146 -0
  155. package/python/foxclaw/finance/nlparse.py +515 -0
  156. package/python/foxclaw/finance/policy.py +301 -0
  157. package/python/foxclaw/finance/reasoner.py +121 -0
  158. package/python/foxclaw/finance/reasoning.py +319 -0
  159. package/python/foxclaw/finance/reconcile.py +175 -0
  160. package/python/foxclaw/finance/runtime.py +500 -0
  161. package/python/foxclaw/governance/__init__.py +24 -0
  162. package/python/foxclaw/governance/middleware.py +176 -0
  163. package/python/foxclaw/harness/__init__.py +17 -0
  164. package/python/foxclaw/harness/_base.py +66 -0
  165. package/python/foxclaw/harness/_policy.py +340 -0
  166. package/python/foxclaw/harness/drift.py +77 -0
  167. package/python/foxclaw/harness/event_bus.py +28 -0
  168. package/python/foxclaw/harness/harness.py +358 -0
  169. package/python/foxclaw/harness/loop_detection.py +145 -0
  170. package/python/foxclaw/itinerary/__init__.py +10 -0
  171. package/python/foxclaw/itinerary/amap_client.py +114 -0
  172. package/python/foxclaw/itinerary/models.py +80 -0
  173. package/python/foxclaw/itinerary/reasoner.py +96 -0
  174. package/python/foxclaw/itinerary/reasoning.py +168 -0
  175. package/python/foxclaw/itinerary/runtime.py +144 -0
  176. package/python/foxclaw/kernel/__init__.py +13 -0
  177. package/python/foxclaw/kernel/capabilities.py +72 -0
  178. package/python/foxclaw/kernel/policy_gate.py +108 -0
  179. package/python/foxclaw/kernel/process_manager.py +186 -0
  180. package/python/foxclaw/kernel/scheduler.py +125 -0
  181. package/python/foxclaw/kernel/sentinel.py +443 -0
  182. package/python/foxclaw/kernel/sentinel_client.py +243 -0
  183. package/python/foxclaw/kernel/spine.py +97 -0
  184. package/python/foxclaw/knowledge/__init__.py +360 -0
  185. package/python/foxclaw/knowledge/truth_crystallizer.py +89 -0
  186. package/python/foxclaw/learning/__init__.py +12 -0
  187. package/python/foxclaw/learning/knowledge_graph.py +144 -0
  188. package/python/foxclaw/learning/models.py +93 -0
  189. package/python/foxclaw/learning/reasoner.py +67 -0
  190. package/python/foxclaw/learning/runtime.py +351 -0
  191. package/python/foxclaw/learning/spaced_repetition.py +91 -0
  192. package/python/foxclaw/mcp_server.py +188 -0
  193. package/python/foxclaw/orchestration/__init__.py +167 -0
  194. package/python/foxclaw/pipeline/__init__.py +23 -0
  195. package/python/foxclaw/pipeline/cognitive_pipeline.py +429 -0
  196. package/python/foxclaw/pipeline/stage_meta.py +21 -0
  197. package/python/foxclaw/pipeline/supervisor_stages.py +201 -0
  198. package/python/foxclaw/planning/__init__.py +17 -0
  199. package/python/foxclaw/planning/llm_planner.py +232 -0
  200. package/python/foxclaw/planning/planner.py +94 -0
  201. package/python/foxclaw/search/__init__.py +24 -0
  202. package/python/foxclaw/search/agent_port.py +190 -0
  203. package/python/foxclaw/search/evidence.py +109 -0
  204. package/python/foxclaw/search/external_search.py +50 -0
  205. package/python/foxclaw/search/hkc_read_port.py +60 -0
  206. package/python/foxclaw/search/internal_search.py +71 -0
  207. package/python/foxclaw/search/models.py +127 -0
  208. package/python/foxclaw/search/planner.py +44 -0
  209. package/python/foxclaw/search/retriever.py +39 -0
  210. package/python/foxclaw/search/runtime.py +127 -0
  211. package/python/foxclaw/search/strategy.py +44 -0
  212. package/python/foxclaw/skills/__init__.py +559 -0
  213. package/python/foxclaw/skills/archive.py +111 -0
  214. package/python/foxclaw/skills/fetch.py +226 -0
  215. package/python/foxclaw/skills/hooks_spec.py +69 -0
  216. package/python/foxclaw/skills/install_cmd.py +216 -0
  217. package/python/foxclaw/skills/lifecycle.py +252 -0
  218. package/python/foxclaw/skills/markdown_skill.py +175 -0
  219. package/python/foxclaw/skills/markdown_store.py +39 -0
  220. package/python/foxclaw/skills/mcp_client.py +184 -0
  221. package/python/foxclaw/skills/mcp_store.py +40 -0
  222. package/python/foxclaw/skills/permission.py +145 -0
  223. package/python/foxclaw/skills/pipeline.py +354 -0
  224. package/python/foxclaw/skills/policy.py +74 -0
  225. package/python/foxclaw/skills/review.py +72 -0
  226. package/python/foxclaw/skills/store.py +57 -0
  227. package/python/frontend/assets/core_bg.jpg +0 -0
  228. package/python/frontend/deck/auth.js +25 -0
  229. package/python/frontend/deck/chat_model_picker.js +483 -0
  230. package/python/frontend/deck/domain.js +276 -0
  231. package/python/frontend/deck/main.js +1806 -0
  232. package/python/frontend/deck/markdown.js +44 -0
  233. package/python/frontend/deck/settings.js +307 -0
  234. package/python/frontend/deck/state.js +5 -0
  235. package/python/frontend/deck/util.js +4 -0
  236. package/python/frontend/deck/visuals.js +190 -0
  237. package/python/frontend/deck.css +855 -0
  238. package/python/frontend/deck.html +686 -0
  239. package/python/frontend/deck_v2.html +341 -0
  240. package/python/frontend/planet/index.html +266 -0
  241. package/python/frontend/planet/planet.css +579 -0
  242. package/python/frontend/planet/planet.js +1436 -0
  243. package/python/frontend/planet/vendor/three.core.min.js +6 -0
  244. package/python/frontend/planet/vendor/three.module.min.js +6 -0
  245. package/python/frontend/v3/assets/index-CQSBdxk4.js +34 -0
  246. package/python/frontend/v3/assets/index-D5nIsjms.css +1 -0
  247. package/python/frontend/v3/index.html +15 -0
  248. package/python/frontend/widget_runtime.js +252 -0
  249. package/python/frontend/widgets.js +639 -0
  250. package/python/frontend_src/index.html +14 -0
  251. package/python/frontend_src/package-lock.json +1376 -0
  252. package/python/frontend_src/package.json +19 -0
  253. package/python/frontend_src/src/App.vue +58 -0
  254. package/python/frontend_src/src/api.js +82 -0
  255. package/python/frontend_src/src/components/ChatDock.vue +365 -0
  256. package/python/frontend_src/src/components/ComposerMenu.vue +75 -0
  257. package/python/frontend_src/src/components/DocViewer.vue +162 -0
  258. package/python/frontend_src/src/components/Inspector.vue +235 -0
  259. package/python/frontend_src/src/components/LeftRail.vue +415 -0
  260. package/python/frontend_src/src/components/MenuPopover.vue +128 -0
  261. package/python/frontend_src/src/components/MessageStream.vue +389 -0
  262. package/python/frontend_src/src/components/ModelPicker.vue +350 -0
  263. package/python/frontend_src/src/components/QuickIngest.vue +112 -0
  264. package/python/frontend_src/src/components/Stage.vue +272 -0
  265. package/python/frontend_src/src/components/ToolMenu.vue +141 -0
  266. package/python/frontend_src/src/components/TopBar.vue +118 -0
  267. package/python/frontend_src/src/components/panels/ActivityPanel.vue +82 -0
  268. package/python/frontend_src/src/components/panels/AgentsPanel.vue +83 -0
  269. package/python/frontend_src/src/components/panels/KnowledgePanel.vue +425 -0
  270. package/python/frontend_src/src/components/panels/MemoryPanel.vue +117 -0
  271. package/python/frontend_src/src/components/panels/PanelBox.vue +50 -0
  272. package/python/frontend_src/src/components/panels/ReviewPanel.vue +292 -0
  273. package/python/frontend_src/src/components/panels/SettingsPanel.vue +358 -0
  274. package/python/frontend_src/src/components/panels/TasksPanel.vue +73 -0
  275. package/python/frontend_src/src/components/panels/ToolsPanel.vue +746 -0
  276. package/python/frontend_src/src/components/panels/UsagePanel.vue +105 -0
  277. package/python/frontend_src/src/lib/fmt.js +44 -0
  278. package/python/frontend_src/src/lib/i18n.js +53 -0
  279. package/python/frontend_src/src/lib/locales/en.js +173 -0
  280. package/python/frontend_src/src/lib/locales/zh.js +174 -0
  281. package/python/frontend_src/src/lib/md.js +190 -0
  282. package/python/frontend_src/src/main.js +9 -0
  283. package/python/frontend_src/src/stores/app.js +91 -0
  284. package/python/frontend_src/src/stores/chat.js +450 -0
  285. package/python/frontend_src/src/stores/model.js +96 -0
  286. package/python/frontend_src/src/stores/panels.js +289 -0
  287. package/python/frontend_src/src/stores/profile.js +31 -0
  288. package/python/frontend_src/src/stores/system.js +25 -0
  289. package/python/frontend_src/src/styles/base.css +138 -0
  290. package/python/frontend_src/src/styles/panel.css +117 -0
  291. package/python/frontend_src/src/styles/tokens.css +78 -0
  292. package/python/frontend_src/vite.config.js +39 -0
  293. package/python/pyproject.toml +268 -0
  294. package/python/requirements.txt +311 -0
  295. package/python/skills/README.md +123 -0
  296. package/python/skills/execution-contract/SKILL.md +30 -0
  297. package/python/skills/general-assistant/SKILL.md +34 -0
  298. package/python/skills/planner/SKILL.md +33 -0
  299. package/python/skills/schedule-itinerary/SKILL.md +35 -0
  300. package/python/skills/skill-composition/SKILL.md +31 -0
  301. package/python/skills/skill-guard/SKILL.md +67 -0
  302. package/python/skills/skill-memory/SKILL.md +50 -0
  303. package/python/skills/skill-router/SKILL.md +35 -0
  304. package/python/tests/conftest.py +54 -0
  305. package/python/tests/run_all.py +200 -0
  306. package/python/tests/test_agent_lifecycle.py +326 -0
  307. package/python/tests/test_ahs_p2.py +393 -0
  308. package/python/tests/test_anthropic_tools.py +192 -0
  309. package/python/tests/test_api.py +97 -0
  310. package/python/tests/test_api_auth.py +79 -0
  311. package/python/tests/test_api_host_guard.py +69 -0
  312. package/python/tests/test_attachments.py +151 -0
  313. package/python/tests/test_audit_chain.py +116 -0
  314. package/python/tests/test_audit_sink.py +130 -0
  315. package/python/tests/test_browser.py +149 -0
  316. package/python/tests/test_browser_request_guard.py +180 -0
  317. package/python/tests/test_capabilities.py +152 -0
  318. package/python/tests/test_channel_signing.py +185 -0
  319. package/python/tests/test_channel_trust.py +109 -0
  320. package/python/tests/test_channels.py +174 -0
  321. package/python/tests/test_cli.py +46 -0
  322. package/python/tests/test_compaction_toolpair.py +119 -0
  323. package/python/tests/test_config_layering.py +61 -0
  324. package/python/tests/test_confirm_nonce.py +186 -0
  325. package/python/tests/test_context_compaction_wiring.py +124 -0
  326. package/python/tests/test_dispatch.py +89 -0
  327. package/python/tests/test_domain_evals.py +334 -0
  328. package/python/tests/test_domain_import.py +72 -0
  329. package/python/tests/test_domain_reasoners.py +134 -0
  330. package/python/tests/test_e2e.py +161 -0
  331. package/python/tests/test_emb_cache.py +97 -0
  332. package/python/tests/test_embedding_singleton.py +87 -0
  333. package/python/tests/test_envfile.py +83 -0
  334. package/python/tests/test_evals_smoke.py +90 -0
  335. package/python/tests/test_event_spine.py +174 -0
  336. package/python/tests/test_evidence_semantic.py +88 -0
  337. package/python/tests/test_evidence_sse.py +94 -0
  338. package/python/tests/test_file_ingest.py +135 -0
  339. package/python/tests/test_finance.py +144 -0
  340. package/python/tests/test_finance_ledger.py +245 -0
  341. package/python/tests/test_finance_persist.py +174 -0
  342. package/python/tests/test_gateway.py +125 -0
  343. package/python/tests/test_governance.py +421 -0
  344. package/python/tests/test_governance_audit.py +324 -0
  345. package/python/tests/test_governance_middleware.py +137 -0
  346. package/python/tests/test_hkc.py +268 -0
  347. package/python/tests/test_hkc_index_build_manager.py +306 -0
  348. package/python/tests/test_hkc_manifest_reuse.py +294 -0
  349. package/python/tests/test_hkc_search_event_sync.py +482 -0
  350. package/python/tests/test_import_path_guard.py +155 -0
  351. package/python/tests/test_injection_guard.py +193 -0
  352. package/python/tests/test_integration.py +117 -0
  353. package/python/tests/test_itinerary.py +100 -0
  354. package/python/tests/test_kee_semantic.py +84 -0
  355. package/python/tests/test_kernel.py +212 -0
  356. package/python/tests/test_learning.py +101 -0
  357. package/python/tests/test_llm_admin.py +145 -0
  358. package/python/tests/test_llm_planner.py +344 -0
  359. package/python/tests/test_markdown_skills.py +62 -0
  360. package/python/tests/test_markdown_skills_openclaw.py +66 -0
  361. package/python/tests/test_mcp_connectors.py +97 -0
  362. package/python/tests/test_mcp_guardian_discovery.py +87 -0
  363. package/python/tests/test_mcp_no_memory.py +43 -0
  364. package/python/tests/test_mcp_policy_gate.py +76 -0
  365. package/python/tests/test_mcp_server.py +92 -0
  366. package/python/tests/test_memory_recall.py +92 -0
  367. package/python/tests/test_metrics.py +58 -0
  368. package/python/tests/test_multi_chain.py +90 -0
  369. package/python/tests/test_multimodal.py +122 -0
  370. package/python/tests/test_negotiation.py +136 -0
  371. package/python/tests/test_orchestration.py +109 -0
  372. package/python/tests/test_orchestration_decoupled.py +103 -0
  373. package/python/tests/test_p3_p5_hardening.py +324 -0
  374. package/python/tests/test_parallel_dispatch.py +207 -0
  375. package/python/tests/test_pipeline.py +492 -0
  376. package/python/tests/test_plan_visible.py +86 -0
  377. package/python/tests/test_planner.py +88 -0
  378. package/python/tests/test_policy_gate.py +108 -0
  379. package/python/tests/test_profile.py +67 -0
  380. package/python/tests/test_readme_claims.py +163 -0
  381. package/python/tests/test_reasoner.py +170 -0
  382. package/python/tests/test_release_check.py +252 -0
  383. package/python/tests/test_resume_checkpoint.py +148 -0
  384. package/python/tests/test_routing.py +85 -0
  385. package/python/tests/test_scheduler.py +138 -0
  386. package/python/tests/test_search.py +194 -0
  387. package/python/tests/test_secrets.py +153 -0
  388. package/python/tests/test_semantic.py +69 -0
  389. package/python/tests/test_sessions.py +90 -0
  390. package/python/tests/test_settings.py +265 -0
  391. package/python/tests/test_shadow_learning.py +290 -0
  392. package/python/tests/test_shared_write.py +200 -0
  393. package/python/tests/test_skill_taint_gate.py +145 -0
  394. package/python/tests/test_skills.py +117 -0
  395. package/python/tests/test_skills_fetch.py +282 -0
  396. package/python/tests/test_stats_and_events.py +162 -0
  397. package/python/tests/test_streaming.py +55 -0
  398. package/python/tests/test_supervisor_pipeline.py +176 -0
  399. package/python/tests/test_supervisor_triage.py +102 -0
  400. package/python/tests/test_taint.py +112 -0
  401. package/python/tests/test_task_stream.py +74 -0
  402. package/python/tests/test_thinking_sse.py +66 -0
  403. package/python/tests/test_thinking_stream.py +76 -0
  404. package/python/tests/test_tool_center.py +113 -0
  405. package/python/tests/test_tool_parallel.py +113 -0
  406. package/python/tests/test_tool_params.py +99 -0
  407. package/python/tests/test_tool_use.py +229 -0
  408. package/python/tests/test_toplevel_concurrency.py +116 -0
  409. package/python/tests/test_tracing.py +256 -0
  410. package/python/tests/test_truth_crystallizer.py +125 -0
  411. package/python/tests/test_truth_flow.py +88 -0
  412. package/python/tests/test_update_check.py +95 -0
  413. package/python/tests/test_url_policy.py +53 -0
  414. package/python/tests/test_verify_evidence.py +212 -0
  415. package/python/tests/test_version_sync.py +100 -0
  416. package/python/tests/test_web_trigger.py +82 -0
  417. package/python/tools/check_faiss_type.py +30 -0
  418. package/python/tools/check_log.ps1 +38 -0
  419. package/python/tools/ivf_status.py +72 -0
  420. package/python/tools/mypy_baseline.txt +13 -0
  421. package/python/tools/mypy_ratchet.py +143 -0
  422. package/python/tools/pack.py +173 -0
  423. package/python/tools/release_check.py +349 -0
  424. package/python/tools/restart_foxclaw.ps1 +71 -0
  425. package/python/tools/shared_write_audit.py +190 -0
  426. package/python/tools/size_baseline.txt +28 -0
  427. package/python/tools/size_check.py +102 -0
  428. package/python/tools/strip_comments.py +228 -0
  429. package/python/tools/verify_env.bat +2 -0
  430. package/python/tools/verify_env.py +25 -0
  431. package/python/vendor/HMR_LICENSE +37 -0
  432. package/python/vendor/HMR_README.md +65 -0
  433. package/python/vendor/hkc/LICENSE +37 -0
  434. package/python/vendor/hkc/hkc_ace/__init__.py +0 -0
  435. package/python/vendor/hkc/hkc_ace/ace.py +457 -0
  436. package/python/vendor/hkc/hkc_ace/skill_taxonomy.json +465 -0
  437. package/python/vendor/hkc/hkc_api/__init__.py +0 -0
  438. package/python/vendor/hkc/hkc_api/container.py +416 -0
  439. package/python/vendor/hkc/hkc_api/ingest_jobs.py +103 -0
  440. package/python/vendor/hkc/hkc_api/main.py +157 -0
  441. package/python/vendor/hkc/hkc_api/routes/__init__.py +0 -0
  442. package/python/vendor/hkc/hkc_api/routes/ability.py +151 -0
  443. package/python/vendor/hkc/hkc_api/routes/config.py +51 -0
  444. package/python/vendor/hkc/hkc_api/routes/knowledge.py +630 -0
  445. package/python/vendor/hkc/hkc_api/routes/search.py +88 -0
  446. package/python/vendor/hkc/hkc_api/schemas.py +153 -0
  447. package/python/vendor/hkc/hkc_api/synthesis.py +179 -0
  448. package/python/vendor/hkc/hkc_core/__init__.py +0 -0
  449. package/python/vendor/hkc/hkc_core/graph/__init__.py +0 -0
  450. package/python/vendor/hkc/hkc_core/graph/base.py +188 -0
  451. package/python/vendor/hkc/hkc_core/graph/sqlite_store.py +789 -0
  452. package/python/vendor/hkc/hkc_core/models/__init__.py +0 -0
  453. package/python/vendor/hkc/hkc_core/models/enums.py +65 -0
  454. package/python/vendor/hkc/hkc_core/models/ku.py +307 -0
  455. package/python/vendor/hkc/hkc_core/utils/__init__.py +0 -0
  456. package/python/vendor/hkc/hkc_core/utils/id_gen.py +113 -0
  457. package/python/vendor/hkc/hkc_crystallizer/__init__.py +44 -0
  458. package/python/vendor/hkc/hkc_crystallizer/adapters.py +94 -0
  459. package/python/vendor/hkc/hkc_crystallizer/candidate.py +145 -0
  460. package/python/vendor/hkc/hkc_crystallizer/crystallizer.py +179 -0
  461. package/python/vendor/hkc/hkc_crystallizer/ingress.py +90 -0
  462. package/python/vendor/hkc/hkc_kde/__init__.py +0 -0
  463. package/python/vendor/hkc/hkc_kde/chunker.py +197 -0
  464. package/python/vendor/hkc/hkc_kde/extractor.py +623 -0
  465. package/python/vendor/hkc/hkc_kde/kde.py +298 -0
  466. package/python/vendor/hkc/hkc_kde/loaders/__init__.py +0 -0
  467. package/python/vendor/hkc/hkc_kde/loaders/ebook_loader.py +153 -0
  468. package/python/vendor/hkc/hkc_kde/loaders/office_loader.py +194 -0
  469. package/python/vendor/hkc/hkc_kde/loaders/pdf_loader.py +162 -0
  470. package/python/vendor/hkc/hkc_kde/loaders/text_loader.py +157 -0
  471. package/python/vendor/hkc/hkc_kde/loaders/web_loader.py +91 -0
  472. package/python/vendor/hkc/hkc_kde/models.py +74 -0
  473. package/python/vendor/hkc/hkc_kde/packager.py +329 -0
  474. package/python/vendor/hkc/hkc_kde/relation_disc.py +151 -0
  475. package/python/vendor/hkc/hkc_kee/__init__.py +0 -0
  476. package/python/vendor/hkc/hkc_kee/dedup.py +227 -0
  477. package/python/vendor/hkc/hkc_kee/fingerprint.py +106 -0
  478. package/python/vendor/hkc/hkc_kee/kee.py +541 -0
  479. package/python/vendor/hkc/hkc_kep/__init__.py +0 -0
  480. package/python/vendor/hkc/hkc_kep/event_bus.py +205 -0
  481. package/python/vendor/hkc/hkc_sdk/__init__.py +63 -0
  482. package/python/vendor/hkc/hkc_sdk/base.py +151 -0
  483. package/python/vendor/hkc/hkc_sdk/direct_client.py +153 -0
  484. package/python/vendor/hkc/hkc_sdk/exceptions.py +39 -0
  485. package/python/vendor/hkc/hkc_sdk/http_client.py +166 -0
  486. package/python/vendor/hkc/hkc_sdk/models.py +171 -0
  487. package/python/vendor/hkc/hkc_search/__init__.py +0 -0
  488. package/python/vendor/hkc/hkc_search/bm25.py +325 -0
  489. package/python/vendor/hkc/hkc_search/embedding_backends.py +374 -0
  490. package/python/vendor/hkc/hkc_search/graph_search.py +132 -0
  491. package/python/vendor/hkc/hkc_search/hybrid.py +331 -0
  492. package/python/vendor/hkc/hkc_search/index_build_manager.py +378 -0
  493. package/python/vendor/hkc/hkc_search/vector_search.py +484 -0
  494. package/python/vendor/hmr/__init__.py +0 -0
  495. package/python/vendor/hmr/core/__init__.py +0 -0
  496. package/python/vendor/hmr/core/hmr.py +684 -0
  497. package/python/vendor/hmr/core/models.py +310 -0
  498. package/python/vendor/hmr/engines/__init__.py +0 -0
  499. package/python/vendor/hmr/engines/evolution.py +710 -0
  500. package/python/vendor/hmr/engines/jit_compiler.py +472 -0
  501. package/python/vendor/hmr/engines/lifecycle.py +264 -0
  502. package/python/vendor/hmr/engines/policy.py +625 -0
  503. package/python/vendor/hmr/engines/recall.py +260 -0
  504. package/python/vendor/hmr/engines/runtime_state.py +158 -0
  505. package/python/vendor/hmr/engines/scheduler.py +323 -0
  506. package/python/vendor/hmr/engines/semantic.py +150 -0
  507. package/python/vendor/hmr/engines/temporal.py +261 -0
  508. package/python/vendor/hmr/engines/thought_chain.py +635 -0
  509. package/python/vendor/hmr/graph/__init__.py +0 -0
  510. package/python/vendor/hmr/graph/cwg.py +338 -0
  511. package/python/vendor/hmr/graph/memory_graph.py +530 -0
  512. package/python/vendor/hmr/storage/__init__.py +0 -0
  513. package/python/vendor/hmr/storage/memory_fs.py +300 -0
  514. 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
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,349 @@
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
+ return code.value == STILL_ACTIVE
59
+ finally:
60
+ kernel32.CloseHandle(handle)
61
+
62
+ try:
63
+ os.kill(pid, 0)
64
+ return True
65
+ except ProcessLookupError:
66
+ return False
67
+ except PermissionError:
68
+ return True
69
+ except Exception:
70
+ return False
71
+
72
+
73
+ @dataclass
74
+ class TaskRecord:
75
+ task_id: str
76
+ description: str
77
+ pid: int
78
+ started: float
79
+ status: str = "in_progress"
80
+ session_id: str = ""
81
+ channel: str = ""
82
+ finished: float = 0.0
83
+ boot_id: str = ""
84
+ retryable: bool = False
85
+ start_count: int = 1
86
+
87
+ def to_dict(self) -> dict:
88
+ return asdict(self)
89
+
90
+
91
+ @dataclass
92
+ class RecoveryPlan:
93
+ """【P2-4】恢复计划:把中断任务分类,调用方(API 层/前端)据此决定动作。"""
94
+ retryable: list = field(default_factory=list)
95
+ notify_user: list = field(default_factory=list)
96
+ needs_manual: list = field(default_factory=list)
97
+ details: list = field(default_factory=list)
98
+
99
+ def to_dict(self) -> dict:
100
+ return asdict(self)
101
+
102
+
103
+ class RestartRecovery:
104
+ """任务生命周期日志 + 启动恢复扫描。"""
105
+
106
+ def __init__(self, log_path: str, current_pid: int | None = None, boot_id: str | None = None,
107
+ spine=None, fsync: bool = True):
108
+ self.log_path = log_path
109
+ self.current_pid = current_pid or os.getpid()
110
+ import uuid
111
+ self.boot_id = boot_id or uuid.uuid4().hex[:16]
112
+ self._active: dict[str, TaskRecord] = {}
113
+ self._spine = spine
114
+ self._fsync = fsync
115
+ os.makedirs(os.path.dirname(log_path) or ".", exist_ok=True)
116
+
117
+ def task_started(self, task_id: str, description: str,
118
+ session_id: str = "", channel: str = "",
119
+ retryable: bool = False) -> TaskRecord:
120
+ rec = TaskRecord(
121
+ task_id=task_id, description=description[:200],
122
+ pid=self.current_pid, started=time.time(),
123
+ session_id=session_id, channel=channel, boot_id=self.boot_id,
124
+ retryable=retryable)
125
+ self._active[task_id] = rec
126
+ self._append({"event": "start", **rec.to_dict()})
127
+ return rec
128
+
129
+ def task_finished(self, task_id: str, status: str = "done"):
130
+ rec = self._active.pop(task_id, None)
131
+ if rec:
132
+ rec.status = status
133
+ rec.finished = time.time()
134
+ self._append({"event": "finish", "task_id": task_id,
135
+ "status": status, "pid": self.current_pid,
136
+ "finished": time.time()})
137
+
138
+ def record_step(self, task_id: str, step: dict) -> None:
139
+ """记一条「已完成的步骤」。管线每完成一个 stage、工具轮每完成一个工具调用各落一条,
140
+ 崩溃/断连后 `steps_of()` 能回答「跑到哪一步」,也让续跑知道哪些步骤不必重做。
141
+
142
+ fail-safe:写日志失败绝不影响任务执行(与任务生命周期记账同一定位)。
143
+ """
144
+ if not task_id or not isinstance(step, dict):
145
+ return
146
+ self._append({"event": "step", "task_id": task_id,
147
+ "ts": time.time(), **step})
148
+
149
+ def steps_of(self, task_id: str, limit: int = 200) -> list:
150
+ """回放日志,取该 task 已记录的步骤(写入顺序;只留最近 limit 条)。
151
+
152
+ 只读、容错:文件不存在/行损坏/中途写一半都跳过,不抛异常。
153
+ """
154
+ out: list = []
155
+ if not task_id or not os.path.exists(self.log_path):
156
+ return out
157
+ try:
158
+ with open(self.log_path, encoding="utf-8") as f:
159
+ for line in f:
160
+ line = line.strip()
161
+ if not line:
162
+ continue
163
+ try:
164
+ ev = json.loads(line)
165
+ except Exception:
166
+ continue
167
+ if ev.get("event") == "step" and ev.get("task_id") == task_id:
168
+ out.append({k: v for k, v in ev.items()
169
+ if k not in ("event", "task_id")})
170
+ except Exception:
171
+ return out
172
+ return out[-limit:] if limit else out
173
+
174
+ def scan_interrupted(self) -> list[TaskRecord]:
175
+ """启动时调用:重建日志状态,找出被中断的任务。
176
+
177
+ 判定(双重,避免 pid 复用误判):
178
+ 1. 任务仍是 in_progress
179
+ 2. 它的 boot_id 不是本次启动(即属于上一次运行)
180
+ boot_id 随机不复用,所以"非本次启动的进行中任务"必然是被中断的。
181
+ """
182
+ records = self._replay()
183
+ interrupted = []
184
+ for task_id, rec in records.items():
185
+ if rec.status != "in_progress":
186
+ continue
187
+ if rec.boot_id == self.boot_id:
188
+ continue
189
+ rec.status = "interrupted"
190
+ interrupted.append(rec)
191
+ self._append({"event": "interrupted", "task_id": task_id,
192
+ "pid": rec.pid, "prev_boot_id": rec.boot_id,
193
+ "pid_still_alive": _pid_alive(rec.pid),
194
+ "detected_by": self.boot_id,
195
+ "detected_at": time.time()})
196
+ return interrupted
197
+
198
+ def _replay(self) -> dict[str, TaskRecord]:
199
+ """回放日志,重建每个 task 的最终状态。"""
200
+ records: dict[str, TaskRecord] = {}
201
+ if not os.path.exists(self.log_path):
202
+ return records
203
+ try:
204
+ with open(self.log_path, encoding="utf-8") as f:
205
+ for line in f:
206
+ line = line.strip()
207
+ if not line:
208
+ continue
209
+ try:
210
+ ev = json.loads(line)
211
+ except Exception:
212
+ continue
213
+ self._apply_event(records, ev)
214
+ except Exception:
215
+ pass
216
+ return records
217
+
218
+ @staticmethod
219
+ def _apply_event(records: dict, ev: dict):
220
+ tid = ev.get("task_id")
221
+ if not tid:
222
+ return
223
+ kind = ev.get("event")
224
+ if kind == "start":
225
+ existing = records.get(tid)
226
+ start_count = (existing.start_count + 1) if existing else 1
227
+ records[tid] = TaskRecord(
228
+ task_id=tid, description=ev.get("description", ""),
229
+ pid=ev.get("pid", 0), started=ev.get("started", 0),
230
+ status="in_progress", session_id=ev.get("session_id", ""),
231
+ channel=ev.get("channel", ""), boot_id=ev.get("boot_id", ""),
232
+ retryable=ev.get("retryable", False),
233
+ start_count=start_count)
234
+ elif kind in ("finish", "interrupted") and tid in records:
235
+ records[tid].status = ("interrupted" if kind == "interrupted"
236
+ else ev.get("status", "done"))
237
+ if kind == "finish":
238
+ records[tid].finished = ev.get("finished", 0)
239
+
240
+ def plan_recovery(self, interrupted: list | None = None) -> RecoveryPlan:
241
+ """【P2-4】给中断任务分类,返回恢复计划(不改 scan_interrupted 的返回,兼容)。
242
+
243
+ 分类规则:
244
+ · retryable=True → retryable(调用方标记过可安全重试)
245
+ · channel 非空 → notify_user(任务来自某个渠道,应通知该渠道用户)
246
+ · 其他 → needs_manual(保守:人工处理)
247
+ 每个分类决定都发 recovery.plan 审计事件(有 spine 时)。
248
+ """
249
+ if interrupted is None:
250
+ interrupted = self.scan_interrupted()
251
+ plan = RecoveryPlan()
252
+ for rec in interrupted:
253
+ if rec.retryable:
254
+ plan.retryable.append(rec.task_id)
255
+ action = "retry"
256
+ elif rec.channel:
257
+ plan.notify_user.append(rec.task_id)
258
+ action = "notify_user"
259
+ else:
260
+ plan.needs_manual.append(rec.task_id)
261
+ action = "manual"
262
+ plan.details.append({
263
+ "task_id": rec.task_id,
264
+ "description": rec.description,
265
+ "channel": rec.channel,
266
+ "session_id": rec.session_id,
267
+ "start_count": rec.start_count,
268
+ "reason": "crash_or_restart",
269
+ "action": action,
270
+ })
271
+ self._emit_recovery_event(rec, action)
272
+ return plan
273
+
274
+ def _emit_recovery_event(self, rec: TaskRecord, action: str):
275
+ if self._spine is None:
276
+ return
277
+ try:
278
+ self._spine.publish("recovery.plan", {
279
+ "task_id": rec.task_id,
280
+ "action": action,
281
+ "channel": rec.channel,
282
+ "boot_id": self.boot_id,
283
+ }, source="recovery")
284
+ except Exception:
285
+ pass
286
+
287
+ def _append(self, entry: dict):
288
+ """追加一条日志(崩溃安全:单行 append)。fsync 可配:默认开(崩溃安全),
289
+ 高频场景可关(牺牲最后一条的持久性换吞吐)。"""
290
+ try:
291
+ with open(self.log_path, "a", encoding="utf-8") as f:
292
+ f.write(json.dumps(entry, ensure_ascii=False) + "\n")
293
+ f.flush()
294
+ if self._fsync:
295
+ os.fsync(f.fileno())
296
+ except Exception:
297
+ pass
298
+
299
+ def compact_log(self, keep_recent_days: float = 7):
300
+ """压实日志:丢弃已完成且超过 N 天的记录,防文件无限膨胀。
301
+ 【P2-4】保留判据用 max(started, finished):旧逻辑只看 started,
302
+ 会丢"开始得早但刚完成"的记录(其 finish 审计线索一并丢失)。
303
+ 步骤级 checkpoint(step 事件)属于被保留的任务 → 一并搬运,否则压实即丢断点。"""
304
+ records = self._replay()
305
+ cutoff = time.time() - keep_recent_days * 86400
306
+ keep = []
307
+ for rec in records.values():
308
+ last_activity = max(rec.started, rec.finished or 0)
309
+ if rec.status == "in_progress" or last_activity >= cutoff:
310
+ keep.append({"event": "start", **rec.to_dict()})
311
+ if rec.status != "in_progress":
312
+ keep.append({"event": "finish", "task_id": rec.task_id,
313
+ "status": rec.status, "finished": rec.finished})
314
+ keep_ids = {e["task_id"] for e in keep if e.get("event") == "start"}
315
+ steps = self._raw_steps(keep_ids)
316
+ try:
317
+ tmp = self.log_path + ".tmp"
318
+ with open(tmp, "w", encoding="utf-8") as f:
319
+ for e in keep:
320
+ f.write(json.dumps(e, ensure_ascii=False) + "\n")
321
+ for e in steps:
322
+ f.write(json.dumps(e, ensure_ascii=False) + "\n")
323
+ os.replace(tmp, self.log_path)
324
+ except Exception:
325
+ pass
326
+
327
+ def _raw_steps(self, task_ids: set) -> list:
328
+ """扫原始日志取指定任务的 step 事件(compact 用;task_ids 为空直接返回)。"""
329
+ out: list = []
330
+ if not task_ids or not os.path.exists(self.log_path):
331
+ return out
332
+ try:
333
+ with open(self.log_path, encoding="utf-8") as f:
334
+ for line in f:
335
+ line = line.strip()
336
+ if not line:
337
+ continue
338
+ try:
339
+ ev = json.loads(line)
340
+ except Exception:
341
+ continue
342
+ if ev.get("event") == "step" and ev.get("task_id") in task_ids:
343
+ out.append(ev)
344
+ except Exception:
345
+ return out
346
+ return out
347
+
348
+ def active_count(self) -> int:
349
+ return len(self._active)
@@ -0,0 +1,141 @@
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
+ from typing import Optional
13
+
14
+
15
+ @dataclass
16
+ class TaskIntent:
17
+ """任务意图:粗粒度类别 + 三个亲和度提示。"""
18
+ category: str
19
+ confidence: float
20
+ memory_affinity: float
21
+ reasoning_depth: float
22
+
23
+
24
+ @dataclass
25
+ class TaskFeatures:
26
+ complexity: float
27
+ memory_dependency: float
28
+ tool_cost: float
29
+ reasoning_depth: float
30
+ urgency: float
31
+ expected_runtime: float
32
+
33
+
34
+ @dataclass
35
+ class SystemContext:
36
+ """系统负载上下文。主管据此调整优先级、决定是否抢占。"""
37
+ active_agents: int = 0
38
+ queue_depth: int = 0
39
+ system_load: float = 0.1
40
+
41
+
42
+ @dataclass
43
+ class ScheduleDecision:
44
+ priority: float
45
+ route_hint: str
46
+ execution_mode: str
47
+ preemptible: bool
48
+ expected_budget: float
49
+ intent: Optional[TaskIntent] = None
50
+ features: Optional[TaskFeatures] = None
51
+ preemption_reason: Optional[str] = None
52
+ reason: str = ""
53
+
54
+ def to_dict(self) -> dict:
55
+ return {
56
+ "priority": self.priority, "route_hint": self.route_hint,
57
+ "execution_mode": self.execution_mode, "preemptible": self.preemptible,
58
+ "expected_budget": self.expected_budget,
59
+ "preemption_reason": self.preemption_reason, "reason": self.reason,
60
+ "intent": self.intent.category if self.intent else None,
61
+ }
62
+
63
+
64
+ class CognitiveScheduler:
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
+ if any(w in task for w in ["紧急", "立刻", "马上", "尽快", "急"]):
90
+ urgency = min(1.0, urgency + 0.3)
91
+ return TaskFeatures(complexity, intent.memory_affinity, tool_cost,
92
+ intent.reasoning_depth, urgency, runtime)
93
+
94
+ def score(self, f: TaskFeatures) -> float:
95
+ """基础分:加权特征。"""
96
+ weighted = (f.complexity * 0.3 + f.memory_dependency * 0.2
97
+ + f.tool_cost * 0.15 + f.reasoning_depth * 0.25
98
+ + f.urgency * 0.1)
99
+ return round(weighted, 3)
100
+
101
+ def adjust(self, base: float, ctx: SystemContext, intent: TaskIntent,
102
+ f: TaskFeatures) -> float:
103
+ """负载调整:负载/队列越深,优先级被压低;紧急/记忆亲和度抬升。"""
104
+ memory_affinity = max(0.25, intent.memory_affinity)
105
+ load_penalty = 1 + ctx.system_load + (ctx.queue_depth * 0.05)
106
+ urgency_boost = max(0.5, f.urgency)
107
+ final = (base * urgency_boost * memory_affinity) / load_penalty
108
+ return min(1.0, round(final, 3))
109
+
110
+ def route_hint(self, priority: float, intent: TaskIntent) -> str:
111
+ if intent.category == "workflow":
112
+ return "workflow"
113
+ if intent.category == "reasoning":
114
+ return "agent"
115
+ if intent.category == "memory" and priority < 0.3:
116
+ return "tool"
117
+ return "agent" if priority > 0.35 else "tool"
118
+
119
+ def preemption_reason(self, priority: float, ctx: SystemContext) -> str | None:
120
+ if priority > 0.35 and ctx.active_agents >= 3:
121
+ return "高优先级 + Agent 压力大"
122
+ if priority > 0.4 and ctx.queue_depth >= 2:
123
+ return "高优先级 + 队列积压"
124
+ return None
125
+
126
+ def schedule(self, task: str, ctx: SystemContext | None = None) -> ScheduleDecision:
127
+ """主入口:给一个任务做完整调度决策。"""
128
+ ctx = ctx or SystemContext()
129
+ intent = self.parse_intent(task)
130
+ features = self.extract_features(task, intent)
131
+ base = self.score(features)
132
+ priority = self.adjust(base, ctx, intent, features)
133
+ route = self.route_hint(priority, intent)
134
+ preempt = self.preemption_reason(priority, ctx)
135
+ mode = "preemptive" if priority > 0.4 else "standard"
136
+ return ScheduleDecision(
137
+ priority=priority, route_hint=route, execution_mode=mode,
138
+ preemptible=preempt is not None,
139
+ expected_budget=round(features.expected_runtime * (1 + priority), 2),
140
+ intent=intent, features=features, preemption_reason=preempt,
141
+ reason=f"意图={intent.category} 优先级={priority} 模式={mode}")