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,28 @@
1
+ """Harness 事件总线(观测优先)。"""
2
+ from typing import Callable
3
+
4
+
5
+ class HarnessEventBus:
6
+ """阶段一:事件总线(观测优先)"""
7
+
8
+ _SPINE_SKIP = {"goal_registered"}
9
+
10
+ def __init__(self, spine=None, source: str = ""):
11
+ self._handlers: dict[str, list[Callable]] = {}
12
+ self._spine = spine
13
+ self._source = source
14
+
15
+ def on(self, event: str, handler: Callable):
16
+ self._handlers.setdefault(event, []).append(handler)
17
+
18
+ def emit(self, event: str, data: dict):
19
+ for h in self._handlers.get(event, []):
20
+ try:
21
+ h(data)
22
+ except Exception as e:
23
+ print(f"[Harness EventBus] handler error: {e}")
24
+ if self._spine is not None and event not in self._SPINE_SKIP:
25
+ try:
26
+ self._spine.publish(event, data, source=self._source or "harness")
27
+ except Exception:
28
+ pass
@@ -0,0 +1,358 @@
1
+ """
2
+ Harness — 跨层约束与校准机制
3
+ 不是某一层的模块,而是每一层都能感知的治理张力
4
+
5
+ 三个检测维度:
6
+ 1. 语义距离 (semantic) — 节点内容 vs 目标基线
7
+ 2. 工具意图 (tool) — 工具类别 vs 允许集合
8
+ 3. 范围蔓延 (scope) — 当前实体 vs 初始实体集
9
+
10
+ Shadow mode:前 N 次只观测不截断,用真实数据校准阈值
11
+ """
12
+
13
+
14
+ import os
15
+ import uuid
16
+ import threading
17
+ from datetime import datetime, timezone
18
+ from pathlib import Path
19
+ from typing import Optional
20
+
21
+ from ..core.thought_chain import ThoughtChain, ThoughtNode, HarnessStatus
22
+
23
+ from ._base import (
24
+ HarnessResult,
25
+ GoalBaseline,
26
+ TOOL_CATEGORIES,
27
+ _harness_ctx_get,
28
+ _harness_ctx_set,
29
+ )
30
+ from .drift import SemanticDrift, ToolIntentDrift, ScopeCreepDrift
31
+ from .event_bus import HarnessEventBus
32
+ from ._policy import PolicyMixin
33
+
34
+ __all__ = [
35
+ "Harness", "HarnessResult", "GoalBaseline",
36
+ "SemanticDrift", "ToolIntentDrift", "ScopeCreepDrift",
37
+ "HarnessEventBus", "TOOL_CATEGORIES",
38
+ ]
39
+
40
+
41
+ class Harness(PolicyMixin):
42
+ """
43
+ Harness 主引擎
44
+ 每个 Agent 实例化一个,共享 ThoughtChain 存储
45
+ """
46
+ def __init__(
47
+ self,
48
+ agent_id: str,
49
+ thought_chain: ThoughtChain,
50
+ policy_path: str = "./foxclaw_data/policy.yaml",
51
+ shadow_rounds: int = 20,
52
+ embedding_model: str | None = None,
53
+ spine=None,
54
+ promote_min_obs: int | None = None,
55
+ ):
56
+ embedding_model = (embedding_model
57
+ or os.environ.get("EMBEDDING_MODEL")
58
+ or os.environ.get("HKC_EMBEDDING_MODEL")
59
+ or os.environ.get("FOXCLAW_EMBEDDING_MODEL")
60
+ or "all-MiniLM-L6-v2")
61
+ self.agent_id = agent_id
62
+ self.thought_chain = thought_chain
63
+ self.policy_path = Path(policy_path).resolve()
64
+ self.shadow_rounds = shadow_rounds
65
+ self._ctx_token = uuid.uuid4().hex
66
+ self._lock = threading.RLock()
67
+
68
+ self.sem_drift = SemanticDrift(embedding_model)
69
+ self.tool_drift = ToolIntentDrift()
70
+ self.scope_drift = ScopeCreepDrift()
71
+
72
+ self.bus = HarnessEventBus(spine=spine, source=agent_id)
73
+
74
+ self._baseline = None
75
+
76
+ self.weights = {"semantic": 0.5, "tool": 0.3, "scope": 0.2}
77
+
78
+ self.block_on_semantic = True
79
+
80
+ self.semantic_degraded: bool = (self.sem_drift.mode != "transformer")
81
+ self._degrade_warned: bool = False
82
+
83
+ self._shadow_mode = True
84
+ self._run_count = 0
85
+ self._delta_history: list[float] = []
86
+
87
+ self._warn_threshold = 0.15
88
+ self._block_threshold = 0.25
89
+
90
+ self._policy: dict = self._load_policy()
91
+ self._policy.setdefault("shadow_rules", [])
92
+ self._policy.setdefault("history", [])
93
+
94
+ if promote_min_obs is not None:
95
+ self.promote_min_obs = promote_min_obs
96
+ else:
97
+ _env = os.environ.get("FOXCLAW_SHADOW_PROMOTE_MIN_OBS", "").strip()
98
+ self.promote_min_obs = int(_env) if _env.isdigit() else 50
99
+
100
+ self._penalized_tools: set[str] = set()
101
+ self._scope_limit: Optional[int] = None
102
+ self._reindex_learned_actions()
103
+
104
+ self._setup_default_handlers()
105
+
106
+ @property
107
+ def _baseline(self):
108
+ return _harness_ctx_get(self._ctx_token)
109
+
110
+ @_baseline.setter
111
+ def _baseline(self, value):
112
+ _harness_ctx_set(self._ctx_token, value)
113
+
114
+
115
+ def register_goal(
116
+ self,
117
+ goal_text: str,
118
+ allowed_tool_categories: set[str],
119
+ baseline_entities: set[str] | None = None,
120
+ ) -> GoalBaseline:
121
+ """接口层调用 — 注册任务目标基线"""
122
+ baseline = GoalBaseline(
123
+ goal_text=goal_text,
124
+ allowed_tool_categories=allowed_tool_categories,
125
+ baseline_entities=baseline_entities or set(),
126
+ )
127
+ baseline.goal_embedding = self.sem_drift.encode(goal_text)
128
+ self._baseline = baseline
129
+ self.bus.emit("goal_registered", baseline.to_dict())
130
+ return baseline
131
+
132
+
133
+ def check(self, node: ThoughtNode, ignore_semantic: bool = False) -> HarnessResult:
134
+ """
135
+ 对一个 ThoughtNode 做 Harness 校验
136
+ 返回 HarnessResult,并更新节点的 harness_status
137
+
138
+ ignore_semantic=True:把语义维度置 0(只按 tool/scope 判定)。用于"工具授权"这类
139
+ 场景 —— 合成的工具调用串对目标句的语义距离是噪声,不该作为拦截依据。默认 False,
140
+ 不影响任何既有调用方的行为。
141
+
142
+ 【B 类修复】语义嵌入降级时(semantic_degraded=True):语义维度**停用**而非
143
+ 继续参与判定。哈希嵌入无区分度 → 漂移分恒 1.0 → 若继续加权会把所有正常
144
+ 请求一律 BLOCK(fail-closed-with-garbage)。降级方向应为 fail-open:
145
+ 停用不可信维度,保留可信的 tool/scope 判定,并在 reason 显式标注降级
146
+ (失效但可见)。走 _check_degraded 分支,正常路径逻辑逐字不变。
147
+ """
148
+ baseline = self._baseline
149
+ if baseline is None:
150
+ result = HarnessResult(
151
+ status="PASS", delta=0.0, dominant_dim="none",
152
+ scores={"semantic": 0.0, "tool": 0.0, "scope": 0.0},
153
+ )
154
+ self.thought_chain.update_harness(node.id, "PASS")
155
+ return result
156
+
157
+ if self.semantic_degraded and not ignore_semantic:
158
+ return self._check_degraded(node)
159
+
160
+ sem_score = 0.0 if ignore_semantic else \
161
+ self.sem_drift.score(node.content, baseline.goal_embedding)
162
+
163
+ with self._lock:
164
+ scores = {
165
+ "semantic": sem_score,
166
+ "tool": self.tool_drift.score(
167
+ node.tool_name or "", baseline.allowed_tool_categories
168
+ ) if node.tool_name else 0.0,
169
+ "scope": self.scope_drift.score(
170
+ set(node.entities), baseline.baseline_entities),
171
+ }
172
+
173
+ if node.tool_name and self._is_penalized_tool(node.tool_name):
174
+ scores["tool"] = 1.0
175
+ if (self._scope_limit is not None and baseline.baseline_entities):
176
+ new_entities = set(node.entities) - baseline.baseline_entities
177
+ if len(new_entities) > self._scope_limit:
178
+ scores["scope"] = 1.0
179
+
180
+ delta = sum(self.weights[k] * v for k, v in scores.items())
181
+ dominant = max(scores, key=lambda k: scores[k])
182
+
183
+ if self._shadow_mode:
184
+ self._delta_history.append(delta)
185
+ self._run_count += 1
186
+
187
+ if self._shadow_mode and self._run_count >= self.shadow_rounds:
188
+ self._calibrate_thresholds()
189
+
190
+ if self._shadow_mode:
191
+ status: HarnessStatus = "PASS"
192
+ reason = None
193
+ elif delta > self._block_threshold:
194
+ if not self.block_on_semantic and dominant == "semantic":
195
+ status = "WARN"
196
+ reason = f"semantic drift Δ={delta:.3f}(调度层仅告警不拦截)"
197
+ else:
198
+ status = "BLOCK"
199
+ reason = f"{dominant} drift={scores[dominant]:.3f} Δ={delta:.3f}"
200
+ elif delta > self._warn_threshold:
201
+ status = "WARN"
202
+ reason = f"{dominant} approaching threshold Δ={delta:.3f}"
203
+ else:
204
+ status = "PASS"
205
+ reason = None
206
+
207
+ result = HarnessResult(
208
+ status=status, delta=delta, dominant_dim=dominant,
209
+ scores=scores, reason=reason,
210
+ )
211
+
212
+ if self._policy.get("shadow_rules"):
213
+ self._observe_shadow_rules(node, result, scores)
214
+
215
+ if status == "BLOCK":
216
+ self._trigger_policy_learning(node, result)
217
+
218
+ self.thought_chain.update_harness(node.id, status, reason, delta, dominant)
219
+ self.bus.emit(f"harness_{status.lower()}", {
220
+ "agent_id": self.agent_id, "node_id": node.id, "node_type": node.type,
221
+ "result": result.__dict__,
222
+ "timestamp": datetime.now(timezone.utc).isoformat(),
223
+ })
224
+ return result
225
+
226
+ def _check_degraded(self, node: ThoughtNode) -> HarnessResult:
227
+ """语义嵌入降级时的判定:停用语义维度,保留 tool/scope 确定性判定。
228
+
229
+ 与 check() 的差异**仅在于**:跳过语义嵌入计算(省钱省时)且 semantic 权重
230
+ 按 0 计入。锁内累加/阈值校准/影子学习复用同一套逻辑,保证 shadow 学习与
231
+ 阈值校准行为与正常路径一致(不因降级而产生第二套语义)。
232
+
233
+ 为何不能只调阈值:无区分度时阈值怎么调都是错的 —— 高阈值漏放跑题,
234
+ 低阈值拦截正常请求。必须**停用该维度**。
235
+ 为何保留 tool/scope:两者是确定性规则(工具类别匹配 / 实体集合运算),
236
+ 不依赖模型质量,降级后仍可信。
237
+ """
238
+ baseline = self._baseline
239
+ if not self._degrade_warned:
240
+ self._degrade_warned = True
241
+ import logging as _lg
242
+ _lg.getLogger("foxclaw.harness").warning(
243
+ "[harness] 语义嵌入不可用(mode=%s),语义漂移维度已停用 —— "
244
+ "漂移检测暂退化为 tool/scope 两维,降级期间跑题不会被拦截。"
245
+ "恢复:pip install sentence-transformers",
246
+ self.sem_drift.mode)
247
+
248
+ with self._lock:
249
+ saved_w = self.weights.get("semantic", 0.0)
250
+ self.weights["semantic"] = 0.0
251
+ try:
252
+ scores = {
253
+ "semantic": 0.0,
254
+ "tool": self.tool_drift.score(
255
+ node.tool_name or "", baseline.allowed_tool_categories
256
+ ) if node.tool_name else 0.0,
257
+ "scope": self.scope_drift.score(
258
+ set(node.entities), baseline.baseline_entities),
259
+ }
260
+ if node.tool_name and self._is_penalized_tool(node.tool_name):
261
+ scores["tool"] = 1.0
262
+ if (self._scope_limit is not None and baseline.baseline_entities):
263
+ new_entities = set(node.entities) - baseline.baseline_entities
264
+ if len(new_entities) > self._scope_limit:
265
+ scores["scope"] = 1.0
266
+
267
+ delta = sum(self.weights[k] * v for k, v in scores.items())
268
+ dominant = max(scores, key=lambda k: scores[k])
269
+ if self._shadow_mode:
270
+ self._delta_history.append(delta)
271
+ self._run_count += 1
272
+ if self._shadow_mode and self._run_count >= self.shadow_rounds:
273
+ self._calibrate_thresholds()
274
+
275
+ if self._shadow_mode:
276
+ status: HarnessStatus = "PASS"
277
+ reason = None
278
+ elif delta > self._block_threshold:
279
+ status = "BLOCK"
280
+ reason = f"{dominant} drift={scores[dominant]:.3f} Δ={delta:.3f}"
281
+ elif delta > self._warn_threshold:
282
+ status = "WARN"
283
+ reason = f"{dominant} approaching threshold Δ={delta:.3f}"
284
+ else:
285
+ status = "PASS"
286
+ reason = None
287
+ reason = ((reason + " [semantic-degraded:治理降级]") if reason
288
+ else "[semantic-degraded:语义维度停用]")
289
+ finally:
290
+ self.weights["semantic"] = saved_w
291
+
292
+ result = HarnessResult(
293
+ status=status, delta=delta, dominant_dim=dominant,
294
+ scores=scores, reason=reason,
295
+ )
296
+ if self._policy.get("shadow_rules"):
297
+ self._observe_shadow_rules(node, result, scores)
298
+ if status == "BLOCK":
299
+ self._trigger_policy_learning(node, result)
300
+
301
+ self.thought_chain.update_harness(node.id, status, reason, delta, dominant)
302
+ self.bus.emit(f"harness_{status.lower()}", {
303
+ "agent_id": self.agent_id, "node_id": node.id, "node_type": node.type,
304
+ "result": result.__dict__, "degraded": True,
305
+ "timestamp": datetime.now(timezone.utc).isoformat(),
306
+ })
307
+ return result
308
+
309
+
310
+ def _setup_default_handlers(self):
311
+ def on_block(data):
312
+ from rich.console import Console
313
+ Console().print(
314
+ f"[bold red][HARNESS BLOCK][/] agent={data['agent_id']} "
315
+ f"node={data['node_id']} reason={data['result']['reason']}"
316
+ )
317
+
318
+ def on_policy_updated(data):
319
+ from rich.console import Console
320
+ Console().print(
321
+ f"[bold yellow][POLICY UPDATE][/] agent={data['agent_id']} "
322
+ f"rule={data['rule_id']} weights={data['new_weights']}"
323
+ )
324
+
325
+ self.bus.on("harness_block", on_block)
326
+ self.bus.on("policy_updated", on_policy_updated)
327
+
328
+
329
+ @property
330
+ def is_shadow_mode(self) -> bool:
331
+ return self._shadow_mode
332
+
333
+ @property
334
+ def thresholds(self) -> dict:
335
+ return {
336
+ "warn": self._warn_threshold,
337
+ "block": self._block_threshold,
338
+ "shadow_mode": self._shadow_mode,
339
+ "run_count": self._run_count,
340
+ "shadow_rounds": self.shadow_rounds,
341
+ "semantic_degraded": self.semantic_degraded,
342
+ "embedding_mode": self.sem_drift.mode,
343
+ }
344
+
345
+ def get_policy_summary(self) -> dict:
346
+ rules = self._policy.get("rules", [])
347
+ shadow = self._policy.get("shadow_rules", [])
348
+ return {
349
+ "version": self._policy.get("version", 1),
350
+ "total_rules": len(rules),
351
+ "manual_rules": sum(1 for r in rules if r.get("source") == "manual"),
352
+ "learned_rules": sum(1 for r in rules if r.get("source") == "harness_learned"),
353
+ "shadow_observing": sum(1 for r in shadow
354
+ if (r.get("shadow") or {}).get("status") == "observing"),
355
+ "shadow_total": len(shadow),
356
+ "history_events": len(self._policy.get("history", [])),
357
+ "weights": dict(self.weights),
358
+ }
@@ -0,0 +1,145 @@
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"
26
+ detector: str = ""
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 = ""
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
+ self.warning = max(2, warning)
49
+ self.critical = max(self.warning + 1, critical)
50
+ self.circuit_break = max(self.critical + 1, circuit_break)
51
+ self.history: deque = deque(maxlen=max(history_size, self.circuit_break))
52
+ self._last_verdict: LoopVerdict | None = None
53
+ self._lock = threading.RLock()
54
+
55
+ @staticmethod
56
+ def _signature(tool_name: str, args: dict) -> str:
57
+ """工具名 + 规范化参数 → 指纹。"""
58
+ try:
59
+ payload = json.dumps(args or {}, sort_keys=True, ensure_ascii=False)
60
+ except Exception:
61
+ payload = str(args)
62
+ return hashlib.sha256(f"{tool_name}::{payload}".encode()).hexdigest()[:16]
63
+
64
+ @staticmethod
65
+ def _result_sig(result) -> str:
66
+ try:
67
+ s = json.dumps(result, sort_keys=True, ensure_ascii=False)[:500]
68
+ except Exception:
69
+ s = str(result)[:500]
70
+ return hashlib.sha256(s.encode()).hexdigest()[:16]
71
+
72
+ def record(self, tool_name: str, args: dict | None = None, result=None) -> LoopVerdict:
73
+ """记录一次工具调用并即时检测。返回 LoopVerdict。"""
74
+ sig = self._signature(tool_name, args or {})
75
+ rhash = self._result_sig(result) if result is not None else ""
76
+ with self._lock:
77
+ self.history.append(_ToolCall(sig, tool_name, rhash))
78
+ verdict = self._detect()
79
+ self._last_verdict = verdict
80
+ return verdict
81
+
82
+ def _level_for(self, count: int) -> str:
83
+ if count >= self.circuit_break:
84
+ return "critical"
85
+ if count >= self.critical:
86
+ return "critical"
87
+ if count >= self.warning:
88
+ return "warning"
89
+ return "ok"
90
+
91
+ def _detect(self) -> LoopVerdict:
92
+ h = list(self.history)
93
+ if len(h) < self.warning:
94
+ return LoopVerdict()
95
+
96
+ last_sig = h[-1].signature
97
+ streak = 0
98
+ for c in reversed(h):
99
+ if c.signature == last_sig:
100
+ streak += 1
101
+ else:
102
+ break
103
+ if streak >= self.warning:
104
+ lvl = self._level_for(streak)
105
+ streak_calls = h[-streak:]
106
+ with_result = [c.result_hash for c in streak_calls if c.result_hash]
107
+ if len(with_result) >= 2 and len(set(with_result)) == 1:
108
+ return LoopVerdict(
109
+ stuck=True, level=lvl, detector="no_progress", count=streak,
110
+ message=(f"工具「{h[-1].tool_name}」轮询 {streak} 次结果无变化"
111
+ + ("(已达熔断阈值,建议中止)" if streak >= self.circuit_break else "")))
112
+ return LoopVerdict(
113
+ stuck=True, level=lvl, detector="repeat", count=streak,
114
+ message=(f"工具「{h[-1].tool_name}」连续重复调用 {streak} 次"
115
+ + ("(已达熔断阈值,建议中止)" if streak >= self.circuit_break else "")))
116
+
117
+ if len(h) >= 4:
118
+ a, b = h[-1].signature, h[-2].signature
119
+ if a != b:
120
+ alt = 0
121
+ expect = [a, b]
122
+ for i, c in enumerate(reversed(h)):
123
+ if c.signature == expect[i % 2]:
124
+ alt += 1
125
+ else:
126
+ break
127
+ if alt >= self.warning:
128
+ return LoopVerdict(
129
+ stuck=True, level=self._level_for(alt),
130
+ detector="ping_pong", count=alt,
131
+ message=(f"工具「{h[-1].tool_name}」与「{h[-2].tool_name}」"
132
+ f"交替横跳 {alt} 次,无进展"))
133
+
134
+ return LoopVerdict()
135
+
136
+ def should_abort(self) -> bool:
137
+ """是否建议中止(达到熔断阈值)。复用最近一次 record 的检测结果。"""
138
+ with self._lock:
139
+ v = self._last_verdict or self._detect()
140
+ return v.stuck and v.count >= self.circuit_break
141
+
142
+ def reset(self):
143
+ with self._lock:
144
+ self.history.clear()
145
+ 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,114 @@
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 = 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
+ def geocode(self, address: str, city: str = "") -> Optional[tuple]:
44
+ """地址 → (lng, lat)。"""
45
+ data = self._get("geocode/geo", {"address": address, "city": city})
46
+ if data and data.get("geocodes"):
47
+ loc = data["geocodes"][0]["location"]
48
+ lng, lat = loc.split(",")
49
+ return float(lng), float(lat)
50
+ return None
51
+
52
+ def regeo(self, lng: float, lat: float) -> Optional[str]:
53
+ """坐标 → 地址。"""
54
+ data = self._get("geocode/regeo", {"location": f"{lng},{lat}"})
55
+ if data and data.get("regeocode"):
56
+ return data["regeocode"].get("formatted_address")
57
+ return None
58
+
59
+ def route(self, origin: str, destination: str, mode: str = "driving",
60
+ city: str = "") -> Optional[dict]:
61
+ """真实路线。origin/destination 为 'lng,lat'。
62
+ 返回 {distance_km, duration_min}。"""
63
+ path_map = {
64
+ "driving": "direction/driving",
65
+ "walking": "direction/walking",
66
+ "cycling": "direction/bicycling",
67
+ "transit": "direction/transit/integrated",
68
+ }
69
+ path = path_map.get(mode, "direction/driving")
70
+ params = {"origin": origin, "destination": destination}
71
+ if mode == "transit":
72
+ params["city"] = city
73
+ data = self._get(path, params)
74
+ if not data:
75
+ return None
76
+ try:
77
+ route = data["route"]
78
+ if mode == "transit":
79
+ seg = route["transits"][0]
80
+ return {"distance_km": round(int(seg["distance"]) / 1000, 2),
81
+ "duration_min": round(int(seg["duration"]) / 60, 1),
82
+ "source": "amap"}
83
+ else:
84
+ path0 = route["paths"][0]
85
+ return {"distance_km": round(int(path0["distance"]) / 1000, 2),
86
+ "duration_min": round(int(path0["duration"]) / 60, 1),
87
+ "source": "amap"}
88
+ except Exception:
89
+ return None
90
+
91
+ def search_poi(self, keywords: str, city: str = "", offset: int = 10) -> Optional[list]:
92
+ data = self._get("place/text",
93
+ {"keywords": keywords, "city": city, "offset": offset})
94
+ if data and data.get("pois"):
95
+ return [{"name": p["name"], "address": p.get("address", ""),
96
+ "location": p["location"], "type": p.get("type", "")}
97
+ for p in data["pois"]]
98
+ return None
99
+
100
+ def nearby(self, location: str, keywords: str = "", radius: int = 1000) -> Optional[list]:
101
+ data = self._get("place/around",
102
+ {"location": location, "keywords": keywords, "radius": radius})
103
+ if data and data.get("pois"):
104
+ return [{"name": p["name"], "address": p.get("address", ""),
105
+ "location": p["location"]} for p in data["pois"]]
106
+ return None
107
+
108
+ def weather(self, city: str) -> Optional[dict]:
109
+ data = self._get("weather/weatherInfo", {"city": city})
110
+ if data and data.get("lives"):
111
+ w = data["lives"][0]
112
+ return {"city": w["city"], "weather": w["weather"],
113
+ "temperature": w["temperature"], "wind": w["winddirection"]}
114
+ return None