foxclaw 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (495) hide show
  1. package/README.md +54 -29
  2. package/bin/foxclaw.cjs +51 -0
  3. package/index.js +11 -10
  4. package/package.json +46 -38
  5. package/python/.dockerignore +35 -0
  6. package/python/.env.example +165 -0
  7. package/python/.gitattributes +46 -0
  8. package/python/.github/workflows/ci.yml +122 -0
  9. package/python/.github/workflows/release.yml +183 -0
  10. package/python/Dockerfile +64 -0
  11. package/python/LICENSE +21 -0
  12. package/python/MANIFEST.in +14 -0
  13. package/python/README.md +192 -0
  14. package/python/README_CN.md +324 -0
  15. package/python/bench/bm25_audit.py +199 -0
  16. package/python/bench/bm25_regression.py +178 -0
  17. package/python/bench/bm25_stress.py +159 -0
  18. package/python/bench/cd_audit.py +238 -0
  19. package/python/bench/chaos.py +142 -0
  20. package/python/bench/concurrency.py +103 -0
  21. package/python/bench/e2e_real_models.py +95 -0
  22. package/python/bench/llm_retry_test.py +248 -0
  23. package/python/bench/load_test.py +196 -0
  24. package/python/bench/vector_ivf_upgrade.py +134 -0
  25. package/python/docs/CHANNELS.md +195 -0
  26. package/python/docs/GOVERNANCE.md +164 -0
  27. package/python/docs/RELEASE.md +109 -0
  28. package/python/docs/RUN.md +75 -0
  29. package/python/docs/RUN_CN.md +121 -0
  30. package/python/docs//351/205/215/347/275/256/346/214/207/345/215/227.md +152 -0
  31. package/python/evals/__init__.py +0 -0
  32. package/python/evals/governance/README.md +61 -0
  33. package/python/evals/governance/__init__.py +0 -0
  34. package/python/evals/governance/cases.py +152 -0
  35. package/python/evals/governance/compare_embeddings.py +94 -0
  36. package/python/evals/governance/convergence.svg +11 -0
  37. package/python/evals/governance/report.json +371 -0
  38. package/python/evals/governance/run_evals.py +400 -0
  39. package/python/examples/governance_middleware_demo.py +77 -0
  40. package/python/examples/langgraph_governance_demo.py +108 -0
  41. package/python/examples/sample_itinerary.csv +6 -0
  42. package/python/examples/sample_knowledge.csv +9 -0
  43. package/python/examples/sample_transactions.csv +14 -0
  44. package/python/foxclaw/__init__.py +526 -0
  45. package/python/foxclaw/agents/__init__.py +12 -0
  46. package/python/foxclaw/agents/agents.py +35 -0
  47. package/python/foxclaw/agents/base.py +865 -0
  48. package/python/foxclaw/agents/impl/__init__.py +1 -0
  49. package/python/foxclaw/agents/impl/_helpers.py +78 -0
  50. package/python/foxclaw/agents/impl/finance.py +248 -0
  51. package/python/foxclaw/agents/impl/office.py +367 -0
  52. package/python/foxclaw/agents/impl/supervisor.py +659 -0
  53. package/python/foxclaw/agents/mixins/_context.py +151 -0
  54. package/python/foxclaw/agents/mixins/augment.py +149 -0
  55. package/python/foxclaw/agents/mixins/llm.py +66 -0
  56. package/python/foxclaw/agents/mixins/memory.py +35 -0
  57. package/python/foxclaw/agents/mixins/preference.py +81 -0
  58. package/python/foxclaw/agents/mixins/prompt.py +77 -0
  59. package/python/foxclaw/agents/mixins/skillbox.py +58 -0
  60. package/python/foxclaw/agents/mixins/tool_exec.py +443 -0
  61. package/python/foxclaw/api/__init__.py +283 -0
  62. package/python/foxclaw/api/app.py +123 -0
  63. package/python/foxclaw/api/middleware.py +317 -0
  64. package/python/foxclaw/api/routes/ahs.py +417 -0
  65. package/python/foxclaw/api/routes/attachments.py +117 -0
  66. package/python/foxclaw/api/routes/browser.py +39 -0
  67. package/python/foxclaw/api/routes/domain.py +314 -0
  68. package/python/foxclaw/api/routes/knowledge.py +163 -0
  69. package/python/foxclaw/api/routes/llm_admin.py +461 -0
  70. package/python/foxclaw/api/routes/memory.py +89 -0
  71. package/python/foxclaw/api/routes/orchestration.py +41 -0
  72. package/python/foxclaw/api/routes/recovery.py +43 -0
  73. package/python/foxclaw/api/routes/review.py +275 -0
  74. package/python/foxclaw/api/routes/search.py +37 -0
  75. package/python/foxclaw/api/routes/system.py +327 -0
  76. package/python/foxclaw/api/routes/task.py +281 -0
  77. package/python/foxclaw/api/routes/ws.py +68 -0
  78. package/python/foxclaw/api/spine.py +55 -0
  79. package/python/foxclaw/browser/__init__.py +13 -0
  80. package/python/foxclaw/browser/action/__init__.py +1 -0
  81. package/python/foxclaw/browser/action/action_executor.py +54 -0
  82. package/python/foxclaw/browser/action/action_types.py +45 -0
  83. package/python/foxclaw/browser/action/action_verifier.py +68 -0
  84. package/python/foxclaw/browser/action/url_policy.py +147 -0
  85. package/python/foxclaw/browser/adapter/__init__.py +1 -0
  86. package/python/foxclaw/browser/adapter/base_adapter.py +23 -0
  87. package/python/foxclaw/browser/adapter/web_adapter.py +46 -0
  88. package/python/foxclaw/browser/driver/__init__.py +1 -0
  89. package/python/foxclaw/browser/driver/base_driver.py +61 -0
  90. package/python/foxclaw/browser/driver/playwright_driver.py +166 -0
  91. package/python/foxclaw/browser/perception/__init__.py +1 -0
  92. package/python/foxclaw/browser/perception/dom_extractor.py +43 -0
  93. package/python/foxclaw/browser/perception/element_indexer.py +57 -0
  94. package/python/foxclaw/browser/perception/semantic_reducer.py +33 -0
  95. package/python/foxclaw/browser/perception/snapshot.py +29 -0
  96. package/python/foxclaw/browser/service.py +148 -0
  97. package/python/foxclaw/browser/session/__init__.py +1 -0
  98. package/python/foxclaw/browser/session/session_manager.py +57 -0
  99. package/python/foxclaw/capability/__init__.py +12 -0
  100. package/python/foxclaw/capability/embedding_service.py +67 -0
  101. package/python/foxclaw/channels/__init__.py +140 -0
  102. package/python/foxclaw/channels/discord.py +184 -0
  103. package/python/foxclaw/channels/dispatcher.py +204 -0
  104. package/python/foxclaw/channels/feishu.py +225 -0
  105. package/python/foxclaw/channels/openclaw_bridge.py +156 -0
  106. package/python/foxclaw/channels/signing.py +187 -0
  107. package/python/foxclaw/channels/telegram.py +121 -0
  108. package/python/foxclaw/channels/weixin.py +225 -0
  109. package/python/foxclaw/cli.py +154 -0
  110. package/python/foxclaw/cognitive/__init__.py +4 -0
  111. package/python/foxclaw/cognitive/reasoner.py +82 -0
  112. package/python/foxclaw/cognitive/state.py +147 -0
  113. package/python/foxclaw/core/__init__.py +9 -0
  114. package/python/foxclaw/core/audit_chain.py +133 -0
  115. package/python/foxclaw/core/audit_sink.py +100 -0
  116. package/python/foxclaw/core/compaction.py +283 -0
  117. package/python/foxclaw/core/config.py +65 -0
  118. package/python/foxclaw/core/confirm_nonce.py +157 -0
  119. package/python/foxclaw/core/dispatch.py +27 -0
  120. package/python/foxclaw/core/envfile.py +48 -0
  121. package/python/foxclaw/core/failure_policy.py +151 -0
  122. package/python/foxclaw/core/gateway.py +85 -0
  123. package/python/foxclaw/core/governance_audit.py +175 -0
  124. package/python/foxclaw/core/idempotency.py +289 -0
  125. package/python/foxclaw/core/injection_guard.py +221 -0
  126. package/python/foxclaw/core/memory_adapter.py +516 -0
  127. package/python/foxclaw/core/metrics.py +89 -0
  128. package/python/foxclaw/core/model_router.py +1097 -0
  129. package/python/foxclaw/core/paths.py +103 -0
  130. package/python/foxclaw/core/recovery.py +362 -0
  131. package/python/foxclaw/core/scheduler.py +144 -0
  132. package/python/foxclaw/core/secrets.py +134 -0
  133. package/python/foxclaw/core/sessions.py +157 -0
  134. package/python/foxclaw/core/settings.py +430 -0
  135. package/python/foxclaw/core/shared_write.py +198 -0
  136. package/python/foxclaw/core/steering_queue.py +153 -0
  137. package/python/foxclaw/core/system_bus.py +84 -0
  138. package/python/foxclaw/core/thought_chain.py +318 -0
  139. package/python/foxclaw/core/token_meter.py +235 -0
  140. package/python/foxclaw/core/tools.py +172 -0
  141. package/python/foxclaw/core/tracing.py +390 -0
  142. package/python/foxclaw/core/truth_pipeline.py +114 -0
  143. package/python/foxclaw/core/vec_recall_adapter.py +503 -0
  144. package/python/foxclaw/core/verify.py +181 -0
  145. package/python/foxclaw/core/web_search.py +242 -0
  146. package/python/foxclaw/finance/__init__.py +15 -0
  147. package/python/foxclaw/finance/evidence.py +153 -0
  148. package/python/foxclaw/finance/fx.py +100 -0
  149. package/python/foxclaw/finance/graph.py +188 -0
  150. package/python/foxclaw/finance/models.py +150 -0
  151. package/python/foxclaw/finance/nlparse.py +575 -0
  152. package/python/foxclaw/finance/policy.py +329 -0
  153. package/python/foxclaw/finance/reasoner.py +129 -0
  154. package/python/foxclaw/finance/reasoning.py +340 -0
  155. package/python/foxclaw/finance/reconcile.py +180 -0
  156. package/python/foxclaw/finance/runtime.py +527 -0
  157. package/python/foxclaw/governance/__init__.py +24 -0
  158. package/python/foxclaw/governance/middleware.py +184 -0
  159. package/python/foxclaw/harness/__init__.py +17 -0
  160. package/python/foxclaw/harness/_base.py +72 -0
  161. package/python/foxclaw/harness/_policy.py +359 -0
  162. package/python/foxclaw/harness/drift.py +89 -0
  163. package/python/foxclaw/harness/event_bus.py +31 -0
  164. package/python/foxclaw/harness/harness.py +446 -0
  165. package/python/foxclaw/harness/loop_detection.py +152 -0
  166. package/python/foxclaw/itinerary/__init__.py +10 -0
  167. package/python/foxclaw/itinerary/amap_client.py +117 -0
  168. package/python/foxclaw/itinerary/models.py +80 -0
  169. package/python/foxclaw/itinerary/reasoner.py +101 -0
  170. package/python/foxclaw/itinerary/reasoning.py +174 -0
  171. package/python/foxclaw/itinerary/runtime.py +148 -0
  172. package/python/foxclaw/kernel/__init__.py +13 -0
  173. package/python/foxclaw/kernel/capabilities.py +73 -0
  174. package/python/foxclaw/kernel/policy_gate.py +114 -0
  175. package/python/foxclaw/kernel/process_manager.py +205 -0
  176. package/python/foxclaw/kernel/scheduler.py +131 -0
  177. package/python/foxclaw/kernel/sentinel.py +453 -0
  178. package/python/foxclaw/kernel/sentinel_client.py +246 -0
  179. package/python/foxclaw/kernel/spine.py +97 -0
  180. package/python/foxclaw/knowledge/__init__.py +376 -0
  181. package/python/foxclaw/knowledge/truth_crystallizer.py +92 -0
  182. package/python/foxclaw/learning/__init__.py +12 -0
  183. package/python/foxclaw/learning/knowledge_graph.py +149 -0
  184. package/python/foxclaw/learning/models.py +93 -0
  185. package/python/foxclaw/learning/reasoner.py +70 -0
  186. package/python/foxclaw/learning/runtime.py +363 -0
  187. package/python/foxclaw/learning/spaced_repetition.py +96 -0
  188. package/python/foxclaw/mcp_server.py +189 -0
  189. package/python/foxclaw/orchestration/__init__.py +184 -0
  190. package/python/foxclaw/pipeline/__init__.py +23 -0
  191. package/python/foxclaw/pipeline/cognitive_pipeline.py +481 -0
  192. package/python/foxclaw/pipeline/supervisor_stages.py +230 -0
  193. package/python/foxclaw/planning/__init__.py +17 -0
  194. package/python/foxclaw/planning/llm_planner.py +232 -0
  195. package/python/foxclaw/planning/planner.py +99 -0
  196. package/python/foxclaw/search/__init__.py +24 -0
  197. package/python/foxclaw/search/agent_port.py +194 -0
  198. package/python/foxclaw/search/evidence.py +113 -0
  199. package/python/foxclaw/search/external_search.py +50 -0
  200. package/python/foxclaw/search/hkc_read_port.py +60 -0
  201. package/python/foxclaw/search/internal_search.py +73 -0
  202. package/python/foxclaw/search/models.py +129 -0
  203. package/python/foxclaw/search/planner.py +44 -0
  204. package/python/foxclaw/search/retriever.py +39 -0
  205. package/python/foxclaw/search/runtime.py +134 -0
  206. package/python/foxclaw/search/strategy.py +44 -0
  207. package/python/foxclaw/skills/__init__.py +613 -0
  208. package/python/foxclaw/skills/hooks_spec.py +73 -0
  209. package/python/foxclaw/skills/lifecycle.py +278 -0
  210. package/python/foxclaw/skills/markdown_skill.py +174 -0
  211. package/python/foxclaw/skills/markdown_store.py +39 -0
  212. package/python/foxclaw/skills/mcp_client.py +190 -0
  213. package/python/foxclaw/skills/mcp_store.py +43 -0
  214. package/python/foxclaw/skills/permission.py +180 -0
  215. package/python/foxclaw/skills/pipeline.py +386 -0
  216. package/python/foxclaw/skills/policy.py +80 -0
  217. package/python/foxclaw/skills/review.py +72 -0
  218. package/python/foxclaw/skills/store.py +57 -0
  219. package/python/frontend/assets/core_bg.jpg +0 -0
  220. package/python/frontend/deck/auth.js +32 -0
  221. package/python/frontend/deck/chat_model_picker.js +537 -0
  222. package/python/frontend/deck/domain.js +296 -0
  223. package/python/frontend/deck/main.js +1950 -0
  224. package/python/frontend/deck/markdown.js +48 -0
  225. package/python/frontend/deck/settings.js +324 -0
  226. package/python/frontend/deck/state.js +6 -0
  227. package/python/frontend/deck/util.js +6 -0
  228. package/python/frontend/deck/visuals.js +212 -0
  229. package/python/frontend/deck.css +855 -0
  230. package/python/frontend/deck.html +686 -0
  231. package/python/frontend/deck_v2.html +341 -0
  232. package/python/frontend/planet/index.html +266 -0
  233. package/python/frontend/planet/planet.css +579 -0
  234. package/python/frontend/planet/planet.js +1637 -0
  235. package/python/frontend/planet/vendor/three.core.min.js +6 -0
  236. package/python/frontend/planet/vendor/three.module.min.js +6 -0
  237. package/python/frontend/v3/assets/index-BD7VPbMV.js +33 -0
  238. package/python/frontend/v3/assets/index-CAvnYWGH.css +1 -0
  239. package/python/frontend/v3/index.html +15 -0
  240. package/python/frontend/widget_runtime.js +295 -0
  241. package/python/frontend/widgets.js +696 -0
  242. package/python/frontend_src/index.html +14 -0
  243. package/python/frontend_src/package-lock.json +1376 -0
  244. package/python/frontend_src/package.json +19 -0
  245. package/python/frontend_src/src/App.vue +62 -0
  246. package/python/frontend_src/src/api.js +89 -0
  247. package/python/frontend_src/src/components/ChatDock.vue +377 -0
  248. package/python/frontend_src/src/components/ComposerMenu.vue +75 -0
  249. package/python/frontend_src/src/components/DocViewer.vue +171 -0
  250. package/python/frontend_src/src/components/Inspector.vue +241 -0
  251. package/python/frontend_src/src/components/LeftRail.vue +394 -0
  252. package/python/frontend_src/src/components/MenuPopover.vue +130 -0
  253. package/python/frontend_src/src/components/MessageStream.vue +401 -0
  254. package/python/frontend_src/src/components/ModelPicker.vue +355 -0
  255. package/python/frontend_src/src/components/QuickIngest.vue +118 -0
  256. package/python/frontend_src/src/components/Stage.vue +226 -0
  257. package/python/frontend_src/src/components/ToolMenu.vue +144 -0
  258. package/python/frontend_src/src/components/TopBar.vue +90 -0
  259. package/python/frontend_src/src/components/panels/ActivityPanel.vue +84 -0
  260. package/python/frontend_src/src/components/panels/AgentsPanel.vue +86 -0
  261. package/python/frontend_src/src/components/panels/KnowledgePanel.vue +441 -0
  262. package/python/frontend_src/src/components/panels/MemoryPanel.vue +118 -0
  263. package/python/frontend_src/src/components/panels/PanelBox.vue +51 -0
  264. package/python/frontend_src/src/components/panels/ReviewPanel.vue +308 -0
  265. package/python/frontend_src/src/components/panels/SettingsPanel.vue +254 -0
  266. package/python/frontend_src/src/components/panels/TasksPanel.vue +74 -0
  267. package/python/frontend_src/src/components/panels/ToolsPanel.vue +640 -0
  268. package/python/frontend_src/src/components/panels/UsagePanel.vue +105 -0
  269. package/python/frontend_src/src/lib/fmt.js +48 -0
  270. package/python/frontend_src/src/lib/md.js +204 -0
  271. package/python/frontend_src/src/main.js +7 -0
  272. package/python/frontend_src/src/stores/app.js +106 -0
  273. package/python/frontend_src/src/stores/chat.js +484 -0
  274. package/python/frontend_src/src/stores/model.js +104 -0
  275. package/python/frontend_src/src/stores/panels.js +330 -0
  276. package/python/frontend_src/src/styles/base.css +138 -0
  277. package/python/frontend_src/src/styles/panel.css +117 -0
  278. package/python/frontend_src/src/styles/tokens.css +78 -0
  279. package/python/frontend_src/vite.config.js +46 -0
  280. package/python/pyproject.toml +265 -0
  281. package/python/skills/README.md +123 -0
  282. package/python/skills/execution-contract/SKILL.md +30 -0
  283. package/python/skills/general-assistant/SKILL.md +34 -0
  284. package/python/skills/planner/SKILL.md +33 -0
  285. package/python/skills/schedule-itinerary/SKILL.md +35 -0
  286. package/python/skills/skill-composition/SKILL.md +31 -0
  287. package/python/skills/skill-guard/SKILL.md +67 -0
  288. package/python/skills/skill-memory/SKILL.md +50 -0
  289. package/python/skills/skill-router/SKILL.md +35 -0
  290. package/python/tests/conftest.py +76 -0
  291. package/python/tests/run_all.py +263 -0
  292. package/python/tests/test_agent_lifecycle.py +360 -0
  293. package/python/tests/test_ahs_p2.py +414 -0
  294. package/python/tests/test_anthropic_tools.py +206 -0
  295. package/python/tests/test_api.py +87 -0
  296. package/python/tests/test_api_auth.py +83 -0
  297. package/python/tests/test_api_host_guard.py +69 -0
  298. package/python/tests/test_attachments.py +167 -0
  299. package/python/tests/test_audit_chain.py +121 -0
  300. package/python/tests/test_audit_sink.py +136 -0
  301. package/python/tests/test_browser.py +153 -0
  302. package/python/tests/test_browser_request_guard.py +184 -0
  303. package/python/tests/test_capabilities.py +158 -0
  304. package/python/tests/test_channel_signing.py +192 -0
  305. package/python/tests/test_channel_trust.py +109 -0
  306. package/python/tests/test_channels.py +184 -0
  307. package/python/tests/test_cli.py +46 -0
  308. package/python/tests/test_compaction_toolpair.py +119 -0
  309. package/python/tests/test_config_layering.py +65 -0
  310. package/python/tests/test_confirm_nonce.py +197 -0
  311. package/python/tests/test_context_compaction_wiring.py +130 -0
  312. package/python/tests/test_dispatch.py +95 -0
  313. package/python/tests/test_domain_evals.py +403 -0
  314. package/python/tests/test_domain_import.py +78 -0
  315. package/python/tests/test_domain_reasoners.py +148 -0
  316. package/python/tests/test_e2e.py +178 -0
  317. package/python/tests/test_emb_cache.py +107 -0
  318. package/python/tests/test_embedding_singleton.py +89 -0
  319. package/python/tests/test_envfile.py +83 -0
  320. package/python/tests/test_evals_smoke.py +114 -0
  321. package/python/tests/test_event_spine.py +182 -0
  322. package/python/tests/test_evidence_semantic.py +101 -0
  323. package/python/tests/test_evidence_sse.py +97 -0
  324. package/python/tests/test_file_ingest.py +146 -0
  325. package/python/tests/test_finance.py +153 -0
  326. package/python/tests/test_finance_ledger.py +252 -0
  327. package/python/tests/test_finance_persist.py +178 -0
  328. package/python/tests/test_gateway.py +144 -0
  329. package/python/tests/test_governance.py +468 -0
  330. package/python/tests/test_governance_audit.py +359 -0
  331. package/python/tests/test_governance_middleware.py +143 -0
  332. package/python/tests/test_hkc.py +286 -0
  333. package/python/tests/test_hkc_index_build_manager.py +339 -0
  334. package/python/tests/test_hkc_manifest_reuse.py +317 -0
  335. package/python/tests/test_hkc_search_event_sync.py +533 -0
  336. package/python/tests/test_import_path_guard.py +161 -0
  337. package/python/tests/test_injection_guard.py +211 -0
  338. package/python/tests/test_integration.py +141 -0
  339. package/python/tests/test_itinerary.py +103 -0
  340. package/python/tests/test_kee_semantic.py +89 -0
  341. package/python/tests/test_kernel.py +222 -0
  342. package/python/tests/test_learning.py +110 -0
  343. package/python/tests/test_llm_admin.py +160 -0
  344. package/python/tests/test_llm_planner.py +366 -0
  345. package/python/tests/test_markdown_skills.py +73 -0
  346. package/python/tests/test_markdown_skills_openclaw.py +71 -0
  347. package/python/tests/test_mcp_connectors.py +109 -0
  348. package/python/tests/test_mcp_guardian_discovery.py +91 -0
  349. package/python/tests/test_mcp_no_memory.py +43 -0
  350. package/python/tests/test_mcp_policy_gate.py +77 -0
  351. package/python/tests/test_mcp_server.py +95 -0
  352. package/python/tests/test_memory_recall.py +95 -0
  353. package/python/tests/test_metrics.py +66 -0
  354. package/python/tests/test_multi_chain.py +102 -0
  355. package/python/tests/test_multimodal.py +135 -0
  356. package/python/tests/test_negotiation.py +146 -0
  357. package/python/tests/test_orchestration.py +119 -0
  358. package/python/tests/test_orchestration_decoupled.py +109 -0
  359. package/python/tests/test_p3_p5_hardening.py +338 -0
  360. package/python/tests/test_parallel_dispatch.py +215 -0
  361. package/python/tests/test_pipeline.py +533 -0
  362. package/python/tests/test_plan_visible.py +86 -0
  363. package/python/tests/test_planner.py +95 -0
  364. package/python/tests/test_policy_gate.py +109 -0
  365. package/python/tests/test_reasoner.py +185 -0
  366. package/python/tests/test_release_check.py +263 -0
  367. package/python/tests/test_resume_checkpoint.py +156 -0
  368. package/python/tests/test_routing.py +95 -0
  369. package/python/tests/test_scheduler.py +141 -0
  370. package/python/tests/test_search.py +204 -0
  371. package/python/tests/test_secrets.py +159 -0
  372. package/python/tests/test_semantic.py +78 -0
  373. package/python/tests/test_sessions.py +91 -0
  374. package/python/tests/test_settings.py +281 -0
  375. package/python/tests/test_shadow_learning.py +330 -0
  376. package/python/tests/test_shared_write.py +207 -0
  377. package/python/tests/test_skill_taint_gate.py +154 -0
  378. package/python/tests/test_skills.py +133 -0
  379. package/python/tests/test_stats_and_events.py +165 -0
  380. package/python/tests/test_streaming.py +60 -0
  381. package/python/tests/test_supervisor_pipeline.py +177 -0
  382. package/python/tests/test_supervisor_triage.py +108 -0
  383. package/python/tests/test_taint.py +117 -0
  384. package/python/tests/test_task_stream.py +85 -0
  385. package/python/tests/test_thinking_sse.py +67 -0
  386. package/python/tests/test_thinking_stream.py +76 -0
  387. package/python/tests/test_tool_center.py +129 -0
  388. package/python/tests/test_tool_parallel.py +113 -0
  389. package/python/tests/test_tool_params.py +103 -0
  390. package/python/tests/test_tool_use.py +242 -0
  391. package/python/tests/test_toplevel_concurrency.py +117 -0
  392. package/python/tests/test_tracing.py +290 -0
  393. package/python/tests/test_truth_crystallizer.py +127 -0
  394. package/python/tests/test_truth_flow.py +93 -0
  395. package/python/tests/test_url_policy.py +53 -0
  396. package/python/tests/test_verify_evidence.py +225 -0
  397. package/python/tests/test_version_sync.py +71 -0
  398. package/python/tests/test_web_trigger.py +99 -0
  399. package/python/tools/check_faiss_type.py +31 -0
  400. package/python/tools/check_log.ps1 +46 -0
  401. package/python/tools/ivf_status.py +72 -0
  402. package/python/tools/mypy_baseline.txt +5 -0
  403. package/python/tools/mypy_ratchet.py +97 -0
  404. package/python/tools/pack.py +178 -0
  405. package/python/tools/release_check.py +349 -0
  406. package/python/tools/restart_foxclaw.ps1 +74 -0
  407. package/python/tools/shared_write_audit.py +204 -0
  408. package/python/tools/size_baseline.txt +28 -0
  409. package/python/tools/size_check.py +102 -0
  410. package/python/tools/verify_env.bat +2 -0
  411. package/python/tools/verify_env.py +25 -0
  412. package/python/vendor/HMR_LICENSE +37 -0
  413. package/python/vendor/HMR_README.md +65 -0
  414. package/python/vendor/hkc/LICENSE +37 -0
  415. package/python/vendor/hkc/hkc_ace/__init__.py +0 -0
  416. package/python/vendor/hkc/hkc_ace/ace.py +457 -0
  417. package/python/vendor/hkc/hkc_ace/skill_taxonomy.json +465 -0
  418. package/python/vendor/hkc/hkc_api/__init__.py +0 -0
  419. package/python/vendor/hkc/hkc_api/container.py +416 -0
  420. package/python/vendor/hkc/hkc_api/ingest_jobs.py +103 -0
  421. package/python/vendor/hkc/hkc_api/main.py +157 -0
  422. package/python/vendor/hkc/hkc_api/routes/__init__.py +0 -0
  423. package/python/vendor/hkc/hkc_api/routes/ability.py +151 -0
  424. package/python/vendor/hkc/hkc_api/routes/config.py +51 -0
  425. package/python/vendor/hkc/hkc_api/routes/knowledge.py +630 -0
  426. package/python/vendor/hkc/hkc_api/routes/search.py +88 -0
  427. package/python/vendor/hkc/hkc_api/schemas.py +153 -0
  428. package/python/vendor/hkc/hkc_api/synthesis.py +179 -0
  429. package/python/vendor/hkc/hkc_core/__init__.py +0 -0
  430. package/python/vendor/hkc/hkc_core/graph/__init__.py +0 -0
  431. package/python/vendor/hkc/hkc_core/graph/base.py +188 -0
  432. package/python/vendor/hkc/hkc_core/graph/sqlite_store.py +789 -0
  433. package/python/vendor/hkc/hkc_core/models/__init__.py +0 -0
  434. package/python/vendor/hkc/hkc_core/models/enums.py +65 -0
  435. package/python/vendor/hkc/hkc_core/models/ku.py +307 -0
  436. package/python/vendor/hkc/hkc_core/utils/__init__.py +0 -0
  437. package/python/vendor/hkc/hkc_core/utils/id_gen.py +113 -0
  438. package/python/vendor/hkc/hkc_crystallizer/__init__.py +44 -0
  439. package/python/vendor/hkc/hkc_crystallizer/adapters.py +94 -0
  440. package/python/vendor/hkc/hkc_crystallizer/candidate.py +145 -0
  441. package/python/vendor/hkc/hkc_crystallizer/crystallizer.py +179 -0
  442. package/python/vendor/hkc/hkc_crystallizer/ingress.py +90 -0
  443. package/python/vendor/hkc/hkc_kde/__init__.py +0 -0
  444. package/python/vendor/hkc/hkc_kde/chunker.py +197 -0
  445. package/python/vendor/hkc/hkc_kde/extractor.py +623 -0
  446. package/python/vendor/hkc/hkc_kde/kde.py +298 -0
  447. package/python/vendor/hkc/hkc_kde/loaders/__init__.py +0 -0
  448. package/python/vendor/hkc/hkc_kde/loaders/ebook_loader.py +153 -0
  449. package/python/vendor/hkc/hkc_kde/loaders/office_loader.py +194 -0
  450. package/python/vendor/hkc/hkc_kde/loaders/pdf_loader.py +162 -0
  451. package/python/vendor/hkc/hkc_kde/loaders/text_loader.py +157 -0
  452. package/python/vendor/hkc/hkc_kde/loaders/web_loader.py +91 -0
  453. package/python/vendor/hkc/hkc_kde/models.py +74 -0
  454. package/python/vendor/hkc/hkc_kde/packager.py +329 -0
  455. package/python/vendor/hkc/hkc_kde/relation_disc.py +151 -0
  456. package/python/vendor/hkc/hkc_kee/__init__.py +0 -0
  457. package/python/vendor/hkc/hkc_kee/dedup.py +227 -0
  458. package/python/vendor/hkc/hkc_kee/fingerprint.py +106 -0
  459. package/python/vendor/hkc/hkc_kee/kee.py +541 -0
  460. package/python/vendor/hkc/hkc_kep/__init__.py +0 -0
  461. package/python/vendor/hkc/hkc_kep/event_bus.py +205 -0
  462. package/python/vendor/hkc/hkc_sdk/__init__.py +63 -0
  463. package/python/vendor/hkc/hkc_sdk/base.py +151 -0
  464. package/python/vendor/hkc/hkc_sdk/direct_client.py +153 -0
  465. package/python/vendor/hkc/hkc_sdk/exceptions.py +39 -0
  466. package/python/vendor/hkc/hkc_sdk/http_client.py +166 -0
  467. package/python/vendor/hkc/hkc_sdk/models.py +171 -0
  468. package/python/vendor/hkc/hkc_search/__init__.py +0 -0
  469. package/python/vendor/hkc/hkc_search/bm25.py +325 -0
  470. package/python/vendor/hkc/hkc_search/embedding_backends.py +374 -0
  471. package/python/vendor/hkc/hkc_search/graph_search.py +132 -0
  472. package/python/vendor/hkc/hkc_search/hybrid.py +331 -0
  473. package/python/vendor/hkc/hkc_search/index_build_manager.py +378 -0
  474. package/python/vendor/hkc/hkc_search/vector_search.py +484 -0
  475. package/python/vendor/hmr/__init__.py +0 -0
  476. package/python/vendor/hmr/core/__init__.py +0 -0
  477. package/python/vendor/hmr/core/hmr.py +684 -0
  478. package/python/vendor/hmr/core/models.py +310 -0
  479. package/python/vendor/hmr/engines/__init__.py +0 -0
  480. package/python/vendor/hmr/engines/evolution.py +710 -0
  481. package/python/vendor/hmr/engines/jit_compiler.py +472 -0
  482. package/python/vendor/hmr/engines/lifecycle.py +264 -0
  483. package/python/vendor/hmr/engines/policy.py +625 -0
  484. package/python/vendor/hmr/engines/recall.py +260 -0
  485. package/python/vendor/hmr/engines/runtime_state.py +158 -0
  486. package/python/vendor/hmr/engines/scheduler.py +323 -0
  487. package/python/vendor/hmr/engines/semantic.py +150 -0
  488. package/python/vendor/hmr/engines/temporal.py +261 -0
  489. package/python/vendor/hmr/engines/thought_chain.py +635 -0
  490. package/python/vendor/hmr/graph/__init__.py +0 -0
  491. package/python/vendor/hmr/graph/cwg.py +338 -0
  492. package/python/vendor/hmr/graph/memory_graph.py +530 -0
  493. package/python/vendor/hmr/storage/__init__.py +0 -0
  494. package/python/vendor/hmr/storage/memory_fs.py +300 -0
  495. package/python/vendor/hmr/storage/vector_store.py +520 -0
