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,80 @@
1
+ """
2
+ Foxclaw 行程子系统 — 数据模型
3
+ 行程行为操作系统的结构:
4
+ Waypoint(行程点:地点+时间+坐标)、RouteLeg(路线段)、
5
+ ItineraryVerdict(行程守门人裁决)
6
+ 核心思想:行程 = 时空中的可计算约束(地理位置 + 时间窗口 + 可达性)。
7
+ 坐标格式遵循高德:经度在前,纬度在后 [lng, lat]。
8
+ """
9
+
10
+ from dataclasses import dataclass, field, asdict
11
+ from datetime import datetime
12
+ from typing import Optional, Literal
13
+ import uuid
14
+
15
+
16
+ TravelMode = Literal["walking", "driving", "transit", "cycling"]
17
+
18
+
19
+ @dataclass
20
+ class Waypoint:
21
+ """一个行程点 = 时空中的一个约束"""
22
+ name: str
23
+ address: str = ""
24
+ lng: Optional[float] = None
25
+ lat: Optional[float] = None
26
+ start_time: Optional[str] = None
27
+ duration_min: int = 0
28
+ city: str = ""
29
+ note: str = ""
30
+ id: str = field(default_factory=lambda: f"wp_{uuid.uuid4().hex[:10]}")
31
+
32
+ @property
33
+ def has_coord(self) -> bool:
34
+ return self.lng is not None and self.lat is not None
35
+
36
+ @property
37
+ def coord_str(self) -> str:
38
+ """高德格式坐标字符串 '经度,纬度'"""
39
+ return f"{self.lng},{self.lat}" if self.has_coord else ""
40
+
41
+ @property
42
+ def dt(self) -> Optional[datetime]:
43
+ return datetime.fromisoformat(self.start_time) if self.start_time else None
44
+
45
+ def to_dict(self) -> dict:
46
+ d = asdict(self)
47
+ d.update(has_coord=self.has_coord, coord_str=self.coord_str)
48
+ return d
49
+
50
+
51
+ @dataclass
52
+ class RouteLeg:
53
+ """两点之间的路线段"""
54
+ from_id: str
55
+ to_id: str
56
+ from_name: str
57
+ to_name: str
58
+ mode: TravelMode = "driving"
59
+ distance_km: float = 0.0
60
+ duration_min: float = 0.0
61
+ source: Literal["estimated", "amap"] = "estimated"
62
+ note: str = ""
63
+
64
+ def to_dict(self) -> dict:
65
+ return asdict(self)
66
+
67
+
68
+ @dataclass
69
+ class ItineraryVerdict:
70
+ """行程守门人裁决(对应财务/学习守门人)"""
71
+ rule_id: str
72
+ action: Literal["time_conflict", "unreachable", "too_dense",
73
+ "cross_city", "suggest_reorder"]
74
+ message: str
75
+ severity: Literal["info", "warn", "critical"] = "warn"
76
+ waypoint_ids: list = field(default_factory=list)
77
+ payload: dict = field(default_factory=dict)
78
+
79
+ def to_dict(self) -> dict:
80
+ return asdict(self)
@@ -0,0 +1,96 @@
1
+ """
2
+ ItineraryConflictReasoner —— 行程冲突推理的认知化(v2 ③ Step4)。
3
+
4
+ 把 ItineraryReasoning.detect_conflicts 的规则裁决,形式化为
5
+ 观察 → 假设 → 证据 → 裁决,
6
+ **规则永远权威**(检测到时间冲突/通勤赶不及 → confirm,绝不被 LLM 推翻);
7
+ 可选 LLM 层(默认关)只对"无硬冲突、但排程紧凑"的微妙情形**升级** approve→confirm,
8
+ 结构上无从推翻规则、无从隐藏已检出的冲突。裁决带证据链(可审计)。
9
+
10
+ 顾问域(非治理闸口):无硬冲突时规则裁决=approve;LLM 出错→维持 approve(见基座
11
+ DomainReasoner.llm_should_escalate 的 fail-safe 语义说明)。
12
+ """
13
+ from __future__ import annotations
14
+ from ..cognitive.reasoner import DomainReasoner, ReasonedVerdict, Evidence
15
+ from .reasoning import ItineraryReasoning
16
+
17
+ TIGHT_RATIO = 1.5
18
+
19
+
20
+ class ItineraryConflictReasoner(DomainReasoner):
21
+ """subject = (waypoints, mode)。裁决:approve(行程可行) / confirm(有冲突或需注意)。"""
22
+
23
+ def __init__(self, mode: str = "driving", llm=None, llm_enabled: bool = False):
24
+ self.mode = mode
25
+ self.llm = llm
26
+ self.llm_enabled = llm_enabled
27
+
28
+ def observe(self, subject) -> dict:
29
+ waypoints, mode = subject
30
+ timed = [w for w in waypoints if getattr(w, "dt", None) is not None]
31
+ return {"count": len(waypoints), "timed": len(timed), "mode": mode or self.mode}
32
+
33
+ def hypothesize(self, obs: dict) -> list:
34
+ h = []
35
+ if obs["timed"] >= 2:
36
+ h += ["可能存在时间重叠", "可能通勤赶不及"]
37
+ if obs["count"] >= 2:
38
+ h.append("可能排程紧凑")
39
+ return h or ["行程点不足以产生冲突"]
40
+
41
+ def gather_evidence(self, subject, obs: dict) -> list:
42
+ waypoints, mode = subject
43
+ mode = obs["mode"]
44
+ conflicts = ItineraryReasoning.detect_conflicts(waypoints, mode)
45
+ ev = [Evidence(factor=c["type"], detail=c["reason"], weight=1.0, raw=c)
46
+ for c in conflicts]
47
+ if not conflicts:
48
+ ev.append(Evidence("no_conflict", "未检测到时间冲突或通勤不可达", 0.5))
49
+ try:
50
+ clusters = ItineraryReasoning.cluster_by_proximity(waypoints)
51
+ if clusters:
52
+ ev.append(Evidence("proximity_clusters",
53
+ f"{len(clusters)} 组可顺路合并:{clusters}", 0.3))
54
+ dens = ItineraryReasoning.density(waypoints, mode)
55
+ ev.append(Evidence("density",
56
+ f"{dens['waypoints']} 点/{dens['total_commute_min']:.0f} 分通勤"
57
+ + ("/跨城" if dens["cross_city"] else ""), 0.3))
58
+ except Exception:
59
+ pass
60
+ return ev
61
+
62
+ def arbitrate(self, subject, obs, hyps, ev) -> ReasonedVerdict:
63
+ triggered = [e for e in ev if e.raw is not None]
64
+ if triggered:
65
+ return ReasonedVerdict(
66
+ "confirm", ";".join(e.detail for e in triggered),
67
+ ev, hyps, "rules")
68
+ if DomainReasoner.llm_available(self.llm, self.llm_enabled) and self._is_tight(subject, obs):
69
+ if DomainReasoner.llm_should_escalate(self.llm, *self._llm_prompt(subject, obs, ev)):
70
+ return ReasonedVerdict("confirm", "排程紧凑,LLM 建议人工确认", ev, hyps, "llm")
71
+ return ReasonedVerdict("approve", "行程可行:无时间冲突、通勤可达", ev, hyps, "rules")
72
+
73
+ def _is_tight(self, subject, obs) -> bool:
74
+ """无硬冲突,但存在某段空档紧贴所需通勤(available < TIGHT_RATIO×need)。"""
75
+ waypoints, mode = subject
76
+ mode = obs["mode"]
77
+ timed = sorted([w for w in waypoints if getattr(w, "dt", None) is not None],
78
+ key=lambda w: w.dt)
79
+ from datetime import timedelta
80
+ for i in range(len(timed) - 1):
81
+ cur, nxt = timed[i], timed[i + 1]
82
+ if not (cur.has_coord and nxt.has_coord):
83
+ continue
84
+ cur_end = cur.dt + timedelta(minutes=cur.duration_min)
85
+ gap = (nxt.dt - cur_end).total_seconds() / 60
86
+ leg = ItineraryReasoning.estimate_leg(cur, nxt, mode)
87
+ if 0 < gap and leg.duration_min <= gap < leg.duration_min * TIGHT_RATIO:
88
+ return True
89
+ return False
90
+
91
+ def _llm_prompt(self, subject, obs, ev):
92
+ ev_text = "\n".join(f"- {e.factor}: {e.detail}" for e in ev) or "(无)"
93
+ system = ("你是行程助手。这份行程规则检查未发现硬冲突,但排程较紧。"
94
+ "判断是否仍建议用户人工确认。只回答 CONFIRM(建议确认)或 OK(无需)。")
95
+ user = f"行程:{obs['count']} 个点,方式 {obs['mode']}。\n证据:\n{ev_text}\n\n只回答一个词:CONFIRM 或 OK。"
96
+ return system, user
@@ -0,0 +1,168 @@
1
+ """
2
+ Foxclaw 行程子系统 — 推理引擎(行程 Agent 的大脑,纯算法)
3
+ 不依赖高德也能跑:路线优化(TSP)、可达性、时间冲突、地理聚类。
4
+ 接入高德后,把"直线估算"替换为"真实路线时间"(见 runtime)。
5
+ """
6
+
7
+ import math
8
+ from datetime import timedelta
9
+ from itertools import permutations
10
+ from typing import Any, cast
11
+
12
+ from .models import RouteLeg, TravelMode, Waypoint
13
+
14
+
15
+ MODE_SPEED = {"walking": 5, "cycling": 15, "transit": 25, "driving": 35}
16
+
17
+
18
+ class ItineraryReasoning:
19
+ """行程推理:把地点+时间变成可决策的行程智能。"""
20
+
21
+ @staticmethod
22
+ def haversine_km(a: Waypoint, b: Waypoint) -> float:
23
+ """两点直线距离(km)。Haversine 公式。"""
24
+ lat_a, lng_a = a.lat, a.lng
25
+ lat_b, lng_b = b.lat, b.lng
26
+ if lat_a is None or lng_a is None or lat_b is None or lng_b is None:
27
+ return 0.0
28
+ R = 6371.0
29
+ dlat = math.radians(lat_b - lat_a)
30
+ dlng = math.radians(lng_b - lng_a)
31
+ x = (math.sin(dlat/2)**2 +
32
+ math.cos(math.radians(lat_a)) * math.cos(math.radians(lat_b)) *
33
+ math.sin(dlng/2)**2)
34
+ return R * 2 * math.atan2(math.sqrt(x), math.sqrt(1-x))
35
+
36
+ @staticmethod
37
+ def estimate_leg(a: Waypoint, b: Waypoint, mode: str = "driving") -> RouteLeg:
38
+ """估算两点路线段(直线距离 × 1.3 绕路系数 / 速度)。"""
39
+ straight = ItineraryReasoning.haversine_km(a, b)
40
+ road_km = straight * 1.3
41
+ speed = MODE_SPEED.get(mode, 35)
42
+ duration = (road_km / speed) * 60 if speed else 0
43
+ return RouteLeg(
44
+ from_id=a.id, to_id=b.id, from_name=a.name, to_name=b.name,
45
+ mode=cast(TravelMode, mode if mode in MODE_SPEED else "driving"),
46
+ distance_km=round(road_km, 2),
47
+ duration_min=round(duration, 1), source="estimated",
48
+ )
49
+
50
+ @staticmethod
51
+ def optimize_order(waypoints: list, mode: str = "driving",
52
+ fixed_start: bool = True) -> dict:
53
+ """最优访问顺序。≤8 点用精确全排列,更多用最近邻启发式。
54
+ fixed_start=True 时第一个点固定为起点(比如"从公司出发")。"""
55
+ pts = [w for w in waypoints if w.has_coord]
56
+ if len(pts) < 2:
57
+ return {"order": [w.id for w in waypoints], "total_km": 0,
58
+ "total_min": 0, "method": "trivial"}
59
+
60
+ def total_dist(seq):
61
+ return sum(ItineraryReasoning.haversine_km(seq[i], seq[i+1])
62
+ for i in range(len(seq)-1))
63
+
64
+ if len(pts) <= 8:
65
+ start = pts[0] if fixed_start else None
66
+ rest = pts[1:] if fixed_start else pts
67
+ best: list = []
68
+ best_d = float("inf")
69
+ for perm in permutations(rest):
70
+ seq = ([start] + list(perm)) if start else list(perm)
71
+ d = total_dist(seq)
72
+ if d < best_d:
73
+ best_d, best = d, seq
74
+ method = "exact"
75
+ ordered = best
76
+ else:
77
+ remaining = pts[1:]
78
+ ordered = [pts[0]]
79
+ while remaining:
80
+ last = ordered[-1]
81
+ nxt = min(remaining, key=lambda w: ItineraryReasoning.haversine_km(last, w))
82
+ ordered.append(nxt)
83
+ remaining.remove(nxt)
84
+ method = "nearest_neighbor"
85
+
86
+ legs = [ItineraryReasoning.estimate_leg(ordered[i], ordered[i+1], mode)
87
+ for i in range(len(ordered)-1)]
88
+ return {
89
+ "order": [w.id for w in ordered],
90
+ "order_names": [w.name for w in ordered],
91
+ "total_km": round(sum(l.distance_km for l in legs), 2),
92
+ "total_min": round(sum(l.duration_min for l in legs), 1),
93
+ "legs": [l.to_dict() for l in legs],
94
+ "method": method,
95
+ }
96
+
97
+ @staticmethod
98
+ def detect_conflicts(waypoints: list, mode: str = "driving") -> list:
99
+ """按时间排序的行程,检测:① 时间重叠 ② 通勤赶不及。"""
100
+ timed = [w for w in waypoints if w.dt is not None]
101
+ timed.sort(key=lambda w: w.dt)
102
+ issues: list[dict[str, Any]] = []
103
+ for i in range(len(timed) - 1):
104
+ cur, nxt = timed[i], timed[i+1]
105
+ cur_end = cur.dt + timedelta(minutes=cur.duration_min)
106
+ if cur_end > nxt.dt:
107
+ issues.append({
108
+ "type": "time_conflict",
109
+ "between": [cur.name, nxt.name],
110
+ "ids": [cur.id, nxt.id],
111
+ "reason": f"「{cur.name}」结束({cur_end.strftime('%H:%M')})"
112
+ f"晚于「{nxt.name}」开始({nxt.dt.strftime('%H:%M')})",
113
+ })
114
+ continue
115
+ if cur.has_coord and nxt.has_coord:
116
+ leg = ItineraryReasoning.estimate_leg(cur, nxt, mode)
117
+ gap_min = (nxt.dt - cur_end).total_seconds() / 60
118
+ if leg.duration_min > gap_min:
119
+ issues.append({
120
+ "type": "unreachable",
121
+ "between": [cur.name, nxt.name],
122
+ "ids": [cur.id, nxt.id],
123
+ "need_min": round(leg.duration_min, 0),
124
+ "available_min": round(gap_min, 0),
125
+ "reason": f"从「{cur.name}」到「{nxt.name}」需 {leg.duration_min:.0f} 分钟,"
126
+ f"但只有 {gap_min:.0f} 分钟空档",
127
+ })
128
+ return issues
129
+
130
+ @staticmethod
131
+ def cluster_by_proximity(waypoints: list, radius_km: float = 3.0) -> list:
132
+ """把相近的行程点聚成簇,建议"顺路一起办"。"""
133
+ pts = [w for w in waypoints if w.has_coord]
134
+ clusters, used = [], set()
135
+ for w in pts:
136
+ if w.id in used:
137
+ continue
138
+ cluster = [w]
139
+ used.add(w.id)
140
+ for other in pts:
141
+ if other.id in used:
142
+ continue
143
+ if ItineraryReasoning.haversine_km(w, other) <= radius_km:
144
+ cluster.append(other)
145
+ used.add(other.id)
146
+ clusters.append([c.name for c in cluster])
147
+ return [c for c in clusters if len(c) > 1]
148
+
149
+ @staticmethod
150
+ def density(waypoints: list, mode: str = "driving") -> dict:
151
+ """行程密度:总点数、总通勤、跨城。"""
152
+ timed = sorted([w for w in waypoints if w.dt], key=lambda w: w.dt)
153
+ cities = set(w.city for w in waypoints if w.city)
154
+ total_commute = 0.0
155
+ for i in range(len(timed) - 1):
156
+ if timed[i].has_coord and timed[i+1].has_coord:
157
+ total_commute += ItineraryReasoning.estimate_leg(
158
+ timed[i], timed[i+1], mode).duration_min
159
+ span_hours = 0
160
+ if len(timed) >= 2:
161
+ span_hours = (timed[-1].dt - timed[0].dt).total_seconds() / 3600
162
+ return {
163
+ "waypoints": len(waypoints),
164
+ "cities": list(cities),
165
+ "cross_city": len(cities) > 1,
166
+ "total_commute_min": round(total_commute, 1),
167
+ "span_hours": round(span_hours, 1),
168
+ }
@@ -0,0 +1,144 @@
1
+ """
2
+ Foxclaw 行程子系统 — Itinerary Runtime
3
+ 行程行为操作系统:采集行程点 → 地理编码(高德或手动) → 路线优化 →
4
+ 冲突/可达检测 → 行程守门人。高德可用时自动用真实路线替换估算。
5
+ """
6
+
7
+ import csv
8
+ from pathlib import Path
9
+ from typing import cast
10
+
11
+ from .models import ItineraryVerdict, TravelMode, Waypoint
12
+ from .reasoning import MODE_SPEED, ItineraryReasoning
13
+ from .amap_client import AMapClient
14
+
15
+
16
+ class ItineraryRuntime:
17
+ """行政 Agent 的行程内核。"""
18
+
19
+ def __init__(self, amap_key: str | None = None, hmr=None):
20
+ self.waypoints: list = []
21
+ self.reasoning = ItineraryReasoning()
22
+ self.amap = AMapClient(amap_key)
23
+ self.hmr = hmr
24
+
25
+ @property
26
+ def amap_enabled(self) -> bool:
27
+ return self.amap.enabled
28
+
29
+ def add_waypoint(self, name: str, address: str = "", lng: float | None = None,
30
+ lat: float | None = None, start_time: str | None = None,
31
+ duration_min: int = 0, city: str = "") -> dict:
32
+ wp = Waypoint(name=name, address=address, lng=lng, lat=lat,
33
+ start_time=start_time, duration_min=duration_min, city=city)
34
+ if not wp.has_coord and address and self.amap_enabled:
35
+ geo = self.amap.geocode(address, city)
36
+ if geo:
37
+ wp.lng, wp.lat = geo
38
+ self.waypoints.append(wp)
39
+ if self.hmr:
40
+ try:
41
+ self.hmr.ingest(content=f"行程:{name} @ {address or wp.coord_str} "
42
+ f"{start_time or ''}",
43
+ type="task", agent_id="office",
44
+ title=f"行程_{name}", tags=["itinerary"])
45
+ except Exception:
46
+ pass
47
+ return wp.to_dict()
48
+
49
+ def import_csv(self, path: str) -> int:
50
+ """CSV 导入。列:name,address,lng,lat,start_time,duration_min,city"""
51
+ count = 0
52
+ with open(Path(path), encoding="utf-8") as f:
53
+ for row in csv.DictReader(f):
54
+ self.add_waypoint(
55
+ name=row["name"], address=row.get("address", ""),
56
+ lng=float(row["lng"]) if row.get("lng") else None,
57
+ lat=float(row["lat"]) if row.get("lat") else None,
58
+ start_time=row.get("start_time") or None,
59
+ duration_min=int(row.get("duration_min") or 0),
60
+ city=row.get("city", ""))
61
+ count += 1
62
+ return count
63
+
64
+ def real_leg(self, a: Waypoint, b: Waypoint, mode: str = "driving"):
65
+ """两点路线:高德可用且有坐标时用真实数据,否则直线估算。"""
66
+ if self.amap_enabled and a.has_coord and b.has_coord:
67
+ r = self.amap.route(a.coord_str, b.coord_str, mode, a.city)
68
+ if r:
69
+ from .models import RouteLeg
70
+ return RouteLeg(from_id=a.id, to_id=b.id, from_name=a.name,
71
+ to_name=b.name,
72
+ mode=cast(TravelMode, mode if mode in MODE_SPEED else "driving"),
73
+ distance_km=r["distance_km"],
74
+ duration_min=r["duration_min"], source="amap")
75
+ return self.reasoning.estimate_leg(a, b, mode)
76
+
77
+ def plan(self, mode: str = "driving", optimize: bool = True) -> dict:
78
+ """完整行程规划:优化顺序 + 冲突检测 + 聚类 + 守门人裁决。"""
79
+ opt = (self.reasoning.optimize_order(self.waypoints, mode)
80
+ if optimize else None)
81
+ conflicts = self.reasoning.detect_conflicts(self.waypoints, mode)
82
+ clusters = self.reasoning.cluster_by_proximity(self.waypoints)
83
+ dens = self.reasoning.density(self.waypoints, mode)
84
+ verdicts = self._gatekeeper(conflicts, clusters, dens)
85
+ return {
86
+ "waypoint_count": len(self.waypoints),
87
+ "amap_enabled": self.amap_enabled,
88
+ "optimized_route": opt,
89
+ "conflicts": conflicts,
90
+ "proximity_clusters": clusters,
91
+ "density": dens,
92
+ "gatekeeper_verdicts": [v.to_dict() for v in verdicts],
93
+ "map_data": self._to_amap_mapdata(opt),
94
+ }
95
+
96
+ def _gatekeeper(self, conflicts, clusters, dens) -> list:
97
+ verdicts = []
98
+ for c in conflicts:
99
+ if c["type"] == "time_conflict":
100
+ verdicts.append(ItineraryVerdict(
101
+ rule_id="time_conflict", action="time_conflict",
102
+ severity="critical", message=c["reason"], waypoint_ids=c["ids"]))
103
+ elif c["type"] == "unreachable":
104
+ verdicts.append(ItineraryVerdict(
105
+ rule_id="unreachable", action="unreachable",
106
+ severity="critical", message=c["reason"], waypoint_ids=c["ids"]))
107
+ if dens["cross_city"]:
108
+ verdicts.append(ItineraryVerdict(
109
+ rule_id="cross_city", action="cross_city", severity="warn",
110
+ message=f"行程跨城市:{','.join(dens['cities'])},注意城际交通时间",
111
+ payload={"cities": dens["cities"]}))
112
+ if dens["total_commute_min"] > 180:
113
+ verdicts.append(ItineraryVerdict(
114
+ rule_id="too_dense", action="too_dense", severity="warn",
115
+ message=f"总通勤 {dens['total_commute_min']:.0f} 分钟过长,建议精简或分天",
116
+ payload=dens))
117
+ for cl in clusters:
118
+ verdicts.append(ItineraryVerdict(
119
+ rule_id="reorder", action="suggest_reorder", severity="info",
120
+ message=f"这几处相近,建议顺路一起办:{' · '.join(cl)}",
121
+ payload={"cluster": cl}))
122
+ return verdicts
123
+
124
+ def _to_amap_mapdata(self, opt) -> list:
125
+ """转成高德 MapTaskData(POI + route),可喂给高德生成可视化地图。"""
126
+ data = []
127
+ for wp in self.waypoints:
128
+ if wp.has_coord:
129
+ data.append({"type": "poi", "lnglat": [wp.lng, wp.lat],
130
+ "text": wp.name, "remark": wp.address or wp.note})
131
+ if opt and opt.get("legs"):
132
+ id2wp = {w.id: w for w in self.waypoints}
133
+ for leg in opt["legs"]:
134
+ a, b = id2wp.get(leg["from_id"]), id2wp.get(leg["to_id"])
135
+ if a and b and a.has_coord and b.has_coord:
136
+ data.append({"type": "route", "routeType": leg["mode"],
137
+ "start": [a.lng, a.lat], "end": [b.lng, b.lat],
138
+ "remark": f"{a.name}→{b.name}"})
139
+ return data
140
+
141
+ def status(self) -> dict:
142
+ return {"waypoints": len(self.waypoints),
143
+ "amap_enabled": self.amap_enabled,
144
+ "with_coord": sum(1 for w in self.waypoints if w.has_coord)}
@@ -0,0 +1,13 @@
1
+ """Foxclaw 内核层:进程管理、调度、事件脊柱、能力注册等 OS 式抽象。"""
2
+ from .process_manager import ProcessManager, ProcessRecord
3
+ from .spine import EventSpine, Event
4
+ from .capabilities import CapabilityRegistry
5
+ from .scheduler import Scheduler
6
+ from .policy_gate import PolicyGate, Verdict
7
+
8
+ __all__ = [
9
+ "ProcessManager", "ProcessRecord",
10
+ "EventSpine", "Event",
11
+ "CapabilityRegistry", "Scheduler",
12
+ "PolicyGate", "Verdict",
13
+ ]
@@ -0,0 +1,72 @@
1
+ """
2
+ CapabilityRegistry —— 能力注册中枢(P1-3:God Object → Composition Root)。
3
+
4
+ 审计结论:Foxclaw.__init__ 是个 God Object,子系统之间还形成两处对象环
5
+ (Supervisor⇄Orchestrator、agents⇄search_runtime⇄agent_port⇄agents)——组件直接
6
+ 互相持有具体引用,违反 Clean Architecture。
7
+
8
+ 方案:引入单一注册中枢。组件不再直接持有"同伴组件"的具体引用,而是持有本注册表、
9
+ 按名字**惰性解析**(依赖倒置:依赖注册表抽象,而非具体同伴)。于是:
10
+ · 直接的"A→B 且 B→A"互持消失(两处直接环被打断);
11
+ · 解析统一走一个 hub(Composition Root 模式),构造顺序不再产生初始化环;
12
+ · 支持 register_lazy:重/可选能力(HKC、SearchRuntime)首次被解析时才构造。
13
+
14
+ 注:hub 与组件之间仍是可达环(注册表持有组件、组件持有注册表),这是 Service Locator
15
+ 的固有形态,由 GC 正常回收;本次要消除的是"具体同伴之间的直接互持",而非对象图可达性。
16
+ """
17
+ from __future__ import annotations
18
+ import threading
19
+ from typing import Any, Callable
20
+
21
+
22
+ class CapabilityRegistry:
23
+ """按名字注册/解析能力。支持惰性(首次解析才构造)。线程安全。"""
24
+
25
+ def __init__(self):
26
+ self._caps: dict[str, Any] = {}
27
+ self._lazy: dict[str, Callable[[], Any]] = {}
28
+ self._lock = threading.RLock()
29
+ self._build_locks: dict[str, threading.Lock] = {}
30
+
31
+ def register(self, name: str, handle: Any) -> None:
32
+ """注册一个已构造好的能力实例(覆盖同名)。"""
33
+ with self._lock:
34
+ self._caps[name] = handle
35
+
36
+ def register_lazy(self, name: str, factory: Callable[[], Any]) -> None:
37
+ """注册惰性能力:首次 get(name) 时调用 factory 构造并缓存。"""
38
+ with self._lock:
39
+ self._lazy[name] = factory
40
+
41
+ def get(self, name: str, default: Any | None = None) -> Any:
42
+ """解析能力。已注册直接返回;惰性则首次构造并缓存;都没有返回 default。
43
+
44
+ 【F1 修复】慢惰性构造(如 search_runtime 首建要装 HKC+载模型)**不持全局锁**,
45
+ 改用 per-name 构造锁:同一能力串行构造(只建一次、无双重构造),不同能力互不阻塞
46
+ (避免"首建 search 时,并发的 cross_request 解析 agents 被卡住")。factory 内部
47
+ 可安全再 get 其它能力(全局锁此时未被本调用持有)。"""
48
+ with self._lock:
49
+ if name in self._caps:
50
+ return self._caps[name]
51
+ factory = self._lazy.get(name)
52
+ if factory is None:
53
+ return default
54
+ blk = self._build_locks.get(name)
55
+ if blk is None:
56
+ blk = self._build_locks[name] = threading.Lock()
57
+ with blk:
58
+ with self._lock:
59
+ if name in self._caps:
60
+ return self._caps[name]
61
+ inst = factory()
62
+ with self._lock:
63
+ self._caps[name] = inst
64
+ return inst
65
+
66
+ def has(self, name: str) -> bool:
67
+ with self._lock:
68
+ return name in self._caps or name in self._lazy
69
+
70
+ def names(self) -> list[str]:
71
+ with self._lock:
72
+ return sorted(set(self._caps) | set(self._lazy))