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,252 @@
1
+ """发布前校验脚本(tools/release_check.py)的规则测试。
2
+
3
+ 锁住三件事:
4
+ 1. 四处版本号必须一致 —— 只改一处的发布物会让用户装到对不上的版本;
5
+ 2. tag 必须指向本次版本 —— 发布不可撤回,yank 也仍占号;
6
+ 3. 产物里不能有 .env / docs/internal / 运行期数据,缺 vendor 只算提示(占位包现状)。
7
+
8
+ 脚本放在 tools/ 不在包里,故按文件路径加载;测试只构造最小归档,不跑真构建(秒级)。
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import importlib.util
14
+ import io
15
+ import json
16
+ import shutil
17
+ import sys
18
+ import tarfile
19
+ import zipfile
20
+ from pathlib import Path
21
+
22
+ import pytest
23
+
24
+ REPO = Path(__file__).resolve().parents[1]
25
+
26
+
27
+ def _load():
28
+ spec = importlib.util.spec_from_file_location(
29
+ "release_check", REPO / "tools" / "release_check.py")
30
+ mod = importlib.util.module_from_spec(spec)
31
+ sys.modules["release_check"] = mod
32
+ spec.loader.exec_module(mod)
33
+ return mod
34
+
35
+
36
+ rc = _load()
37
+
38
+
39
+ def _wheel(dist: Path, version="0.1.0", name="foxclaw", extra=(),
40
+ description=True) -> Path:
41
+ p = dist / f"{name}-{version}-py3-none-any.whl"
42
+ body = ("\n\n" + "long description body. " * 12) if description else ""
43
+ with zipfile.ZipFile(p, "w") as z:
44
+ z.writestr(f"{name}/__init__.py", "__version__ = '0.1.0'\n")
45
+ z.writestr(f"{name}/cli.py", "def main(): pass\n")
46
+ z.writestr(f"{name}-{version}.dist-info/METADATA",
47
+ f"Name: {name}\nVersion: {version}{body}\n")
48
+ for e in extra:
49
+ z.writestr(e, "x")
50
+ return p
51
+
52
+
53
+ def _sdist(dist: Path, version="0.1.0", name="foxclaw", extra=()) -> Path:
54
+ p = dist / f"{name}-{version}.tar.gz"
55
+ with tarfile.open(p, "w:gz") as t:
56
+ for arc in (f"{name}-{version}/foxclaw/cli.py",
57
+ f"{name}-{version}/PKG-INFO", *extra):
58
+ data = b"x"
59
+ info = tarfile.TarInfo(arc)
60
+ info.size = len(data)
61
+ t.addfile(info, io.BytesIO(data))
62
+ return p
63
+
64
+
65
+ def _tgz(dist: Path, version="0.1.0", name="foxclaw", pkg=None, extra=()) -> Path:
66
+ p = dist / f"{name}-{version}.tgz"
67
+ payload = json.dumps(pkg if pkg is not None else
68
+ {"name": name, "version": version}).encode()
69
+ with tarfile.open(p, "w:gz") as t:
70
+ for arc, data in (("package/package.json", payload),
71
+ ("package/index.js", b"export default {}"),
72
+ *[(e, b"x") for e in extra]):
73
+ info = tarfile.TarInfo(arc)
74
+ info.size = len(data)
75
+ t.addfile(info, io.BytesIO(data))
76
+ return p
77
+
78
+
79
+ def _mini_repo(root: Path, version: str, npm_version: str) -> Path:
80
+ """只含四处版本来源的最小仓库,用来测"漏改一处"。"""
81
+ (root / "foxclaw").mkdir(parents=True)
82
+ (root / "npm").mkdir()
83
+ (root / "pyproject.toml").write_text(
84
+ f'[project]\nname = "foxclaw"\nversion = "{version}"\n', encoding="utf-8")
85
+ (root / "foxclaw" / "__init__.py").write_text(
86
+ f'__version__ = "{version}"\n', encoding="utf-8")
87
+ (root / "npm" / "package.json").write_text(
88
+ json.dumps({"name": "foxclaw", "version": npm_version}), encoding="utf-8")
89
+ (root / "npm" / "index.js").write_text(
90
+ f'export const version = "{npm_version}";\n', encoding="utf-8")
91
+ return root
92
+
93
+
94
+ def test_repo_versions_are_consistent():
95
+ """真仓库当前状态:四处一致,无 error。"""
96
+ assert [i for i in rc.check_versions(REPO) if i.level == "error"] == []
97
+
98
+
99
+ def test_single_missed_source_is_an_error(tmp_path):
100
+ root = _mini_repo(tmp_path, "0.2.0", npm_version="0.1.0")
101
+ errs = [i for i in rc.check_versions(root) if i.level == "error"]
102
+ assert errs and any("npm" in i.msg for i in errs), errs
103
+
104
+
105
+ def test_unreadable_source_is_an_error(tmp_path):
106
+ root = _mini_repo(tmp_path, "0.1.0", "0.1.0")
107
+ (root / "npm" / "index.js").write_text("// 没有导出\n", encoding="utf-8")
108
+ assert any(i.code == "unreadable" for i in rc.check_versions(root))
109
+
110
+
111
+ @pytest.mark.parametrize("tag", ["v0.1.0", "0.1.0"])
112
+ def test_tag_matching_version_passes(tag):
113
+ assert rc.check_tag(tag, "0.1.0") == []
114
+
115
+
116
+ @pytest.mark.parametrize("tag", ["v0.2.0", "v0.1.0-rc1"])
117
+ def test_tag_drift_is_an_error(tag):
118
+ errs = rc.check_tag(tag, "0.1.0")
119
+ assert errs and errs[0].code == "tag_mismatch"
120
+
121
+
122
+ def test_empty_tag_is_skipped():
123
+ assert rc.check_tag("", "0.1.0") == []
124
+
125
+
126
+ def test_repo_declares_readme():
127
+ """真仓库必须声明正文:0.1.1 在 PyPI 上的项目页就是空白的,这就是回归点。"""
128
+ assert [i for i in rc.check_metadata(REPO) if i.level == "error"] == []
129
+ assert rc.readme_target(REPO).endswith(".md")
130
+
131
+
132
+ def test_missing_readme_is_error(tmp_path):
133
+ root = _mini_repo(tmp_path, "0.1.0", "0.1.0")
134
+ assert any(i.code == "no_readme" for i in rc.check_metadata(root))
135
+
136
+
137
+ def test_short_readme_is_error(tmp_path):
138
+ root = _mini_repo(tmp_path, "0.1.0", "0.1.0")
139
+ (root / "pyproject.toml").write_text(
140
+ '[project]\nname = "foxclaw"\nversion = "0.1.0"\nreadme = "README.md"\n',
141
+ encoding="utf-8")
142
+ (root / "README.md").write_text("短\n", encoding="utf-8")
143
+ assert any(i.code == "readme_too_short" for i in rc.check_metadata(root))
144
+
145
+
146
+ def test_readme_pointing_to_missing_file_is_error(tmp_path):
147
+ root = _mini_repo(tmp_path, "0.1.0", "0.1.0")
148
+ (root / "pyproject.toml").write_text(
149
+ '[project]\nname = "foxclaw"\nversion = "0.1.0"\nreadme = "NOPE.md"\n',
150
+ encoding="utf-8")
151
+ assert any(i.code == "readme_missing" for i in rc.check_metadata(root))
152
+
153
+
154
+ @pytest.mark.parametrize("entry", [
155
+ "foxclaw/.env",
156
+ "foxclaw/.env.local",
157
+ "foxclaw-0.1.0/docs/internal/secret.md",
158
+ "foxclaw/foxclaw_data/sessions.db",
159
+ "foxclaw/old.zip",
160
+ "foxclaw/__pycache__/x.pyc",
161
+ "foxclaw/.venv/lib/x.py",
162
+ ])
163
+ def test_forbidden_entries_detected(entry):
164
+ assert rc.forbidden_entry(entry), entry
165
+
166
+
167
+ @pytest.mark.parametrize("entry", [
168
+ "foxclaw/.env.example",
169
+ "foxclaw/cli.py",
170
+ "foxclaw/vendor/hmr/hmr/__init__.py",
171
+ ])
172
+ def test_allowed_entries_pass(entry):
173
+ assert rc.forbidden_entry(entry) is None
174
+
175
+
176
+ def test_check_dist_passes_with_vendor_warning_only(tmp_path):
177
+ dist = tmp_path / "dist"
178
+ dist.mkdir()
179
+ _wheel(dist)
180
+ _sdist(dist)
181
+ issues = rc.check_dist(dist, "foxclaw", "0.1.0")
182
+ assert [i for i in issues if i.level == "error"] == []
183
+ assert any(i.code == "no_vendor" for i in issues), "缺 vendor 应显式提示"
184
+
185
+
186
+ def test_check_dist_flags_missing_wheel(tmp_path):
187
+ dist = tmp_path / "dist"
188
+ dist.mkdir()
189
+ _sdist(dist)
190
+ assert any(i.code == "wheel_count" for i in rc.check_dist(dist, "foxclaw", "0.1.0"))
191
+
192
+
193
+ def test_check_dist_flags_version_drift_in_filename(tmp_path):
194
+ dist = tmp_path / "dist"
195
+ dist.mkdir()
196
+ _wheel(dist, version="0.2.0")
197
+ _sdist(dist, version="0.1.0")
198
+ codes = {i.code for i in rc.check_dist(dist, "foxclaw", "0.1.0")}
199
+ assert "wheel_name" in codes or "meta_mismatch" in codes, codes
200
+
201
+
202
+ def test_check_dist_flags_secret_inside_archive(tmp_path):
203
+ dist = tmp_path / "dist"
204
+ dist.mkdir()
205
+ _wheel(dist, extra=(".env",))
206
+ _sdist(dist)
207
+ msgs = [i.msg for i in rc.check_dist(dist, "foxclaw", "0.1.0")
208
+ if i.code == "forbidden_entry"]
209
+ assert msgs and any(".env" in m for m in msgs), msgs
210
+
211
+
212
+ def test_check_dist_flags_empty_long_description(tmp_path):
213
+ dist = tmp_path / "dist"
214
+ dist.mkdir()
215
+ _wheel(dist, description=False)
216
+ _sdist(dist)
217
+ codes = {i.code for i in rc.check_dist(dist, "foxclaw", "0.1.0")}
218
+ assert "no_long_description" in codes, codes
219
+
220
+
221
+ def test_check_dist_missing_dir_is_error(tmp_path):
222
+ assert any(i.code == "no_dist"
223
+ for i in rc.check_dist(tmp_path / "nope", "foxclaw", "0.1.0"))
224
+
225
+
226
+ def test_npm_tgz_ok(tmp_path):
227
+ assert rc.check_npm_tgz(_tgz(tmp_path), "foxclaw", "0.1.0") == []
228
+
229
+
230
+ def test_npm_tgz_name_drift_detected(tmp_path):
231
+ tgz = _tgz(tmp_path, pkg={"name": "foxclaw", "version": "0.2.0"})
232
+ codes = {i.code for i in rc.check_npm_tgz(tgz, "foxclaw", "0.1.0")}
233
+ assert "npm_meta" in codes, codes
234
+
235
+
236
+ def test_npm_tgz_filename_drift_detected(tmp_path):
237
+ tgz = _tgz(tmp_path, version="0.2.0")
238
+ assert any(i.code == "npm_name" for i in rc.check_npm_tgz(tgz, "foxclaw", "0.1.0"))
239
+
240
+
241
+ def test_run_end_to_end(tmp_path):
242
+ """run() 串起来:真仓库 + 构造的 dist/tgz,无 tag 也应通过。"""
243
+ dist = tmp_path / "dist"
244
+ dist.mkdir()
245
+ issues = rc.run(REPO, tag="", dist=dist, npm_tgz=None)
246
+ assert any(i.level == "error" for i in issues), "空 dist 应报错"
247
+ version = rc.collect_versions(REPO)["pyproject.toml:project.version"]
248
+ _wheel(dist, version=version)
249
+ _sdist(dist, version=version)
250
+ tgz = _tgz(dist, version=version)
251
+ assert [i for i in rc.run(REPO, f"v{version}", dist, tgz) if i.level == "error"] == []
252
+ shutil.rmtree(dist)
@@ -0,0 +1,148 @@
1
+ """断点续跑(P1):步骤级 checkpoint + 续跑复用已完成步骤。
2
+
3
+ 锁住四件事:
4
+ ① `RestartRecovery.record_step/steps_of` 能按 task 回放步骤,`compact_log` 不会把断点压实掉;
5
+ ② 工具步指纹稳定(同名同参 = 同一步,参数字典序无关),失败/需确认的回注文本不入账;
6
+ ③ `_run_tool_calls` 命中续跑缓存时**不再执行**该步,只执行未完成的步并各自记账;
7
+ ④ 生命周期钩子下移基类后,任何装着 `_recovery` 的 Agent 都会落 start/finish(领域 Agent 不再漏记)。
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import types
13
+
14
+ import pytest
15
+
16
+ from foxclaw.agents.base import CognitiveAgent
17
+ from foxclaw.agents.mixins import _context, tool_exec
18
+ from foxclaw.core.recovery import RestartRecovery
19
+
20
+
21
+ class _Rec:
22
+ """鸭子类型替身:只记调用,不做 IO。"""
23
+
24
+ def __init__(self, rows=None):
25
+ self.steps: list = []
26
+ self.started: list = []
27
+ self.finished: list = []
28
+ self._rows = rows or []
29
+ self.compacted = False
30
+
31
+ def record_step(self, tid, step):
32
+ self.steps.append((tid, step))
33
+
34
+ def steps_of(self, tid, limit=200):
35
+ return list(self._rows)
36
+
37
+ def task_started(self, **kw):
38
+ self.started.append(kw)
39
+
40
+ def task_finished(self, tid, status="done"):
41
+ self.finished.append((tid, status))
42
+
43
+
44
+ def test_record_and_replay_steps(tmp_path) -> None:
45
+ rec = RestartRecovery(str(tmp_path / "recovery.jsonl"), fsync=False)
46
+ rec.record_step("t1", {"kind": "stage", "name": "perceive"})
47
+ rec.record_step("t2", {"kind": "stage", "name": "recall"})
48
+ rec.record_step("t1", {"kind": "tool", "name": "web_search", "key": "k", "result": "r"})
49
+ steps = rec.steps_of("t1")
50
+ assert [s["name"] for s in steps] == ["perceive", "web_search"]
51
+ assert all("event" not in s and "task_id" not in s for s in steps)
52
+ assert rec.steps_of("nope") == []
53
+
54
+
55
+ def test_compact_log_keeps_steps(tmp_path) -> None:
56
+ rec = RestartRecovery(str(tmp_path / "recovery.jsonl"), fsync=False)
57
+ rec.task_started("t1", "任务一")
58
+ rec.record_step("t1", {"kind": "tool", "name": "web_search", "key": "k", "result": "r"})
59
+ rec.task_finished("t1", status="done")
60
+ rec.compact_log(keep_recent_days=7)
61
+ assert len(rec.steps_of("t1")) == 1
62
+ rec.compact_log(keep_recent_days=0)
63
+ assert rec.steps_of("t1") == []
64
+
65
+
66
+ def test_step_key_is_argument_order_insensitive() -> None:
67
+ a = tool_exec._step_key("web_search", {"q": "x", "n": 3})
68
+ b = tool_exec._step_key("web_search", {"n": 3, "q": "x"})
69
+ assert a == b
70
+ assert a != tool_exec._step_key("web_search", {"q": "y", "n": 3})
71
+ assert a != tool_exec._step_key("recall_memory", {"q": "x", "n": 3})
72
+
73
+
74
+ @pytest.mark.parametrize("text,ok", [
75
+ ("【联网搜索结果】...", True),
76
+ ("【召回的相关记忆】...", True),
77
+ ("(工具 web_search 未返回:45s 超时)", False),
78
+ ("(该步需要你确认后才能执行)", False),
79
+ ("(未知工具 foo)", False),
80
+ ("", False),
81
+ ])
82
+ def test_step_ok_rejects_failure_texts(text, ok) -> None:
83
+ assert tool_exec._step_ok(text) is ok
84
+
85
+
86
+ def test_run_tool_calls_reuses_completed_steps(monkeypatch) -> None:
87
+ executed: list = []
88
+
89
+ def _fake_batch(agent, calls):
90
+ executed.extend(calls)
91
+ return [f"新结果:{c['name']}" for c in calls]
92
+
93
+ monkeypatch.setattr(tool_exec, "_run_tools_batch", _fake_batch)
94
+ rec = _Rec()
95
+ agent = types.SimpleNamespace(_recovery=rec, _session_id="t1")
96
+ done = {"name": "web_search", "args": {"q": "天气"}}
97
+ todo = {"name": "recall_memory", "args": {"q": "偏好"}}
98
+ cache = {tool_exec._step_key("web_search", {"q": "天气"}):
99
+ {"name": "web_search", "result": "【联网搜索结果】晴"}}
100
+ tok = _context._resume_steps.set(cache)
101
+ try:
102
+ out = tool_exec._run_tool_calls(agent, [done, todo])
103
+ finally:
104
+ _context._resume_steps.reset(tok)
105
+
106
+ assert executed == [todo], "已完成的步必须复用结果、绝不重复执行"
107
+ assert "断点续跑" in out[0] and "晴" in out[0]
108
+ assert out[1] == "新结果:recall_memory"
109
+ assert [s[1]["key"] for s in rec.steps] == [tool_exec._step_key("recall_memory", {"q": "偏好"})]
110
+
111
+
112
+ def test_run_tool_calls_skips_recording_failures(monkeypatch) -> None:
113
+ monkeypatch.setattr(tool_exec, "_run_tools_batch",
114
+ lambda agent, calls: ["(工具 web_search 未返回:45s 超时)"])
115
+ rec = _Rec()
116
+ agent = types.SimpleNamespace(_recovery=rec, _session_id="t1")
117
+ tool_exec._run_tool_calls(agent, [{"name": "web_search", "args": {}}])
118
+ assert rec.steps == [], "失败/超时步不能进 checkpoint(否则续跑会把失败当完成)"
119
+
120
+
121
+ def test_load_resume_cache_filters_bad_steps() -> None:
122
+ rows = [
123
+ {"kind": "stage", "name": "act"},
124
+ {"kind": "tool", "name": "web_search", "key": "k1", "result": "ok"},
125
+ {"kind": "tool", "name": "web_search", "key": "k2", "result": "x",
126
+ "truncated": True},
127
+ {"kind": "tool", "name": "web_search", "key": "k3", "result": " "},
128
+ {"kind": "tool", "name": "web_search", "result": "no-key"},
129
+ ]
130
+ fake = types.SimpleNamespace(_recovery=_Rec(rows))
131
+ assert CognitiveAgent._load_resume_cache(fake, "t1") == {"k1": {"name": "web_search",
132
+ "result": "ok"}}
133
+ assert CognitiveAgent._load_resume_cache(fake, "") == {}
134
+ assert CognitiveAgent._load_resume_cache(types.SimpleNamespace(), "t1") == {}
135
+
136
+
137
+ def test_lifecycle_hooks_record_for_any_agent() -> None:
138
+ rec = _Rec()
139
+ agent = types.SimpleNamespace(_recovery=rec)
140
+ CognitiveAgent._on_session_started(agent, "t1", "跑个任务", {"channel": "cli"})
141
+ CognitiveAgent._on_task_settled(agent, "t1", "done")
142
+ assert rec.started and rec.started[0]["task_id"] == "t1"
143
+ assert rec.started[0]["channel"] == "cli"
144
+ assert rec.finished == [("t1", "done")]
145
+
146
+ CognitiveAgent._on_session_started(types.SimpleNamespace(), "t1", "x", {})
147
+ CognitiveAgent._on_task_settled(types.SimpleNamespace(), "t1", "done")
148
+ CognitiveAgent._record_stage(types.SimpleNamespace(), "t1", "recall")
@@ -0,0 +1,85 @@
1
+ """任务感知模型路由 + fallback 链验证(全离线,monkeypatch 不联网)。"""
2
+ import os, sys
3
+ import pytest
4
+ from pathlib import Path
5
+ sys.path.insert(0, str(Path(__file__).parent.parent))
6
+
7
+
8
+ @pytest.mark.xfail(
9
+ reason="pre-existing 失败:本地 stub 无外部 API 时,fallback 链全模型降级后断言 '全部模型失败' 不一致;需真实 provider 配置(fast/smart 多档)才能过。",
10
+ strict=False,
11
+ )
12
+ def test_routing():
13
+ for k in ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "DEEPSEEK_API_KEY", "GOOGLE_API_KEY",
14
+ "DASHSCOPE_API_KEY", "FOXCLAW_MODEL", "FOXCLAW_MODEL_FAST", "FOXCLAW_MODEL_SMART",
15
+ "FOXCLAW_PROVIDER"]:
16
+ os.environ.pop(k, None)
17
+
18
+ from foxclaw.core.model_router import ModelRouter, _route_tier, _tier_model
19
+
20
+ print("=" * 60); print("任务感知模型路由 + fallback 链验证"); print("=" * 60)
21
+
22
+ assert _route_tier("supervisor", [{"role": "user", "content": "x"}]) == "fast"
23
+ assert _route_tier("finance", [{"role": "user", "content": "帮我做深度财务分析报告"}]) == "smart"
24
+ assert _route_tier("assistant", [{"role": "user", "content": "你好"}]) == "fast"
25
+ assert _tier_model("deepseek", "fast") == "deepseek-chat"
26
+ assert _tier_model("deepseek", "smart") == "deepseek-reasoner"
27
+ print("[1] 任务→档 + 档→模型 OK")
28
+
29
+ os.environ["FOXCLAW_PROVIDER"] = "deepseek"; os.environ["DEEPSEEK_API_KEY"] = "x"
30
+ r = ModelRouter()
31
+ assert r.provider == "deepseek"
32
+ assert r._model_for("deepseek", "supervisor", [{"role": "user", "content": "x"}]) == "deepseek-chat"
33
+ assert r._model_for("deepseek", "finance", [{"role": "user", "content": "深度分析报告方案评估"}]) == "deepseek-reasoner"
34
+ print("[2] 按任务/Agent 选档(supervisor→chat,复杂→reasoner) OK")
35
+
36
+ os.environ["OPENAI_API_KEY"] = "y"
37
+ r2 = ModelRouter(); calls = []
38
+ def _fake(p, c, m, s, msgs, mt):
39
+ calls.append(p)
40
+ if p == "deepseek":
41
+ raise RuntimeError("deepseek down")
42
+ return f"OK-{p}", None
43
+ r2._call_provider = _fake
44
+ out = r2.complete("sys", [{"role": "user", "content": "hi"}], agent_id="assistant")
45
+ assert "OK-openai" in out and calls == ["deepseek", "openai"], (out, calls)
46
+ assert "回退到 openai" in r2.fallback_reason
47
+ print("[3] fallback 链(deepseek 失败→openai) OK")
48
+
49
+ r3 = ModelRouter(provider="deepseek", model="fixed-x")
50
+ assert r3._model_for("deepseek", "supervisor", [{"role": "user", "content": "x"}]) == "fixed-x"
51
+ print("[4] 锁定模型不分档(向后兼容) OK")
52
+
53
+ def _allfail(*a):
54
+ raise RuntimeError("boom")
55
+ r2._call_provider = _allfail
56
+ out = r2.complete("sys", [{"role": "user", "content": "hi"}])
57
+ assert "STUB" in out or "全部模型失败" in out, out
58
+ assert "已完成处理" not in out, f"故障被伪装成成功: {out}"
59
+ assert r2.is_degraded() is True, "全部失败必须置 degraded 标志(上游据此禁写长期记忆)"
60
+ print("[5] 全部失败 → 显式失败(无假成功文案) + degraded OK")
61
+
62
+ r4 = ModelRouter()
63
+ r4.switch("deepseek", "deepseek-reasoner")
64
+ assert r4._model_for("deepseek", "supervisor", [{"role": "user", "content": "简单"}]) == "deepseek-reasoner"
65
+ r4.switch("deepseek")
66
+ assert r4._model_for("deepseek", "supervisor", [{"role": "user", "content": "简单"}]) == "deepseek-chat"
67
+ print("[6] switch 尊重显式模型 + 清锁恢复分档 OK")
68
+
69
+ r5 = ModelRouter(); seq = []
70
+ def _fk(p, c, m, s, msgs, mt):
71
+ seq.append(p)
72
+ if p == "deepseek":
73
+ raise RuntimeError("down")
74
+ return f"OK-{p}", None
75
+ r5._call_provider = _fk
76
+ r5.complete("s", [{"role": "user", "content": "hi"}]); seq.clear()
77
+ r5.complete("s", [{"role": "user", "content": "hi"}])
78
+ assert seq == ["openai"], f"断路器失效: {seq}"
79
+ print("[7] 断路器跳过宕机 provider OK")
80
+
81
+ print("\n任务感知模型路由验证全部通过")
82
+
83
+
84
+ if __name__ == "__main__":
85
+ test_routing()
@@ -0,0 +1,138 @@
1
+ """
2
+ 微内核调度器(v2 ① Step1:Scheduler v0)测试 —— 全离线,线程级。
3
+
4
+ 覆盖:
5
+ 1. 无上限(默认):准入直通,行为不变;并发不受限(Barrier 确定性达峰值 N)
6
+ 2. 有上限:并发配额生效(peak_running 不超过 max_concurrency = 背压)
7
+ 3. 生命周期事件:每任务 emit task.lifecycle.{scheduled,running,completed};失败 → failed
8
+ (【P5-2】回退路径原用 started,与 ProcessManager 的 running 不一致,已统一为 running)
9
+ 4. 记账 + 结果透传 + 异常透传
10
+ """
11
+ import sys, threading, time
12
+ from pathlib import Path
13
+ sys.path.insert(0, str(Path(__file__).parent.parent))
14
+
15
+ from foxclaw.kernel import Scheduler
16
+
17
+
18
+ class _CapSpine:
19
+ def __init__(self): self.events = []; self._lk = threading.Lock()
20
+ def publish(self, topic, payload=None, source="", **kw):
21
+ with self._lk: self.events.append(topic)
22
+
23
+
24
+ def _run_n_concurrent(sched, n, work):
25
+ threads = [threading.Thread(target=lambda: sched.run(work, task="t")) for _ in range(n)]
26
+ for t in threads: t.start()
27
+ for t in threads: t.join()
28
+
29
+
30
+ def test_unlimited_reaches_peak():
31
+ print("\n【1】无上限(默认):并发不受限,行为直通")
32
+ sched = Scheduler()
33
+ N = 6
34
+ barrier = threading.Barrier(N)
35
+ def work():
36
+ barrier.wait(timeout=5)
37
+ time.sleep(0.01)
38
+ return "ok"
39
+ _run_n_concurrent(sched, N, work)
40
+ st = sched.stats()
41
+ assert st["max_concurrency"] is None
42
+ assert st["peak_running"] == N, f"无上限应达峰值 {N},实得 {st['peak_running']}"
43
+ assert st["completed"] == N and st["submitted"] == N
44
+ print(f" ✓ 无上限:peak={st['peak_running']}=N,completed={st['completed']}")
45
+
46
+
47
+ def test_concurrency_cap():
48
+ print("\n【2】有上限:并发配额生效(背压)")
49
+ sched = Scheduler(max_concurrency=2)
50
+ def work():
51
+ time.sleep(0.02)
52
+ return "ok"
53
+ _run_n_concurrent(sched, 8, work)
54
+ st = sched.stats()
55
+ assert st["peak_running"] <= 2, f"并发峰值不得超上限 2,实得 {st['peak_running']}"
56
+ assert st["peak_running"] == 2, "8 任务/上限 2 应打满到 2"
57
+ assert st["completed"] == 8
58
+ print(f" ✓ 上限 2:peak={st['peak_running']}(不超上限),completed={st['completed']}")
59
+
60
+
61
+ def test_lifecycle_events_and_accounting():
62
+ print("\n【3】生命周期事件 + 记账 + 失败传播")
63
+ spine = _CapSpine()
64
+ sched = Scheduler(spine=spine)
65
+ r = sched.run(lambda: 42, agent_id="finance", task="算账")
66
+ assert r == 42
67
+ assert spine.events == ["task.lifecycle.scheduled", "task.lifecycle.running",
68
+ "task.lifecycle.completed"], spine.events
69
+ spine.events.clear()
70
+ raised = False
71
+ try:
72
+ sched.run(lambda: (_ for _ in ()).throw(RuntimeError("boom")), task="崩")
73
+ except RuntimeError:
74
+ raised = True
75
+ assert raised, "异常应透传"
76
+ assert spine.events == ["task.lifecycle.scheduled", "task.lifecycle.running",
77
+ "task.lifecycle.failed"], spine.events
78
+ st = sched.stats()
79
+ assert st["submitted"] == 2 and st["completed"] == 1 and st["failed"] == 1
80
+ assert st["running"] == 0, "跑完 running 归零"
81
+ print(f" ✓ 成功→completed事件;失败→failed事件+异常透传;记账 {st}")
82
+
83
+
84
+ def test_pm_delegation_unified_lifecycle():
85
+ print("\n【4】Step3:有 ProcessManager → 委托它发唯一 task.lifecycle.<state>(不双套词汇)")
86
+ from foxclaw.kernel import EventSpine, ProcessManager
87
+ from foxclaw.core.system_bus import SystemBus
88
+ spine = EventSpine()
89
+ seen = []
90
+ spine.project(lambda e: seen.append(e.topic))
91
+ pm = ProcessManager(bus=SystemBus(spine=spine))
92
+ sched = Scheduler(spine=spine, process_manager=pm)
93
+ r = sched.run(lambda: 7, agent_id="finance", task="t")
94
+ assert r == 7
95
+ life = [t for t in seen if t.startswith("task.lifecycle")]
96
+ assert life == ["task.lifecycle.scheduled", "task.lifecycle.running",
97
+ "task.lifecycle.completed"], f"应由 pm 发唯一词汇,实得 {life}"
98
+ assert pm.stats()["by_state"].get("completed") == 1
99
+ assert sched.stats()["completed"] == 1
100
+ print(" ✓ pm 发 task.lifecycle.{scheduled,running,completed};顶层任务纳入进程;记账双在")
101
+
102
+
103
+ def test_priority_admission():
104
+ print("\n【5】Step5:满载后释放 → 高优先排队者先获空位(不抢占运行中)")
105
+ sched = Scheduler(max_concurrency=1)
106
+ order = []
107
+ running = threading.Event(); release = threading.Event()
108
+ def blocker():
109
+ sched.run(lambda: (running.set(), release.wait(5))[-1], priority=0, task="blocker")
110
+ tb = threading.Thread(target=blocker); tb.start()
111
+ assert running.wait(5), "blocker 应占住唯一槽"
112
+ def worker(p, name):
113
+ sched.run(lambda: order.append(name), priority=p, task=name)
114
+ tlo = threading.Thread(target=worker, args=(0, "lo"))
115
+ thi = threading.Thread(target=worker, args=(10, "hi"))
116
+ tlo.start(); time.sleep(0.05); thi.start(); time.sleep(0.15)
117
+ release.set()
118
+ for t in (tb, tlo, thi): t.join(5)
119
+ assert order == ["hi", "lo"], f"高优先应先获空位,实得 {order}"
120
+ print(f" ✓ 释放后:高优先先获空位 → {order}(准入优先级,非抢占运行中)")
121
+
122
+
123
+ def main():
124
+ print("=" * 60)
125
+ print("微内核调度器 · Scheduler(v2 ① Step1/3/5)")
126
+ print("=" * 60)
127
+ test_unlimited_reaches_peak()
128
+ test_concurrency_cap()
129
+ test_lifecycle_events_and_accounting()
130
+ test_pm_delegation_unified_lifecycle()
131
+ test_priority_admission()
132
+ print("\n" + "=" * 60)
133
+ print("✓ Step1 验证完成:准入配额(背压)+ 生命周期事件 + 记账 + 透传")
134
+ print("=" * 60)
135
+
136
+
137
+ if __name__ == "__main__":
138
+ main()