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,152 @@
1
+ """
2
+ Foxclaw Harness — 工具循环检测 (Tool-Loop Detection)
3
+
4
+ 借鉴 OpenClaw 的循环检测思路,适配 Foxclaw 的 Harness 体系。
5
+ 断路器管"失败",循环检测管"无进展死循环"——Agent 不报错却陷入
6
+ 重复/横跳,白烧 token。这是行为异常的一种,与 Harness 漂移检测同源。
7
+
8
+ 三种循环模式:
9
+ repeat 同一调用(工具+参数签名)连续重复 N 次
10
+ ping_pong 两个调用交替出现(A→B→A→B)
11
+ no_progress 同工具反复调用但结果无变化(轮询卡死)
12
+ 多级:warning(预警)→ critical(严重)→ 熔断(建议中止)
13
+ """
14
+ from __future__ import annotations
15
+ import hashlib
16
+ import json
17
+ import threading
18
+ from collections import deque
19
+ from dataclasses import dataclass
20
+
21
+
22
+ @dataclass
23
+ class LoopVerdict:
24
+ stuck: bool = False
25
+ level: str = "ok" # ok / warning / critical
26
+ detector: str = "" # repeat / ping_pong / no_progress
27
+ count: int = 0
28
+ message: str = ""
29
+
30
+ def to_dict(self) -> dict:
31
+ return {"stuck": self.stuck, "level": self.level,
32
+ "detector": self.detector, "count": self.count,
33
+ "message": self.message}
34
+
35
+
36
+ @dataclass
37
+ class _ToolCall:
38
+ signature: str # 工具名+参数的指纹
39
+ tool_name: str
40
+ result_hash: str = "" # 结果指纹(判 no_progress)
41
+
42
+
43
+ class ToolLoopDetector:
44
+ """监控工具调用历史,检测无进展死循环。每个 Agent/会话一个实例。"""
45
+
46
+ def __init__(self, history_size: int = 30, warning: int = 6,
47
+ critical: int = 12, circuit_break: int = 20):
48
+ # 阈值单调递增保证(warning < critical < circuit_break)
49
+ self.warning = max(2, warning)
50
+ self.critical = max(self.warning + 1, critical)
51
+ self.circuit_break = max(self.critical + 1, circuit_break)
52
+ self.history: deque = deque(maxlen=max(history_size, self.circuit_break))
53
+ self._last_verdict = None
54
+ # 【深审·线程安全】SkillAdapter 持单个共享实例,invoke(含管线内 dispatch)可被并发任务
55
+ # 同时调 record → history.append + _detect(list(history)) 非原子,需锁(线程安全铁律)。
56
+ self._lock = threading.RLock()
57
+
58
+ @staticmethod
59
+ def _signature(tool_name: str, args: dict) -> str:
60
+ """工具名 + 规范化参数 → 指纹。"""
61
+ try:
62
+ payload = json.dumps(args or {}, sort_keys=True, ensure_ascii=False)
63
+ except Exception:
64
+ payload = str(args)
65
+ return hashlib.sha256(f"{tool_name}::{payload}".encode()).hexdigest()[:16]
66
+
67
+ @staticmethod
68
+ def _result_sig(result) -> str:
69
+ try:
70
+ s = json.dumps(result, sort_keys=True, ensure_ascii=False)[:500]
71
+ except Exception:
72
+ s = str(result)[:500]
73
+ return hashlib.sha256(s.encode()).hexdigest()[:16]
74
+
75
+ def record(self, tool_name: str, args: dict = None, result=None) -> LoopVerdict:
76
+ """记录一次工具调用并即时检测。返回 LoopVerdict。"""
77
+ sig = self._signature(tool_name, args or {})
78
+ rhash = self._result_sig(result) if result is not None else ""
79
+ with self._lock: # append + _detect 必须原子(并发 record 会交错)
80
+ self.history.append(_ToolCall(sig, tool_name, rhash))
81
+ self._last_verdict = self._detect()
82
+ return self._last_verdict
83
+
84
+ def _level_for(self, count: int) -> str:
85
+ if count >= self.circuit_break:
86
+ return "critical" # 达熔断阈值仍按 critical 等级(由 stuck+建议中止体现)
87
+ if count >= self.critical:
88
+ return "critical"
89
+ if count >= self.warning:
90
+ return "warning"
91
+ return "ok"
92
+
93
+ def _detect(self) -> LoopVerdict:
94
+ h = list(self.history)
95
+ if len(h) < self.warning:
96
+ return LoopVerdict()
97
+
98
+ # 1. repeat / no_progress:末尾连续相同签名
99
+ last_sig = h[-1].signature
100
+ streak = 0
101
+ for c in reversed(h):
102
+ if c.signature == last_sig:
103
+ streak += 1
104
+ else:
105
+ break
106
+ if streak >= self.warning:
107
+ lvl = self._level_for(streak)
108
+ # no_progress 优先:同签名重复 且 (有结果的那些)结果都相同
109
+ # (只比对非空 result_hash,避免混合有/无 result 时漏判轮询卡死)
110
+ streak_calls = h[-streak:]
111
+ with_result = [c.result_hash for c in streak_calls if c.result_hash]
112
+ if len(with_result) >= 2 and len(set(with_result)) == 1:
113
+ return LoopVerdict(
114
+ stuck=True, level=lvl, detector="no_progress", count=streak,
115
+ message=(f"工具「{h[-1].tool_name}」轮询 {streak} 次结果无变化"
116
+ + ("(已达熔断阈值,建议中止)" if streak >= self.circuit_break else "")))
117
+ # 否则按 repeat
118
+ return LoopVerdict(
119
+ stuck=True, level=lvl, detector="repeat", count=streak,
120
+ message=(f"工具「{h[-1].tool_name}」连续重复调用 {streak} 次"
121
+ + ("(已达熔断阈值,建议中止)" if streak >= self.circuit_break else "")))
122
+
123
+ # 3. ping_pong:末尾两签名交替(A→B→A→B)
124
+ if len(h) >= 4:
125
+ a, b = h[-1].signature, h[-2].signature
126
+ if a != b:
127
+ alt = 0
128
+ expect = [a, b]
129
+ for i, c in enumerate(reversed(h)):
130
+ if c.signature == expect[i % 2]:
131
+ alt += 1
132
+ else:
133
+ break
134
+ if alt >= self.warning:
135
+ return LoopVerdict(
136
+ stuck=True, level=self._level_for(alt),
137
+ detector="ping_pong", count=alt,
138
+ message=(f"工具「{h[-1].tool_name}」与「{h[-2].tool_name}」"
139
+ f"交替横跳 {alt} 次,无进展"))
140
+
141
+ return LoopVerdict()
142
+
143
+ def should_abort(self) -> bool:
144
+ """是否建议中止(达到熔断阈值)。复用最近一次 record 的检测结果。"""
145
+ with self._lock:
146
+ v = self._last_verdict or self._detect()
147
+ return v.stuck and v.count >= self.circuit_break
148
+
149
+ def reset(self):
150
+ with self._lock:
151
+ self.history.clear()
152
+ self._last_verdict = None
@@ -0,0 +1,10 @@
1
+ """Foxclaw — Itinerary: 行政 Agent 的行程行为操作系统内核(接高德地图)"""
2
+ from .models import Waypoint, RouteLeg, ItineraryVerdict
3
+ from .reasoning import ItineraryReasoning, MODE_SPEED
4
+ from .amap_client import AMapClient
5
+ from .runtime import ItineraryRuntime
6
+
7
+ __all__ = [
8
+ "Waypoint", "RouteLeg", "ItineraryVerdict",
9
+ "ItineraryReasoning", "MODE_SPEED", "AMapClient", "ItineraryRuntime",
10
+ ]
@@ -0,0 +1,117 @@
1
+ """
2
+ Foxclaw 行程子系统 — 高德地图适配层
3
+ 封装高德 Web Service API。无 API Key 时所有方法降级返回 None,
4
+ 行程内核自动回退到纯算法估算(直线距离)。
5
+ 本地配置环境变量 AMAP_MAPS_API_KEY 后即接入真实高德数据。
6
+
7
+ 对齐高德 Web Service API 参数格式:
8
+ - 坐标:经度在前,纬度在后,逗号分隔 "116.397,39.909"
9
+ - geocode / regeo / route(walking|driving|transit|cycling) / poi / weather
10
+
11
+ 也可改为接官方 MCP server: @amap/amap-maps-mcp-server
12
+ (通过 npx 启动,环境变量同样是 AMAP_MAPS_API_KEY)。
13
+ """
14
+
15
+ import os
16
+ import json
17
+ from typing import Optional
18
+ from urllib import request, parse
19
+
20
+
21
+ AMAP_BASE = "https://restapi.amap.com/v3"
22
+
23
+
24
+ class AMapClient:
25
+ """高德 Web Service 客户端。无 Key 则 enabled=False,调用返回 None。"""
26
+
27
+ def __init__(self, api_key: str = None):
28
+ self.api_key = api_key or os.environ.get("AMAP_MAPS_API_KEY")
29
+ self.enabled = bool(self.api_key)
30
+
31
+ def _get(self, path: str, params: dict) -> Optional[dict]:
32
+ if not self.enabled:
33
+ return None
34
+ params["key"] = self.api_key
35
+ url = f"{AMAP_BASE}/{path}?{parse.urlencode(params)}"
36
+ try:
37
+ with request.urlopen(url, timeout=8) as resp:
38
+ data = json.loads(resp.read().decode("utf-8"))
39
+ return data if data.get("status") == "1" else None
40
+ except Exception:
41
+ return None
42
+
43
+ # ── 地理编码 ──────────────────────────────────────────
44
+ def geocode(self, address: str, city: str = "") -> Optional[tuple]:
45
+ """地址 → (lng, lat)。"""
46
+ data = self._get("geocode/geo", {"address": address, "city": city})
47
+ if data and data.get("geocodes"):
48
+ loc = data["geocodes"][0]["location"] # "lng,lat"
49
+ lng, lat = loc.split(",")
50
+ return float(lng), float(lat)
51
+ return None
52
+
53
+ def regeo(self, lng: float, lat: float) -> Optional[str]:
54
+ """坐标 → 地址。"""
55
+ data = self._get("geocode/regeo", {"location": f"{lng},{lat}"})
56
+ if data and data.get("regeocode"):
57
+ return data["regeocode"].get("formatted_address")
58
+ return None
59
+
60
+ # ── 路线规划 ──────────────────────────────────────────
61
+ def route(self, origin: str, destination: str, mode: str = "driving",
62
+ city: str = "") -> Optional[dict]:
63
+ """真实路线。origin/destination 为 'lng,lat'。
64
+ 返回 {distance_km, duration_min}。"""
65
+ path_map = {
66
+ "driving": "direction/driving",
67
+ "walking": "direction/walking",
68
+ "cycling": "direction/bicycling",
69
+ "transit": "direction/transit/integrated",
70
+ }
71
+ path = path_map.get(mode, "direction/driving")
72
+ params = {"origin": origin, "destination": destination}
73
+ if mode == "transit":
74
+ params["city"] = city
75
+ data = self._get(path, params)
76
+ if not data:
77
+ return None
78
+ try:
79
+ route = data["route"]
80
+ if mode == "transit":
81
+ seg = route["transits"][0]
82
+ return {"distance_km": round(int(seg["distance"]) / 1000, 2),
83
+ "duration_min": round(int(seg["duration"]) / 60, 1),
84
+ "source": "amap"}
85
+ else:
86
+ path0 = route["paths"][0]
87
+ return {"distance_km": round(int(path0["distance"]) / 1000, 2),
88
+ "duration_min": round(int(path0["duration"]) / 60, 1),
89
+ "source": "amap"}
90
+ except Exception:
91
+ return None
92
+
93
+ # ── POI / 周边 / 天气 ─────────────────────────────────
94
+ def search_poi(self, keywords: str, city: str = "", offset: int = 10) -> Optional[list]:
95
+ data = self._get("place/text",
96
+ {"keywords": keywords, "city": city, "offset": offset})
97
+ if data and data.get("pois"):
98
+ return [{"name": p["name"], "address": p.get("address", ""),
99
+ "location": p["location"], "type": p.get("type", "")}
100
+ for p in data["pois"]]
101
+ return None
102
+
103
+ def nearby(self, location: str, keywords: str = "", radius: int = 1000) -> Optional[list]:
104
+ data = self._get("place/around",
105
+ {"location": location, "keywords": keywords, "radius": radius})
106
+ if data and data.get("pois"):
107
+ return [{"name": p["name"], "address": p.get("address", ""),
108
+ "location": p["location"]} for p in data["pois"]]
109
+ return None
110
+
111
+ def weather(self, city: str) -> Optional[dict]:
112
+ data = self._get("weather/weatherInfo", {"city": city})
113
+ if data and data.get("lives"):
114
+ w = data["lives"][0]
115
+ return {"city": w["city"], "weather": w["weather"],
116
+ "temperature": w["temperature"], "wind": w["winddirection"]}
117
+ return None
@@ -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 # 地点名:公司、客户A、机场
23
+ address: str = "" # 地址(可经地理编码转坐标)
24
+ lng: Optional[float] = None # 经度(高德:经度在前)
25
+ lat: Optional[float] = None # 纬度
26
+ start_time: Optional[str] = None # 计划到达时间 ISO
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" # 估算 or 高德真实
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,101 @@
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
+ # "紧凑"阈值:某段空档 / 所需通勤 < 该比例的倍数即算逼近(未赶不及但很赶),
18
+ # 交给可选 LLM 复核是否提醒用户。仅在启用 LLM 时生效。
19
+ TIGHT_RATIO = 1.5
20
+
21
+
22
+ class ItineraryConflictReasoner(DomainReasoner):
23
+ """subject = (waypoints, mode)。裁决:approve(行程可行) / confirm(有冲突或需注意)。"""
24
+
25
+ def __init__(self, mode: str = "driving", llm=None, llm_enabled: bool = False):
26
+ self.mode = mode
27
+ self.llm = llm
28
+ self.llm_enabled = llm_enabled
29
+
30
+ def observe(self, subject) -> dict:
31
+ waypoints, mode = subject
32
+ timed = [w for w in waypoints if getattr(w, "dt", None) is not None]
33
+ return {"count": len(waypoints), "timed": len(timed), "mode": mode or self.mode}
34
+
35
+ def hypothesize(self, obs: dict) -> list:
36
+ h = []
37
+ if obs["timed"] >= 2:
38
+ h += ["可能存在时间重叠", "可能通勤赶不及"]
39
+ if obs["count"] >= 2:
40
+ h.append("可能排程紧凑")
41
+ return h or ["行程点不足以产生冲突"]
42
+
43
+ def gather_evidence(self, subject, obs: dict) -> list:
44
+ waypoints, mode = subject
45
+ mode = obs["mode"]
46
+ conflicts = ItineraryReasoning.detect_conflicts(waypoints, mode) # 规则=裁决证据
47
+ ev = [Evidence(factor=c["type"], detail=c["reason"], weight=1.0, raw=c)
48
+ for c in conflicts]
49
+ if not conflicts:
50
+ ev.append(Evidence("no_conflict", "未检测到时间冲突或通勤不可达", 0.5))
51
+ # informational(raw=None):顺路簇/密度,丰满证据链但不参与裁决
52
+ try:
53
+ clusters = ItineraryReasoning.cluster_by_proximity(waypoints)
54
+ if clusters:
55
+ ev.append(Evidence("proximity_clusters",
56
+ f"{len(clusters)} 组可顺路合并:{clusters}", 0.3))
57
+ dens = ItineraryReasoning.density(waypoints, mode)
58
+ ev.append(Evidence("density",
59
+ f"{dens['waypoints']} 点/{dens['total_commute_min']:.0f} 分通勤"
60
+ + ("/跨城" if dens["cross_city"] else ""), 0.3))
61
+ except Exception:
62
+ pass
63
+ return ev
64
+
65
+ def arbitrate(self, subject, obs, hyps, ev) -> ReasonedVerdict:
66
+ triggered = [e for e in ev if e.raw is not None]
67
+ if triggered:
68
+ # 【规则权威】检出硬冲突 → confirm,绝不被 LLM 推翻/隐藏
69
+ return ReasonedVerdict(
70
+ "confirm", ";".join(e.detail for e in triggered),
71
+ ev, hyps, "rules")
72
+ # 无硬冲突:可选让 LLM 就"紧凑排程"复核是否升级(只 approve→confirm)
73
+ if DomainReasoner.llm_available(self.llm, self.llm_enabled) and self._is_tight(subject, obs):
74
+ if DomainReasoner.llm_should_escalate(self.llm, *self._llm_prompt(subject, obs, ev)):
75
+ return ReasonedVerdict("confirm", "排程紧凑,LLM 建议人工确认", ev, hyps, "llm")
76
+ return ReasonedVerdict("approve", "行程可行:无时间冲突、通勤可达", ev, hyps, "rules")
77
+
78
+ def _is_tight(self, subject, obs) -> bool:
79
+ """无硬冲突,但存在某段空档紧贴所需通勤(available < TIGHT_RATIO×need)。"""
80
+ waypoints, mode = subject
81
+ mode = obs["mode"]
82
+ timed = sorted([w for w in waypoints if getattr(w, "dt", None) is not None],
83
+ key=lambda w: w.dt)
84
+ from datetime import timedelta
85
+ for i in range(len(timed) - 1):
86
+ cur, nxt = timed[i], timed[i + 1]
87
+ if not (cur.has_coord and nxt.has_coord):
88
+ continue
89
+ cur_end = cur.dt + timedelta(minutes=cur.duration_min)
90
+ gap = (nxt.dt - cur_end).total_seconds() / 60
91
+ leg = ItineraryReasoning.estimate_leg(cur, nxt, mode)
92
+ if 0 < gap and leg.duration_min <= gap < leg.duration_min * TIGHT_RATIO:
93
+ return True
94
+ return False
95
+
96
+ def _llm_prompt(self, subject, obs, ev):
97
+ ev_text = "\n".join(f"- {e.factor}: {e.detail}" for e in ev) or "(无)"
98
+ system = ("你是行程助手。这份行程规则检查未发现硬冲突,但排程较紧。"
99
+ "判断是否仍建议用户人工确认。只回答 CONFIRM(建议确认)或 OK(无需)。")
100
+ user = f"行程:{obs['count']} 个点,方式 {obs['mode']}。\n证据:\n{ev_text}\n\n只回答一个词:CONFIRM 或 OK。"
101
+ return system, user
@@ -0,0 +1,174 @@
1
+ """
2
+ Foxclaw 行程子系统 — 推理引擎(行程 Agent 的大脑,纯算法)
3
+ 不依赖高德也能跑:路线优化(TSP)、可达性、时间冲突、地理聚类。
4
+ 接入高德后,把"直线估算"替换为"真实路线时间"(见 runtime)。
5
+ """
6
+
7
+ import math
8
+ from datetime import timedelta
9
+ from itertools import permutations
10
+
11
+ from .models import Waypoint, RouteLeg
12
+
13
+
14
+ # 各出行方式的估算平均速度(km/h),接高德后用真实数据替换
15
+ MODE_SPEED = {"walking": 5, "cycling": 15, "transit": 25, "driving": 35}
16
+
17
+
18
+ class ItineraryReasoning:
19
+ """行程推理:把地点+时间变成可决策的行程智能。"""
20
+
21
+ # ── 距离与可达性(直线估算)──────────────────────────
22
+ @staticmethod
23
+ def haversine_km(a: Waypoint, b: Waypoint) -> float:
24
+ """两点直线距离(km)。Haversine 公式。"""
25
+ if not (a.has_coord and b.has_coord):
26
+ return 0.0
27
+ R = 6371.0
28
+ dlat = math.radians(b.lat - a.lat)
29
+ dlng = math.radians(b.lng - a.lng)
30
+ x = (math.sin(dlat/2)**2 +
31
+ math.cos(math.radians(a.lat)) * math.cos(math.radians(b.lat)) *
32
+ math.sin(dlng/2)**2)
33
+ return R * 2 * math.atan2(math.sqrt(x), math.sqrt(1-x))
34
+
35
+ @staticmethod
36
+ def estimate_leg(a: Waypoint, b: Waypoint, mode: str = "driving") -> RouteLeg:
37
+ """估算两点路线段(直线距离 × 1.3 绕路系数 / 速度)。"""
38
+ straight = ItineraryReasoning.haversine_km(a, b)
39
+ road_km = straight * 1.3 # 绕路系数
40
+ speed = MODE_SPEED.get(mode, 35)
41
+ duration = (road_km / speed) * 60 if speed else 0
42
+ return RouteLeg(
43
+ from_id=a.id, to_id=b.id, from_name=a.name, to_name=b.name,
44
+ mode=mode, distance_km=round(road_km, 2),
45
+ duration_min=round(duration, 1), source="estimated",
46
+ )
47
+
48
+ # ── 路线优化(TSP)────────────────────────────────────
49
+ @staticmethod
50
+ def optimize_order(waypoints: list, mode: str = "driving",
51
+ fixed_start: bool = True) -> dict:
52
+ """最优访问顺序。≤8 点用精确全排列,更多用最近邻启发式。
53
+ fixed_start=True 时第一个点固定为起点(比如"从公司出发")。"""
54
+ pts = [w for w in waypoints if w.has_coord]
55
+ if len(pts) < 2:
56
+ return {"order": [w.id for w in waypoints], "total_km": 0,
57
+ "total_min": 0, "method": "trivial"}
58
+
59
+ def total_dist(seq):
60
+ return sum(ItineraryReasoning.haversine_km(seq[i], seq[i+1])
61
+ for i in range(len(seq)-1))
62
+
63
+ if len(pts) <= 8:
64
+ # 精确解
65
+ start = pts[0] if fixed_start else None
66
+ rest = pts[1:] if fixed_start else pts
67
+ best, best_d = None, float("inf")
68
+ for perm in permutations(rest):
69
+ seq = ([start] + list(perm)) if start else list(perm)
70
+ d = total_dist(seq)
71
+ if d < best_d:
72
+ best_d, best = d, seq
73
+ method = "exact"
74
+ ordered = best
75
+ else:
76
+ # 最近邻启发式
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
+ # 计算总距离/时间
87
+ legs = [ItineraryReasoning.estimate_leg(ordered[i], ordered[i+1], mode)
88
+ for i in range(len(ordered)-1)]
89
+ return {
90
+ "order": [w.id for w in ordered],
91
+ "order_names": [w.name for w in ordered],
92
+ "total_km": round(sum(l.distance_km for l in legs), 2),
93
+ "total_min": round(sum(l.duration_min for l in legs), 1),
94
+ "legs": [l.to_dict() for l in legs],
95
+ "method": method,
96
+ }
97
+
98
+ # ── 时间冲突 + 可达性 ─────────────────────────────────
99
+ @staticmethod
100
+ def detect_conflicts(waypoints: list, mode: str = "driving") -> list:
101
+ """按时间排序的行程,检测:① 时间重叠 ② 通勤赶不及。"""
102
+ timed = [w for w in waypoints if w.dt is not None]
103
+ timed.sort(key=lambda w: w.dt)
104
+ issues = []
105
+ for i in range(len(timed) - 1):
106
+ cur, nxt = timed[i], timed[i+1]
107
+ cur_end = cur.dt + timedelta(minutes=cur.duration_min)
108
+ # 时间重叠
109
+ if cur_end > nxt.dt:
110
+ issues.append({
111
+ "type": "time_conflict",
112
+ "between": [cur.name, nxt.name],
113
+ "ids": [cur.id, nxt.id],
114
+ "reason": f"「{cur.name}」结束({cur_end.strftime('%H:%M')})"
115
+ f"晚于「{nxt.name}」开始({nxt.dt.strftime('%H:%M')})",
116
+ })
117
+ continue
118
+ # 可达性:中间通勤时间够不够
119
+ if cur.has_coord and nxt.has_coord:
120
+ leg = ItineraryReasoning.estimate_leg(cur, nxt, mode)
121
+ gap_min = (nxt.dt - cur_end).total_seconds() / 60
122
+ if leg.duration_min > gap_min:
123
+ issues.append({
124
+ "type": "unreachable",
125
+ "between": [cur.name, nxt.name],
126
+ "ids": [cur.id, nxt.id],
127
+ "need_min": round(leg.duration_min, 0),
128
+ "available_min": round(gap_min, 0),
129
+ "reason": f"从「{cur.name}」到「{nxt.name}」需 {leg.duration_min:.0f} 分钟,"
130
+ f"但只有 {gap_min:.0f} 分钟空档",
131
+ })
132
+ return issues
133
+
134
+ # ── 地理聚类(顺路归组)───────────────────────────────
135
+ @staticmethod
136
+ def cluster_by_proximity(waypoints: list, radius_km: float = 3.0) -> list:
137
+ """把相近的行程点聚成簇,建议"顺路一起办"。"""
138
+ pts = [w for w in waypoints if w.has_coord]
139
+ clusters, used = [], set()
140
+ for w in pts:
141
+ if w.id in used:
142
+ continue
143
+ cluster = [w]
144
+ used.add(w.id)
145
+ for other in pts:
146
+ if other.id in used:
147
+ continue
148
+ if ItineraryReasoning.haversine_km(w, other) <= radius_km:
149
+ cluster.append(other)
150
+ used.add(other.id)
151
+ clusters.append([c.name for c in cluster])
152
+ return [c for c in clusters if len(c) > 1] # 只返回有2+点的簇
153
+
154
+ # ── 行程密度 ──────────────────────────────────────────
155
+ @staticmethod
156
+ def density(waypoints: list, mode: str = "driving") -> dict:
157
+ """行程密度:总点数、总通勤、跨城。"""
158
+ timed = sorted([w for w in waypoints if w.dt], key=lambda w: w.dt)
159
+ cities = set(w.city for w in waypoints if w.city)
160
+ total_commute = 0.0
161
+ for i in range(len(timed) - 1):
162
+ if timed[i].has_coord and timed[i+1].has_coord:
163
+ total_commute += ItineraryReasoning.estimate_leg(
164
+ timed[i], timed[i+1], mode).duration_min
165
+ span_hours = 0
166
+ if len(timed) >= 2:
167
+ span_hours = (timed[-1].dt - timed[0].dt).total_seconds() / 3600
168
+ return {
169
+ "waypoints": len(waypoints),
170
+ "cities": list(cities),
171
+ "cross_city": len(cities) > 1,
172
+ "total_commute_min": round(total_commute, 1),
173
+ "span_hours": round(span_hours, 1),
174
+ }