@@ -0,0 +1,414 @@
1
+ """批次 6 · P2 层 + P3-2 测试
2
+
3
+ [1] P2-2:compose 遇 needs_confirmation → 暂停不回滚(pending),blocked 仍回滚(对照)
4
+ [2] P2-2:断点续跑 resume_state(第一步不重跑)
5
+ [3] P2-2:AHSPipeline success 判定区分 pending + is_pending 属性
6
+ [4] P2-1:run_async 返回结果 + 不阻塞事件循环(并发两个慢 handler < 2x 串行)+ 同步 run 兼容
7
+ [5] P2-3:idempotency 追加日志(3 次 mark = 3 行)+ compact 合并快照 + 重启回放
8
+ [6] P2-3:旧快照格式兼容 + 容量淘汰发 delete 日志(重启不复活)
9
+ [7] P2-3:持久化失败发 idempotency.persist_failed 事件 + stats 计数
10
+ [8] P2-4:plan_recovery 分类(retryable/notify_user/needs_manual)+ recovery.plan 事件
11
+ [9] P2-4:start_count 递增 + compact_log 用 max(started,finished) 不丢刚完成的 + fsync=False 可用
12
+ [10] P3-2:/api/ahs/run 走 run_async,暴露 is_pending/pending_confirmation
13
+ """
14
+ import asyncio
15
+ import builtins
16
+ import json
17
+ import os
18
+ import sys
19
+ import tempfile
20
+ import time
21
+ from pathlib import Path
22
+
23
+ sys.path.insert(0, str(Path(__file__).parent.parent))
24
+
25
+ FAILS = []
26
+
27
+
28
+ def check(cond, label):
29
+ if cond:
30
+ print(f" ✓ {label}")
31
+ else:
32
+ FAILS.append(label)
33
+ print(f" ✗ {label}")
34
+
35
+
36
+ # ═══════════════ [1]-[3] P2-2 compose pending ═══════════════
37
+ from foxclaw.skills import SkillRegistry, SkillSpec, SkillAdapter
38
+ from foxclaw.skills.pipeline import compose, plan, AHSPipeline
39
+
40
+
41
+ def _make_pending_registry(counter: dict):
42
+ """两个 skill:第一个必成功,第二个 destructive → 未确认时 needs_confirmation。
43
+
44
+ 注意本仓权限模型(permission.py Stage4):strict 模式下 side_effect ∈
45
+ {write, send, pay} 一律 ask。所以第一步用 read(放行),destructive 的
46
+ send_email 留给第二步制造 pending——文稿骨架假设 write 可直接成功,
47
+ 与本仓 Stage4 不符,按真实模型修正。"""
48
+ reg = SkillRegistry()
49
+
50
+ def _create(title=""):
51
+ counter["create"] = counter.get("create", 0) + 1
52
+ return {"created": title}
53
+
54
+ reg.register(SkillSpec(
55
+ id="create_event", name="创建日程", description="创建日程",
56
+ kind="local", triggers=["创建", "日程"], side_effect="read",
57
+ handler=_create, params={"title": "str"}))
58
+ reg.register(SkillSpec(
59
+ id="send_email", name="发邮件", description="发邮件",
60
+ kind="local", triggers=["发送", "邮件"], side_effect="send",
61
+ destructive=True, # ← 未确认 → needs_confirmation
62
+ handler=lambda to="": {"sent": to}, params={"to": "str"}))
63
+ return reg
64
+
65
+
66
+ def test_compose_pending():
67
+ print("\n【1】P2-2:needs_confirmation → 暂停(pending),不回滚")
68
+ reg = _make_pending_registry({})
69
+ adp = SkillAdapter(reg)
70
+ p = plan("创建日程 然后 发送邮件", reg)
71
+ check(len(p.tasks) == 2 and p.tasks[0].suggested_skill == "create_event"
72
+ and p.tasks[1].suggested_skill == "send_email",
73
+ f"前置:计划两步且路由正确(实为 {[(t.id, t.suggested_skill) for t in p.tasks]})")
74
+ res = compose(p, reg, adp, confirmed=False)
75
+ check(res.is_pending, "待确认 → is_pending")
76
+ check(res.pending_confirmation["skill_id"] == "send_email",
77
+ "pending 定位到 send_email")
78
+ check(not res.rolled_back and res.rollback_log == [],
79
+ "pending 不触发回滚(rolled_back=False, rollback_log 空)")
80
+ check(not res.is_failed and res.failed == [],
81
+ "pending 不算失败(failed 空)")
82
+ check(len(res.completed_steps) == 1
83
+ and res.completed_steps[0][1] == "create_event",
84
+ "第一步(create_event)已完成并存入 completed_steps")
85
+ check(res.pending_confirmation["resume_at"] == "T2"
86
+ and len(res.pending_confirmation["completed_steps"]) == 1,
87
+ "pending_confirmation 带断点现场(resume_at=T2 + completed_steps)")
88
+ check(res.final_output.get("pending") is True
89
+ and res.final_output.get("completed") == ["T1"],
90
+ "final_output 标记 pending + 已完成列表")
91
+
92
+
93
+ def test_compose_failed_still_rolls_back():
94
+ print("\n【1b】对照:P2-2 后真失败(not_connected)仍触发回滚")
95
+ # confirmed=True:write/send 都过权限;第二步 handler=None → dispatch not_connected
96
+ # → 真失败 → 回滚第一步(write 副作用,有 compensate)。若误走 pending 则不回滚。
97
+ reg = SkillRegistry()
98
+ rolled = []
99
+ reg.register(SkillSpec(
100
+ id="create_event", name="创建日程", description="创建日程",
101
+ kind="local", triggers=["创建", "日程"], side_effect="write",
102
+ handler=lambda title="": {"created": title}, params={"title": "str"},
103
+ compensate=lambda out: rolled.append(out)))
104
+ reg.register(SkillSpec(
105
+ id="send_email", name="发邮件", description="发邮件",
106
+ kind="local", triggers=["发送", "邮件"], side_effect="send",
107
+ handler=None, # ← 无 handler → not_connected(非 pending)
108
+ params={"to": "str"}))
109
+ adp = SkillAdapter(reg)
110
+ res = compose(plan("创建日程 然后 发送邮件", reg), reg, adp, confirmed=True)
111
+ check(res.is_failed and not res.is_pending,
112
+ "not_connected → failed 而非 pending")
113
+ check(res.rolled_back and len(rolled) == 1,
114
+ "关键路径失败 → 逆序回滚已执行的副作用步骤(补偿被调 1 次)")
115
+
116
+
117
+ def test_compose_resume():
118
+ print("\n【2】P2-2:断点续跑 resume_state(第一步不重跑)")
119
+ counter = {}
120
+ reg = _make_pending_registry(counter)
121
+ adp = SkillAdapter(reg)
122
+ p = plan("创建日程 然后 发送邮件", reg)
123
+ res1 = compose(p, reg, adp, confirmed=False)
124
+ check(res1.is_pending, "前置:第一次 compose 停在 pending")
125
+ res2 = compose(p, reg, adp, confirmed=True,
126
+ resume_state=res1.pending_confirmation)
127
+ check(not res2.is_pending and not res2.is_failed,
128
+ "确认后续跑:不再 pending、不失败")
129
+ check(counter.get("create") == 1,
130
+ f"第一步不重跑(create 调用数=1,实为 {counter.get('create')})")
131
+ check(len(res2.completed_steps) == 2,
132
+ "续跑后 completed_steps=2(恢复的 1 + 新执行的 1)")
133
+ check(res2.final_output.get("sent") is not None
134
+ or "send_email" in res2.final_output,
135
+ f"续跑真正执行了 send_email(final_output={res2.final_output})")
136
+
137
+
138
+ def test_ahs_pipeline_pending():
139
+ print("\n【3】P2-2:AHSPipeline success 判定区分 pending")
140
+ reg = _make_pending_registry({})
141
+ pipe = AHSPipeline(reg, SkillAdapter(reg))
142
+ result = pipe.run("创建日程 然后 发送邮件", confirmed=False)
143
+ check(result.success is False, "pending 时 success=False(外向结果与旧一致)")
144
+ check(result.is_pending and result.pending_confirmation is not None,
145
+ "PipelineResult.is_pending / pending_confirmation 可用")
146
+ check("待确认" in result.summary, f"summary 指出待确认(实为 {result.summary})")
147
+ comp = result.composition
148
+ check(comp["pending_confirmation"]["skill_id"] == "send_email"
149
+ and comp["completed_steps"] == ["T1"]
150
+ and comp["rolled_back"] is False,
151
+ "composition 字典带 pending_confirmation/completed_steps,未回滚")
152
+
153
+
154
+ # ═══════════════ [4] P2-1 run_async ═══════════════
155
+ def test_run_async():
156
+ print("\n【4】P2-1:run_async 异步入口")
157
+ reg = SkillRegistry()
158
+ reg.register(SkillSpec(
159
+ id="calc", name="计算器", description="计算",
160
+ kind="local", triggers=["计算"], side_effect="read",
161
+ handler=lambda expression="": {"result": eval(expression)},
162
+ params={"expression": "str"}))
163
+
164
+ def _slow(expression=""):
165
+ time.sleep(0.5)
166
+ return {"result": eval(expression)}
167
+ reg.register(SkillSpec(
168
+ id="slow", name="慢计算", description="慢计算",
169
+ kind="local", triggers=["慢"], side_effect="read",
170
+ handler=_slow, params={"expression": "str"}))
171
+ pipe = AHSPipeline(reg, SkillAdapter(reg))
172
+
173
+ async def _main():
174
+ # a) 返回 PipelineResult
175
+ r = await pipe.run_async("计算 1+1")
176
+ check(r.task == "计算 1+1" and (r.success or r.is_pending),
177
+ "run_async 返回 PipelineResult")
178
+ # b) 不阻塞事件循环:两个 0.5s 慢任务并发,若串行 ≥1.0s,并发 ≈0.5s
179
+ # (阈值 0.9s:串行/并发区分度大,且留足调度抖动余量——0.3/0.5 的窄距实测会偶抖)
180
+ t0 = time.perf_counter()
181
+ await asyncio.gather(pipe.run_async("慢 1+1"), pipe.run_async("慢 2+2"))
182
+ elapsed = time.perf_counter() - t0
183
+ check(elapsed < 0.9,
184
+ f"并发两个慢 run_async 耗时 {elapsed:.2f}s < 0.9s(未阻塞事件循环)")
185
+ asyncio.run(_main())
186
+ # c) 同步 run 向后兼容
187
+ r_sync = pipe.run("计算 1+1")
188
+ check(r_sync.task == "计算 1+1", "同步 run 仍可用(向后兼容)")
189
+
190
+
191
+ # ═══════════════ [5]-[7] P2-3 idempotency ═══════════════
192
+ from foxclaw.core.idempotency import IdempotencyGuard
193
+
194
+
195
+ class _Spine:
196
+ def __init__(self):
197
+ self.events = []
198
+
199
+ def publish(self, event, payload, source=""):
200
+ self.events.append((event, payload))
201
+
202
+
203
+ def test_idem_append_log():
204
+ print("\n【5】P2-3:追加日志(O(1) 写)+ compact 合并 + 重启回放")
205
+ d = tempfile.mkdtemp(prefix="idem_a_")
206
+ path = os.path.join(d, "idem.json")
207
+ g = IdempotencyGuard(persist_path=path, compact_every=1000) # 不触发 compact
208
+ g.mark("k1"); g.mark("k2"); g.mark("k3")
209
+ log_path = path + ".log"
210
+ check(os.path.exists(log_path), "mark 产生追加日志")
211
+ with open(log_path, encoding="utf-8") as f:
212
+ lines = [l for l in f if l.strip()]
213
+ check(len(lines) == 3, f"3 次 mark = 3 行日志(实为 {len(lines)}),非全量写")
214
+ check(not os.path.exists(path), "未达阈值不写全量快照")
215
+
216
+ # compact 合并:compact_every=5,第 5 次 mark 触发(轮转日志→快照;新 append 开新文件)
217
+ d2 = tempfile.mkdtemp(prefix="idem_b_")
218
+ path2 = os.path.join(d2, "idem.json")
219
+ g2 = IdempotencyGuard(persist_path=path2, compact_every=5)
220
+ for i in range(5):
221
+ g2.mark(f"k{i}")
222
+ check(not os.path.exists(path2 + ".log")
223
+ and not os.path.exists(path2 + ".log.rot"),
224
+ "compact 后追加日志清空(轮转本已删,新日志未生)")
225
+ with open(path2, encoding="utf-8") as f:
226
+ data = json.load(f)
227
+ check(len(data) == 5, f"快照含全部 5 条(实为 {len(data)})")
228
+ # compact 之后的新 mark 进新日志;重启 = 读快照(5) + 回放新日志(1) = 6
229
+ g2.mark("k5")
230
+ g2b = IdempotencyGuard(persist_path=path2, compact_every=10000)
231
+ check(all(g2b.seen(f"k{i}") for i in range(6)),
232
+ "compact 后继续 mark:重启回放 = 快照5 + 新日志1 = 6 全可见")
233
+
234
+ # 重启回放:g3 不写快照(阈值大),g4 启动 = 读快照(无)+ 回放日志
235
+ d3 = tempfile.mkdtemp(prefix="idem_c_")
236
+ path3 = os.path.join(d3, "idem.json")
237
+ g3 = IdempotencyGuard(persist_path=path3, compact_every=10000)
238
+ g3.mark("k1"); g3.mark("k2"); g3.update_result("k1", {"r": 1})
239
+ g4 = IdempotencyGuard(persist_path=path3)
240
+ check(g4.seen("k1") and g4.seen("k2"), "重启后日志回放:k1/k2 仍可见")
241
+ check(g4.get_result("k1") == {"r": 1}, "update 事件回放:结果已回填")
242
+
243
+
244
+ def test_idem_compat_and_eviction():
245
+ print("\n【6】P2-3:旧快照兼容 + 容量淘汰发 delete(重启不复活)")
246
+ # 旧格式:纯全量快照,无 .log → 直接可读
247
+ d = tempfile.mkdtemp(prefix="idem_d_")
248
+ path = os.path.join(d, "idem.json")
249
+ with open(path, "w", encoding="utf-8") as f:
250
+ json.dump({"legacy_k": {"created": time.time(), "result": {"x": 1}}}, f)
251
+ g = IdempotencyGuard(persist_path=path)
252
+ check(g.seen("legacy_k") and g.get_result("legacy_k") == {"x": 1},
253
+ "旧版纯快照格式兼容(无日志,回放为空)")
254
+
255
+ # 容量淘汰必须落 delete:否则重启回放 mark 日志会"复活"被淘汰的 key
256
+ d2 = tempfile.mkdtemp(prefix="idem_e_")
257
+ path2 = os.path.join(d2, "idem.json")
258
+ g2 = IdempotencyGuard(persist_path=path2, max_size=2, compact_every=10000)
259
+ g2.mark("k1"); time.sleep(0.01); g2.mark("k2"); time.sleep(0.01); g2.mark("k3")
260
+ check(not g2.seen("k1"), "容量=2:k1 被淘汰出内存")
261
+ g3 = IdempotencyGuard(persist_path=path2, max_size=2, compact_every=10000)
262
+ check(not g3.seen("k1") and g3.seen("k2") and g3.seen("k3"),
263
+ "重启回放后 k1 不复活(delete 日志生效),k2/k3 在")
264
+
265
+
266
+ def test_idem_persist_failure():
267
+ print("\n【7】P2-3:持久化失败发事件 + stats 计数(不静默)")
268
+ spine = _Spine()
269
+ d = tempfile.mkdtemp(prefix="idem_f_")
270
+ path = os.path.join(d, "idem.json")
271
+ g = IdempotencyGuard(persist_path=path, spine=spine)
272
+ orig_open = builtins.open
273
+
274
+ def _bad_open(*a, **kw):
275
+ if ".log" in str(a[0]):
276
+ raise OSError("disk full")
277
+ return orig_open(*a, **kw)
278
+ builtins.open = _bad_open
279
+ try:
280
+ g.mark("k1") # 追加日志失败 → 计数 + 事件;主流程不炸
281
+ finally:
282
+ builtins.open = orig_open
283
+ check(g.seen("k1"), "持久化失败不影响内存主流程")
284
+ check(g.stats()["persist_failures"] >= 1, "stats() 暴露 persist_failures 计数")
285
+ check(any(e[0] == "idempotency.persist_failed" for e in spine.events),
286
+ "发 idempotency.persist_failed 事件")
287
+ s = IdempotencyGuard().stats()
288
+ check("persist_failures" in s and "ops_since_compact" in s,
289
+ "stats() 含新字段(无 persist_path 也一样)")
290
+
291
+
292
+ # ═══════════════ [8]-[9] P2-4 recovery ═══════════════
293
+ from foxclaw.core.recovery import RestartRecovery, RecoveryPlan
294
+
295
+
296
+ def test_recovery_plan():
297
+ print("\n【8】P2-4:plan_recovery 分类 + recovery.plan 事件")
298
+ spine = _Spine()
299
+ d = tempfile.mkdtemp(prefix="recv_a_")
300
+ log = os.path.join(d, "log.jsonl")
301
+ r1 = RestartRecovery(log, boot_id="boot1")
302
+ r1.task_started("t1", "可重试任务", retryable=True)
303
+ r1.task_started("t2", "渠道任务", channel="telegram")
304
+ r1.task_started("t3", "无标记任务")
305
+ r2 = RestartRecovery(log, boot_id="boot2", spine=spine)
306
+ plan_obj = r2.plan_recovery()
307
+ check(isinstance(plan_obj, RecoveryPlan), "返回 RecoveryPlan")
308
+ check("t1" in plan_obj.retryable, "retryable=True → retryable")
309
+ check("t2" in plan_obj.notify_user, "channel 非空 → notify_user")
310
+ check("t3" in plan_obj.needs_manual, "无标记 → needs_manual(保守)")
311
+ check(len(plan_obj.details) == 3
312
+ and {d["action"] for d in plan_obj.details} == {"retry", "notify_user", "manual"},
313
+ "details 逐任务带 action")
314
+ evts = [e for e in spine.events if e[0] == "recovery.plan"]
315
+ check(len(evts) == 3, f"每个分类决定发 recovery.plan 事件(实为 {len(evts)})")
316
+ # scan_interrupted 返回类型不变(兼容)
317
+ check(isinstance(r2.scan_interrupted(), list), "scan_interrupted 仍返回 list(兼容)")
318
+
319
+
320
+ def test_recovery_details():
321
+ print("\n【9】P2-4:start_count / compact max(started,finished) / fsync 开关")
322
+ d = tempfile.mkdtemp(prefix="recv_b_")
323
+ log = os.path.join(d, "log.jsonl")
324
+ r = RestartRecovery(log, boot_id="boot1")
325
+ r.task_started("t1", "first")
326
+ r.task_finished("t1", status="done")
327
+ r.task_started("t1", "second")
328
+ records = r._replay()
329
+ check(records["t1"].start_count == 2, "同一 task_id 两次 start → start_count=2")
330
+ check(records["t1"].status == "in_progress" and records["t1"].description == "second",
331
+ "重复 start 保留最后一次")
332
+
333
+ # compact_log:started 很早、finished 刚刚 → 旧逻辑(只看 started)会丢,新逻辑保留
334
+ d2 = tempfile.mkdtemp(prefix="recv_c_")
335
+ log2 = os.path.join(d2, "log.jsonl")
336
+ old = time.time() - 30 * 86400
337
+ with open(log2, "w", encoding="utf-8") as f:
338
+ f.write(json.dumps({"event": "start", "task_id": "t_old", "description": "x",
339
+ "pid": 1, "started": old, "boot_id": "b0"}) + "\n")
340
+ f.write(json.dumps({"event": "finish", "task_id": "t_old", "status": "done",
341
+ "pid": 1, "finished": time.time()}) + "\n")
342
+ r2 = RestartRecovery(log2, boot_id="boot1")
343
+ r2.compact_log(keep_recent_days=7)
344
+ check("t_old" in r2._replay(),
345
+ "compact 用 max(started,finished):started 30 天前但刚完成的记录不丢")
346
+ check(r2._replay()["t_old"].finished > 0, "compact 保留 finished 时间戳")
347
+
348
+ # fsync=False 可用(高频场景开关)
349
+ d3 = tempfile.mkdtemp(prefix="recv_d_")
350
+ log3 = os.path.join(d3, "log.jsonl")
351
+ r3 = RestartRecovery(log3, boot_id="boot1", fsync=False)
352
+ r3.task_started("t1", "x")
353
+ r3.task_finished("t1")
354
+ check(r3._replay()["t1"].status == "done", "fsync=False 正常读写(开关兼容)")
355
+ # 空中断 → 空计划
356
+ r4 = RestartRecovery(os.path.join(d3, "empty.jsonl"), boot_id="b")
357
+ p4 = r4.plan_recovery([])
358
+ check(p4.details == [] and p4.retryable == [], "无中断 → 空计划(不炸)")
359
+
360
+
361
+ # ═══════════════ [10] P3-2 /api/ahs/run ═══════════════
362
+ def test_api_ahs_run():
363
+ print("\n【10】P3-2:/api/ahs/run 走 run_async + 暴露 pending 字段")
364
+ os.environ["FOXCLAW_SHADOW_ROUNDS"] = "6"
365
+ os.environ.setdefault("FOXCLAW_EMBEDDING_MODEL", "foxclaw-nonexistent-fast")
366
+ os.environ["FOXCLAW_DATA_DIR"] = tempfile.mkdtemp(prefix="foxclaw_ahsrun_")
367
+ os.environ["FOXCLAW_SKIP_DOTENV"] = "1"
368
+ for _k in ("DEEPSEEK_API_KEY", "ANTHROPIC_API_KEY", "OPENAI_API_KEY",
369
+ "GOOGLE_API_KEY", "DASHSCOPE_API_KEY", "FOXCLAW_API_KEY"):
370
+ os.environ[_k] = ""
371
+ from fastapi.testclient import TestClient
372
+ from foxclaw.api import app
373
+ with TestClient(app) as c:
374
+ r = c.post("/api/ahs/run", json={"task": "计算 12*12"})
375
+ check(r.status_code == 200, f"端点 200(实为 {r.status_code})")
376
+ body = r.json()
377
+ check("is_pending" in body and "pending_confirmation" in body,
378
+ "响应暴露 is_pending/pending_confirmation 字段")
379
+ check(body.get("success") in (True, False), "success 字段正常返回")
380
+ # resume_state 参数被接受(闭环 P2-2 续跑;空断点现场 → 正常全跑)
381
+ r2 = c.post("/api/ahs/run", json={
382
+ "task": "计算 3*3", "confirmed": True,
383
+ "resume_state": {"completed_steps": [], "resume_at": None}})
384
+ check(r2.status_code == 200 and "summary" in r2.json(),
385
+ f"resume_state 参数兼容(实为 {r2.status_code}: {r2.text[:120]})")
386
+
387
+
388
+ def main():
389
+ print("=" * 60)
390
+ print("批次 6 · P2 层(idempotency/recovery/compose/run_async)+ P3-2 测试")
391
+ print("=" * 60)
392
+ test_compose_pending()
393
+ test_compose_failed_still_rolls_back()
394
+ test_compose_resume()
395
+ test_ahs_pipeline_pending()
396
+ test_run_async()
397
+ test_idem_append_log()
398
+ test_idem_compat_and_eviction()
399
+ test_idem_persist_failure()
400
+ test_recovery_plan()
401
+ test_recovery_details()
402
+ test_api_ahs_run()
403
+ print("\n" + "=" * 60)
404
+ if FAILS:
405
+ print(f"✗ {len(FAILS)} 项断言失败:")
406
+ for f in FAILS:
407
+ print(f" - {f}")
408
+ sys.exit(1)
409
+ print("✓ 批次 6 全部断言通过")
410
+ print("=" * 60)
411
+
412
+
413
+ if __name__ == "__main__":
414
+ main()
@@ -0,0 +1,206 @@
1
+ """
2
+ P0-2 anthropic tool_use 原生接入测试 —— 全离线(fake anthropic client,不打真实网络)。
3
+
4
+ 覆盖:
5
+ 1. _to_anthropic_tools:openai tools 格式 → anthropic 格式
6
+ 2. _to_anthropic_tool_choice:auto/required/指定工具
7
+ 3. supports_tools:anthropic 纳入支持(有 client 时)
8
+ 4. call_tools anthropic 分支:tool_use block 解析(结构化 input 无需 JSON 解析)
9
+ 5. call_tools anthropic:纯文本回答(text block)
10
+ 6. tool_choice=none → 不传 tools,文本收敛
11
+ 7. format_tool_result anthropic:tool_result block
12
+ 8. fail-safe:fake client 抛异常 → supported=False;无 client(stub)→ supported=False
13
+ 9. 回灌闭环:_reply_with_tools 用 anthropic 格式 assistant_msg/tool_result
14
+ """
15
+ import os, sys, types
16
+ from pathlib import Path
17
+ os.environ.setdefault("HF_HUB_OFFLINE", "1")
18
+ os.environ.setdefault("TRANSFORMERS_OFFLINE", "1")
19
+ os.environ["FOXCLAW_EMBEDDING_MODEL"] = "foxclaw-nonexistent-fast"
20
+ sys.path.insert(0, str(Path(__file__).parent.parent))
21
+
22
+ from foxclaw.core.model_router import ModelRouter
23
+
24
+
25
+ def _block(**kw):
26
+ return types.SimpleNamespace(**kw)
27
+
28
+
29
+ def _fake_client(script):
30
+ """fake anthropic client:client.messages.create(**kw) → 按脚本返回 content blocks。"""
31
+ c = types.SimpleNamespace()
32
+ c.calls = []
33
+ c.script = list(script)
34
+
35
+ def create(**kwargs):
36
+ c.calls.append(kwargs)
37
+ return types.SimpleNamespace(content=list(c.script.pop(0)))
38
+
39
+ c.messages = types.SimpleNamespace(create=create)
40
+ return c
41
+
42
+
43
+ def _router(client):
44
+ """用 __new__ 构造 anthropic ModelRouter(绕过 __init__ 的 SDK/网络探测)。"""
45
+ r = ModelRouter.__new__(ModelRouter)
46
+ r.provider = "anthropic"
47
+ r.model = "claude-sonnet-4-20250514"
48
+ r._locked_model = ""
49
+ r._client = client
50
+ r._cooldown = {}
51
+ r.meter = None
52
+ r.fallback_reason = ""
53
+ r.degraded = False
54
+ r.degraded_reason = ""
55
+ r._call_degraded = False
56
+ return r
57
+
58
+
59
+ def _plan_tool():
60
+ return {"type": "function",
61
+ "function": {"name": "submit_plan", "description": "d",
62
+ "parameters": {"type": "object",
63
+ "properties": {"flow": {"type": "string"}},
64
+ "required": ["flow"]}}}
65
+
66
+
67
+ # ── 1. 格式转换 ───────────────────────────────────────────────────────────────
68
+ def test_to_anthropic_tools():
69
+ out = ModelRouter._to_anthropic_tools([
70
+ _plan_tool(),
71
+ {"name": "direct", "description": "", "input_schema": {}}, # 已是 anthropic 格式
72
+ ])
73
+ assert len(out) == 2
74
+ assert out[0]["name"] == "submit_plan"
75
+ assert out[0]["input_schema"]["required"] == ["flow"]
76
+ assert out[1]["name"] == "direct"
77
+ # 非法项(空名/非 dict)跳过
78
+ assert ModelRouter._to_anthropic_tools([{"type": "function", "function": {"name": ""}}]) == []
79
+ assert ModelRouter._to_anthropic_tools(None) == []
80
+
81
+
82
+ def test_to_anthropic_tool_choice():
83
+ assert ModelRouter._to_anthropic_tool_choice("auto") == {"type": "auto"}
84
+ assert ModelRouter._to_anthropic_tool_choice(None) == {"type": "auto"}
85
+ assert ModelRouter._to_anthropic_tool_choice("none") == {"type": "none"}
86
+ assert ModelRouter._to_anthropic_tool_choice("required") == {"type": "any"}
87
+ assert ModelRouter._to_anthropic_tool_choice(
88
+ {"type": "function", "function": {"name": "x"}}) == {"type": "tool", "name": "x"}
89
+
90
+
91
+ # ── 2. supports_tools ──────────────────────────────────────────────────────────
92
+ def test_supports_tools_anthropic():
93
+ r = _router(_fake_client([[]]))
94
+ assert r.is_stub is False
95
+ assert r.supports_tools() is True
96
+ # 无 client(stub)→ 不支持
97
+ r2 = ModelRouter.__new__(ModelRouter)
98
+ r2.provider = "anthropic"; r2._client = None
99
+ assert r2.supports_tools() is False
100
+
101
+
102
+ # ── 3. call_tools anthropic 分支 ──────────────────────────────────────────────
103
+ def test_call_tools_anthropic_tool_use():
104
+ client = _fake_client([[
105
+ _block(type="tool_use", id="toolu_1", name="submit_plan",
106
+ input={"flow": "sequential",
107
+ "steps": [{"name": "查支出", "agent": "finance"}]}),
108
+ ]])
109
+ r = _router(client)
110
+ res = r.call_tools("sys", [{"role": "user", "content": "t"}], [_plan_tool()])
111
+ assert res["supported"] is True
112
+ assert res["tool_calls"] == [{"id": "toolu_1", "name": "submit_plan",
113
+ "args": {"flow": "sequential",
114
+ "steps": [{"name": "查支出", "agent": "finance"}]},
115
+ "args_parse_failed": False}]
116
+ # 入参:tools 转 anthropic 格式 + tool_choice 转 auto + system 单独传 + 运行时注入
117
+ kw = client.calls[0]
118
+ assert kw["system"].startswith("【运行时信息") and "sys" in kw["system"]
119
+ assert kw["tools"][0]["name"] == "submit_plan"
120
+ assert kw["tool_choice"] == {"type": "auto"}
121
+ assert all(m["role"] != "system" for m in kw["messages"]), "anthropic 不允许 system 角色入 messages"
122
+ # assistant_msg 是 anthropic 原生 blocks
123
+ assert res["assistant_msg"]["role"] == "assistant"
124
+ assert res["assistant_msg"]["content"][0]["type"] == "tool_use"
125
+
126
+
127
+ def test_call_tools_anthropic_text_only():
128
+ client = _fake_client([[_block(type="text", text="你好,无需工具。")]])
129
+ r = _router(client)
130
+ res = r.call_tools("sys", [{"role": "user", "content": "hi"}], [])
131
+ assert res["text"] == "你好,无需工具。"
132
+ assert res["tool_calls"] == []
133
+ assert res["assistant_msg"]["content"] == [{"type": "text", "text": "你好,无需工具。"}]
134
+
135
+
136
+ def test_call_tools_anthropic_tool_choice_none():
137
+ client = _fake_client([[_block(type="text", text="收敛答案")]])
138
+ r = _router(client)
139
+ res = r.call_tools("sys", [{"role": "user", "content": "hi"}], [_plan_tool()],
140
+ tool_choice="none")
141
+ assert res["text"] == "收敛答案"
142
+ kw = client.calls[0]
143
+ assert kw["tool_choice"] == {"type": "none"}, "none 映射为 {type:none}"
144
+ assert kw["tools"][0]["name"] == "submit_plan", "历史含 tool_use 时必须仍传 tools"
145
+
146
+
147
+ # ── 4. format_tool_result ─────────────────────────────────────────────────────
148
+ def test_format_tool_result_anthropic():
149
+ r = _router(_fake_client([[]]))
150
+ out = r.format_tool_result({"id": "toolu_9", "name": "web_search"}, "结果")
151
+ assert out == {"role": "user", "content": [
152
+ {"type": "tool_result", "tool_use_id": "toolu_9", "content": "结果"}]}
153
+
154
+
155
+ # ── 5. fail-safe ──────────────────────────────────────────────────────────────
156
+ def test_call_tools_anthropic_failsafe():
157
+ def create(**kw):
158
+ raise RuntimeError("boom")
159
+ client = types.SimpleNamespace()
160
+ client.messages = types.SimpleNamespace(create=create)
161
+ r = _router(client)
162
+ res = r.call_tools("sys", [{"role": "user", "content": "t"}], [])
163
+ assert res["supported"] is False and res.get("error")
164
+
165
+ # stub(无 client)→ supported=False
166
+ r2 = _router(None)
167
+ res2 = r2.call_tools("sys", [{"role": "user", "content": "t"}], [])
168
+ assert res2["supported"] is False
169
+
170
+
171
+ # ── 6. 回灌闭环(_reply_with_tools 在 anthropic 下)────────────────────────────
172
+ def test_reply_with_tools_anthropic_loop():
173
+ from foxclaw.agents.base import CognitiveAgent
174
+ client = _fake_client([
175
+ # 第1轮:模型要求调 recall_memory
176
+ [_block(type="tool_use", id="c1", name="recall_memory", input={"query": "偏好"})],
177
+ # 第2轮:拿到结果后给最终答案
178
+ [_block(type="text", text="根据记忆,你喜欢灰色。")],
179
+ ])
180
+ r = _router(client)
181
+ agent = types.SimpleNamespace()
182
+ agent.router = r
183
+ agent.system_prompt = "sys"
184
+ agent.AGENT_ID = "assistant"
185
+ agent._dispatch_tool = lambda name, args: f"RESULT[{name}:{args.get('query', '')}]"
186
+
187
+ out = CognitiveAgent._reply_with_tools(
188
+ agent, "我喜欢什么颜色", [{"role": "user", "content": "我喜欢什么颜色"}])
189
+ assert out == "根据记忆,你喜欢灰色。"
190
+ # 第2轮的 messages 必须含 anthropic 格式 assistant(tool_use) + user(tool_result)
191
+ kw2 = client.calls[1]
192
+ has_tool_result = any(
193
+ isinstance(m.get("content"), list)
194
+ and any(b.get("type") == "tool_result" for b in m["content"])
195
+ for m in kw2["messages"])
196
+ assert has_tool_result, "回灌必须用 anthropic tool_result block"
197
+ has_tool_use = any(
198
+ isinstance(m.get("content"), list)
199
+ and any(b.get("type") == "tool_use" for b in m["content"])
200
+ for m in kw2["messages"])
201
+ assert has_tool_use, "回灌必须含 anthropic assistant tool_use block"
202
+
203
+
204
+ if __name__ == "__main__":
205
+ import pytest
206
+ pytest.main([__file__, "-v"])