foxclaw 0.1.2 → 0.1.4

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 (368) hide show
  1. package/index.js +1 -8
  2. package/package.json +1 -1
  3. package/python/.env.example +8 -0
  4. package/python/.github/workflows/ci.yml +47 -2
  5. package/python/.github/workflows/release.yml +5 -0
  6. package/python/CHANGELOG.md +100 -0
  7. package/python/CHANGELOG_CN.md +100 -0
  8. package/python/README.md +17 -2
  9. package/python/README_CN.md +38 -20
  10. package/python/bench/bm25_audit.py +1 -14
  11. package/python/bench/bm25_regression.py +0 -1
  12. package/python/bench/bm25_stress.py +0 -4
  13. package/python/bench/cd_audit.py +0 -23
  14. package/python/bench/chaos.py +6 -8
  15. package/python/bench/e2e_real_models.py +0 -1
  16. package/python/bench/llm_retry_test.py +2 -9
  17. package/python/bench/load_test.py +5 -8
  18. package/python/bench/vector_ivf_upgrade.py +1 -7
  19. package/python/docs/CONTENT_GUARD.md +189 -0
  20. package/python/docs/CONTENT_GUARD_CN.md +189 -0
  21. package/python/docs/RELEASE.md +78 -37
  22. package/python/docs/RUN_CN.md +2 -2
  23. package/python/evals/governance/cases.py +0 -37
  24. package/python/evals/governance/run_evals.py +5 -37
  25. package/python/examples/governance_middleware_demo.py +1 -6
  26. package/python/examples/langgraph_governance_demo.py +7 -11
  27. package/python/foxclaw/__init__.py +74 -152
  28. package/python/foxclaw/agents/base.py +82 -136
  29. package/python/foxclaw/agents/impl/_helpers.py +2 -10
  30. package/python/foxclaw/agents/impl/finance.py +4 -21
  31. package/python/foxclaw/agents/impl/office.py +7 -24
  32. package/python/foxclaw/agents/impl/supervisor.py +12 -79
  33. package/python/foxclaw/agents/mixins/_context.py +0 -23
  34. package/python/foxclaw/agents/mixins/augment.py +10 -16
  35. package/python/foxclaw/agents/mixins/llm.py +7 -6
  36. package/python/foxclaw/agents/mixins/memory.py +23 -3
  37. package/python/foxclaw/agents/mixins/preference.py +58 -8
  38. package/python/foxclaw/agents/mixins/prompt.py +59 -12
  39. package/python/foxclaw/agents/mixins/skillbox.py +6 -6
  40. package/python/foxclaw/agents/mixins/tool_exec.py +16 -42
  41. package/python/foxclaw/agents/rules/finance/RULES.md +131 -0
  42. package/python/foxclaw/api/__init__.py +18 -77
  43. package/python/foxclaw/api/_state.py +43 -0
  44. package/python/foxclaw/api/bootstrap.py +114 -0
  45. package/python/foxclaw/api/middleware.py +5 -28
  46. package/python/foxclaw/api/routes/ahs.py +60 -73
  47. package/python/foxclaw/api/routes/attachments.py +5 -6
  48. package/python/foxclaw/api/routes/browser.py +7 -11
  49. package/python/foxclaw/api/routes/domain.py +18 -26
  50. package/python/foxclaw/api/routes/knowledge.py +3 -21
  51. package/python/foxclaw/api/routes/llm_admin.py +21 -33
  52. package/python/foxclaw/api/routes/memory.py +50 -12
  53. package/python/foxclaw/api/routes/orchestration.py +3 -5
  54. package/python/foxclaw/api/routes/recovery.py +4 -3
  55. package/python/foxclaw/api/routes/review.py +2 -11
  56. package/python/foxclaw/api/routes/search.py +3 -7
  57. package/python/foxclaw/api/routes/system.py +33 -49
  58. package/python/foxclaw/api/routes/task.py +31 -49
  59. package/python/foxclaw/api/routes/ws.py +2 -6
  60. package/python/foxclaw/api/spine.py +1 -4
  61. package/python/foxclaw/behavior/__init__.py +20 -0
  62. package/python/foxclaw/behavior/behavior.yaml +62 -0
  63. package/python/foxclaw/behavior/spec.py +287 -0
  64. package/python/foxclaw/browser/action/action_executor.py +1 -5
  65. package/python/foxclaw/browser/action/action_types.py +0 -1
  66. package/python/foxclaw/browser/action/url_policy.py +1 -5
  67. package/python/foxclaw/browser/adapter/web_adapter.py +1 -1
  68. package/python/foxclaw/browser/driver/playwright_driver.py +20 -16
  69. package/python/foxclaw/browser/perception/dom_extractor.py +0 -1
  70. package/python/foxclaw/browser/perception/element_indexer.py +1 -2
  71. package/python/foxclaw/browser/service.py +7 -11
  72. package/python/foxclaw/capability/embedding_service.py +4 -6
  73. package/python/foxclaw/channels/__init__.py +9 -11
  74. package/python/foxclaw/channels/discord.py +4 -13
  75. package/python/foxclaw/channels/dispatcher.py +2 -27
  76. package/python/foxclaw/channels/feishu.py +10 -20
  77. package/python/foxclaw/channels/openclaw_bridge.py +4 -9
  78. package/python/foxclaw/channels/signing.py +9 -9
  79. package/python/foxclaw/channels/telegram.py +3 -4
  80. package/python/foxclaw/channels/weixin.py +7 -17
  81. package/python/foxclaw/cli.py +42 -13
  82. package/python/foxclaw/cognitive/reasoner.py +9 -10
  83. package/python/foxclaw/cognitive/state.py +18 -24
  84. package/python/foxclaw/core/audit_chain.py +1 -3
  85. package/python/foxclaw/core/audit_sink.py +3 -3
  86. package/python/foxclaw/core/compaction.py +8 -26
  87. package/python/foxclaw/core/config.py +2 -2
  88. package/python/foxclaw/core/confirm_nonce.py +6 -12
  89. package/python/foxclaw/core/content_guard.py +341 -0
  90. package/python/foxclaw/core/envfile.py +2 -2
  91. package/python/foxclaw/core/failure_policy.py +17 -26
  92. package/python/foxclaw/core/gateway.py +6 -10
  93. package/python/foxclaw/core/governance_audit.py +3 -7
  94. package/python/foxclaw/core/idempotency.py +15 -22
  95. package/python/foxclaw/core/injection_guard.py +10 -29
  96. package/python/foxclaw/core/memory_adapter.py +53 -60
  97. package/python/foxclaw/core/metrics.py +4 -5
  98. package/python/foxclaw/core/model_router.py +74 -150
  99. package/python/foxclaw/core/paths.py +1 -1
  100. package/python/foxclaw/core/recovery.py +21 -34
  101. package/python/foxclaw/core/scheduler.py +23 -26
  102. package/python/foxclaw/core/secrets.py +0 -3
  103. package/python/foxclaw/core/sensitive_write.py +283 -0
  104. package/python/foxclaw/core/sessions.py +4 -6
  105. package/python/foxclaw/core/settings.py +16 -32
  106. package/python/foxclaw/core/shared_write.py +11 -23
  107. package/python/foxclaw/core/steering_queue.py +12 -11
  108. package/python/foxclaw/core/system_bus.py +1 -1
  109. package/python/foxclaw/core/thought_chain.py +30 -32
  110. package/python/foxclaw/core/token_meter.py +9 -13
  111. package/python/foxclaw/core/tools.py +4 -12
  112. package/python/foxclaw/core/tracing.py +25 -40
  113. package/python/foxclaw/core/truth_pipeline.py +5 -9
  114. package/python/foxclaw/core/update_check.py +149 -0
  115. package/python/foxclaw/core/vec_recall_adapter.py +11 -37
  116. package/python/foxclaw/core/verify.py +6 -14
  117. package/python/foxclaw/core/web_search.py +4 -28
  118. package/python/foxclaw/finance/evidence.py +14 -18
  119. package/python/foxclaw/finance/fx.py +1 -3
  120. package/python/foxclaw/finance/graph.py +9 -18
  121. package/python/foxclaw/finance/models.py +20 -24
  122. package/python/foxclaw/finance/nlparse.py +12 -72
  123. package/python/foxclaw/finance/policy.py +8 -36
  124. package/python/foxclaw/finance/reasoner.py +3 -11
  125. package/python/foxclaw/finance/reasoning.py +13 -34
  126. package/python/foxclaw/finance/reconcile.py +5 -10
  127. package/python/foxclaw/finance/runtime.py +19 -46
  128. package/python/foxclaw/governance/middleware.py +13 -21
  129. package/python/foxclaw/harness/_base.py +2 -8
  130. package/python/foxclaw/harness/_policy.py +19 -38
  131. package/python/foxclaw/harness/drift.py +1 -13
  132. package/python/foxclaw/harness/event_bus.py +2 -5
  133. package/python/foxclaw/harness/harness.py +9 -97
  134. package/python/foxclaw/harness/loop_detection.py +11 -18
  135. package/python/foxclaw/itinerary/amap_client.py +2 -5
  136. package/python/foxclaw/itinerary/models.py +7 -7
  137. package/python/foxclaw/itinerary/reasoner.py +1 -6
  138. package/python/foxclaw/itinerary/reasoning.py +15 -21
  139. package/python/foxclaw/itinerary/runtime.py +8 -12
  140. package/python/foxclaw/kernel/capabilities.py +5 -6
  141. package/python/foxclaw/kernel/policy_gate.py +4 -10
  142. package/python/foxclaw/kernel/process_manager.py +17 -36
  143. package/python/foxclaw/kernel/scheduler.py +3 -9
  144. package/python/foxclaw/kernel/sentinel.py +13 -23
  145. package/python/foxclaw/kernel/sentinel_client.py +12 -15
  146. package/python/foxclaw/kernel/spine.py +9 -9
  147. package/python/foxclaw/knowledge/__init__.py +4 -20
  148. package/python/foxclaw/knowledge/truth_crystallizer.py +5 -8
  149. package/python/foxclaw/learning/knowledge_graph.py +4 -9
  150. package/python/foxclaw/learning/models.py +13 -13
  151. package/python/foxclaw/learning/reasoner.py +1 -4
  152. package/python/foxclaw/learning/runtime.py +19 -31
  153. package/python/foxclaw/learning/spaced_repetition.py +2 -7
  154. package/python/foxclaw/mcp_server.py +2 -3
  155. package/python/foxclaw/orchestration/__init__.py +16 -33
  156. package/python/foxclaw/pipeline/cognitive_pipeline.py +45 -97
  157. package/python/foxclaw/pipeline/stage_meta.py +21 -0
  158. package/python/foxclaw/pipeline/supervisor_stages.py +13 -42
  159. package/python/foxclaw/planning/llm_planner.py +3 -3
  160. package/python/foxclaw/planning/planner.py +6 -11
  161. package/python/foxclaw/search/agent_port.py +7 -11
  162. package/python/foxclaw/search/evidence.py +0 -4
  163. package/python/foxclaw/search/internal_search.py +3 -5
  164. package/python/foxclaw/search/models.py +11 -13
  165. package/python/foxclaw/search/planner.py +3 -3
  166. package/python/foxclaw/search/retriever.py +1 -1
  167. package/python/foxclaw/search/runtime.py +1 -8
  168. package/python/foxclaw/skills/__init__.py +46 -100
  169. package/python/foxclaw/skills/archive.py +111 -0
  170. package/python/foxclaw/skills/fetch.py +226 -0
  171. package/python/foxclaw/skills/hooks_spec.py +21 -25
  172. package/python/foxclaw/skills/install_cmd.py +216 -0
  173. package/python/foxclaw/skills/lifecycle.py +16 -42
  174. package/python/foxclaw/skills/markdown_skill.py +10 -9
  175. package/python/foxclaw/skills/mcp_client.py +5 -11
  176. package/python/foxclaw/skills/mcp_store.py +0 -3
  177. package/python/foxclaw/skills/permission.py +13 -48
  178. package/python/foxclaw/skills/pipeline.py +21 -53
  179. package/python/foxclaw/skills/policy.py +4 -10
  180. package/python/foxclaw/skills/review.py +3 -3
  181. package/python/frontend/deck/auth.js +0 -7
  182. package/python/frontend/deck/chat_model_picker.js +0 -54
  183. package/python/frontend/deck/domain.js +0 -20
  184. package/python/frontend/deck/main.js +0 -144
  185. package/python/frontend/deck/markdown.js +0 -4
  186. package/python/frontend/deck/settings.js +1 -18
  187. package/python/frontend/deck/state.js +0 -1
  188. package/python/frontend/deck/util.js +0 -2
  189. package/python/frontend/deck/visuals.js +0 -22
  190. package/python/frontend/planet/planet.js +18 -201
  191. package/python/frontend/v3/assets/index-JKfHlb-a.css +1 -0
  192. package/python/frontend/v3/assets/index-tN90dpUH.js +34 -0
  193. package/python/frontend/v3/index.html +3 -3
  194. package/python/frontend/widget_runtime.js +0 -43
  195. package/python/frontend/widgets.js +0 -57
  196. package/python/frontend_src/index.html +1 -1
  197. package/python/frontend_src/package-lock.json +2 -2
  198. package/python/frontend_src/package.json +1 -1
  199. package/python/frontend_src/src/App.vue +3 -7
  200. package/python/frontend_src/src/api.js +0 -7
  201. package/python/frontend_src/src/components/ChatDock.vue +27 -39
  202. package/python/frontend_src/src/components/DocViewer.vue +0 -9
  203. package/python/frontend_src/src/components/Inspector.vue +0 -6
  204. package/python/frontend_src/src/components/LeftRail.vue +60 -39
  205. package/python/frontend_src/src/components/MenuPopover.vue +9 -11
  206. package/python/frontend_src/src/components/MessageStream.vue +18 -30
  207. package/python/frontend_src/src/components/ModelPicker.vue +0 -5
  208. package/python/frontend_src/src/components/QuickIngest.vue +0 -6
  209. package/python/frontend_src/src/components/Stage.vue +71 -25
  210. package/python/frontend_src/src/components/ToolMenu.vue +0 -3
  211. package/python/frontend_src/src/components/TopBar.vue +38 -10
  212. package/python/frontend_src/src/components/panels/ActivityPanel.vue +0 -2
  213. package/python/frontend_src/src/components/panels/AgentsPanel.vue +2 -5
  214. package/python/frontend_src/src/components/panels/KnowledgePanel.vue +0 -16
  215. package/python/frontend_src/src/components/panels/MemoryPanel.vue +42 -3
  216. package/python/frontend_src/src/components/panels/PanelBox.vue +6 -7
  217. package/python/frontend_src/src/components/panels/ReviewPanel.vue +0 -16
  218. package/python/frontend_src/src/components/panels/SettingsPanel.vue +142 -38
  219. package/python/frontend_src/src/components/panels/TasksPanel.vue +0 -1
  220. package/python/frontend_src/src/components/panels/ToolsPanel.vue +125 -19
  221. package/python/frontend_src/src/lib/fmt.js +0 -4
  222. package/python/frontend_src/src/lib/i18n.js +53 -0
  223. package/python/frontend_src/src/lib/locales/en.js +173 -0
  224. package/python/frontend_src/src/lib/locales/zh.js +174 -0
  225. package/python/frontend_src/src/lib/md.js +0 -14
  226. package/python/frontend_src/src/main.js +2 -0
  227. package/python/frontend_src/src/stores/app.js +17 -32
  228. package/python/frontend_src/src/stores/chat.js +0 -34
  229. package/python/frontend_src/src/stores/model.js +0 -8
  230. package/python/frontend_src/src/stores/panels.js +16 -44
  231. package/python/frontend_src/src/stores/profile.js +31 -0
  232. package/python/frontend_src/src/stores/system.js +25 -0
  233. package/python/frontend_src/vite.config.js +0 -7
  234. package/python/pyproject.toml +17 -3
  235. package/python/requirements.txt +311 -0
  236. package/python/tests/conftest.py +0 -22
  237. package/python/tests/run_all.py +4 -66
  238. package/python/tests/test_agent_lifecycle.py +4 -38
  239. package/python/tests/test_agent_rules.py +81 -0
  240. package/python/tests/test_ahs_p2.py +4 -25
  241. package/python/tests/test_anthropic_tools.py +1 -15
  242. package/python/tests/test_api.py +19 -9
  243. package/python/tests/test_api_auth.py +1 -5
  244. package/python/tests/test_attachments.py +9 -25
  245. package/python/tests/test_audit_chain.py +2 -7
  246. package/python/tests/test_audit_sink.py +0 -6
  247. package/python/tests/test_behavior_spec.py +198 -0
  248. package/python/tests/test_browser.py +2 -6
  249. package/python/tests/test_browser_request_guard.py +0 -4
  250. package/python/tests/test_capabilities.py +0 -6
  251. package/python/tests/test_channel_signing.py +2 -9
  252. package/python/tests/test_channels.py +1 -11
  253. package/python/tests/test_config_layering.py +0 -4
  254. package/python/tests/test_confirm_nonce.py +1 -12
  255. package/python/tests/test_conflict_dismiss.py +143 -0
  256. package/python/tests/test_content_guard.py +177 -0
  257. package/python/tests/test_context_compaction_wiring.py +1 -7
  258. package/python/tests/test_dispatch.py +0 -6
  259. package/python/tests/test_domain_evals.py +11 -80
  260. package/python/tests/test_domain_import.py +0 -6
  261. package/python/tests/test_domain_reasoners.py +0 -14
  262. package/python/tests/test_e2e.py +0 -17
  263. package/python/tests/test_emb_cache.py +7 -17
  264. package/python/tests/test_embedding_singleton.py +1 -3
  265. package/python/tests/test_evals_smoke.py +0 -24
  266. package/python/tests/test_event_spine.py +10 -18
  267. package/python/tests/test_evidence_semantic.py +1 -14
  268. package/python/tests/test_evidence_sse.py +3 -6
  269. package/python/tests/test_file_ingest.py +0 -11
  270. package/python/tests/test_finance.py +0 -9
  271. package/python/tests/test_finance_ledger.py +5 -12
  272. package/python/tests/test_finance_persist.py +8 -12
  273. package/python/tests/test_gateway.py +3 -22
  274. package/python/tests/test_governance.py +5 -52
  275. package/python/tests/test_governance_audit.py +4 -39
  276. package/python/tests/test_governance_middleware.py +1 -7
  277. package/python/tests/test_hkc.py +6 -24
  278. package/python/tests/test_hkc_index_build_manager.py +0 -33
  279. package/python/tests/test_hkc_manifest_reuse.py +4 -27
  280. package/python/tests/test_hkc_search_event_sync.py +3 -54
  281. package/python/tests/test_import_path_guard.py +0 -6
  282. package/python/tests/test_injection_guard.py +18 -36
  283. package/python/tests/test_integration.py +5 -29
  284. package/python/tests/test_itinerary.py +0 -3
  285. package/python/tests/test_kee_semantic.py +0 -5
  286. package/python/tests/test_kernel.py +1 -11
  287. package/python/tests/test_learning.py +3 -12
  288. package/python/tests/test_llm_admin.py +4 -19
  289. package/python/tests/test_llm_planner.py +1 -23
  290. package/python/tests/test_markdown_skills.py +3 -14
  291. package/python/tests/test_markdown_skills_openclaw.py +4 -9
  292. package/python/tests/test_mcp_connectors.py +3 -15
  293. package/python/tests/test_mcp_guardian_discovery.py +2 -6
  294. package/python/tests/test_mcp_no_memory.py +3 -3
  295. package/python/tests/test_mcp_policy_gate.py +0 -1
  296. package/python/tests/test_mcp_server.py +6 -9
  297. package/python/tests/test_memory_recall.py +1 -4
  298. package/python/tests/test_metrics.py +0 -8
  299. package/python/tests/test_multi_chain.py +1 -13
  300. package/python/tests/test_multimodal.py +5 -18
  301. package/python/tests/test_negotiation.py +3 -13
  302. package/python/tests/test_orchestration.py +1 -11
  303. package/python/tests/test_orchestration_decoupled.py +0 -6
  304. package/python/tests/test_p3_p5_hardening.py +3 -17
  305. package/python/tests/test_parallel_dispatch.py +4 -12
  306. package/python/tests/test_pipeline.py +17 -58
  307. package/python/tests/test_plan_visible.py +4 -4
  308. package/python/tests/test_planner.py +0 -7
  309. package/python/tests/test_policy_gate.py +2 -3
  310. package/python/tests/test_profile.py +67 -0
  311. package/python/tests/test_readme_claims.py +163 -0
  312. package/python/tests/test_reasoner.py +2 -17
  313. package/python/tests/test_release_check.py +2 -13
  314. package/python/tests/test_resume_checkpoint.py +7 -15
  315. package/python/tests/test_routing.py +2 -12
  316. package/python/tests/test_scheduler.py +7 -10
  317. package/python/tests/test_search.py +1 -11
  318. package/python/tests/test_secrets.py +4 -10
  319. package/python/tests/test_semantic.py +1 -10
  320. package/python/tests/test_sensitive_write.py +181 -0
  321. package/python/tests/test_sessions.py +3 -4
  322. package/python/tests/test_settings.py +2 -18
  323. package/python/tests/test_shadow_learning.py +3 -43
  324. package/python/tests/test_shared_write.py +0 -7
  325. package/python/tests/test_skill_taint_gate.py +1 -10
  326. package/python/tests/test_skills.py +2 -18
  327. package/python/tests/test_skills_fetch.py +282 -0
  328. package/python/tests/test_stats_and_events.py +2 -5
  329. package/python/tests/test_streaming.py +2 -7
  330. package/python/tests/test_supervisor_pipeline.py +4 -5
  331. package/python/tests/test_supervisor_triage.py +4 -10
  332. package/python/tests/test_taint.py +2 -7
  333. package/python/tests/test_task_stream.py +1 -12
  334. package/python/tests/test_thinking_sse.py +0 -1
  335. package/python/tests/test_tool_center.py +1 -17
  336. package/python/tests/test_tool_parallel.py +2 -2
  337. package/python/tests/test_tool_params.py +1 -5
  338. package/python/tests/test_tool_use.py +0 -13
  339. package/python/tests/test_toplevel_concurrency.py +3 -4
  340. package/python/tests/test_tracing.py +7 -41
  341. package/python/tests/test_truth_crystallizer.py +4 -6
  342. package/python/tests/test_truth_flow.py +2 -7
  343. package/python/tests/test_update_check.py +95 -0
  344. package/python/tests/test_verify_evidence.py +8 -21
  345. package/python/tests/test_version_sync.py +35 -6
  346. package/python/tests/test_web_trigger.py +0 -17
  347. package/python/tools/build_content_lexicon.py +218 -0
  348. package/python/tools/bundle_web.py +98 -0
  349. package/python/tools/check_faiss_type.py +0 -1
  350. package/python/tools/check_log.ps1 +0 -8
  351. package/python/tools/mypy_baseline.txt +12 -4
  352. package/python/tools/mypy_ratchet.py +52 -6
  353. package/python/tools/pack.py +0 -5
  354. package/python/tools/recheck_conflicts.py +156 -0
  355. package/python/tools/release_check.py +24 -15
  356. package/python/tools/restart_foxclaw.ps1 +5 -8
  357. package/python/tools/shared_write_audit.py +1 -15
  358. package/python/tools/size_check.py +1 -1
  359. package/python/tools/strip_comments.py +228 -0
  360. package/python/vendor/hkc/hkc_api/routes/ability.py +31 -0
  361. package/python/vendor/hkc/hkc_api/schemas.py +11 -0
  362. package/python/vendor/hkc/hkc_kee/kee.py +124 -3
  363. package/python/vendor/hkc/hkc_sdk/base.py +22 -0
  364. package/python/vendor/hkc/hkc_sdk/direct_client.py +10 -0
  365. package/python/vendor/hkc/hkc_sdk/http_client.py +19 -0
  366. package/python/foxclaw/api/app.py +0 -123
  367. package/python/frontend/v3/assets/index-BD7VPbMV.js +0 -33
  368. package/python/frontend/v3/assets/index-CAvnYWGH.css +0 -1
@@ -22,8 +22,8 @@ from dataclasses import dataclass
22
22
  @dataclass
23
23
  class LoopVerdict:
24
24
  stuck: bool = False
25
- level: str = "ok" # ok / warning / critical
26
- detector: str = "" # repeat / ping_pong / no_progress
25
+ level: str = "ok"
26
+ detector: str = ""
27
27
  count: int = 0
28
28
  message: str = ""
29
29
 
@@ -35,9 +35,9 @@ class LoopVerdict:
35
35
 
36
36
  @dataclass
37
37
  class _ToolCall:
38
- signature: str # 工具名+参数的指纹
38
+ signature: str
39
39
  tool_name: str
40
- result_hash: str = "" # 结果指纹(判 no_progress)
40
+ result_hash: str = ""
41
41
 
42
42
 
43
43
  class ToolLoopDetector:
@@ -45,14 +45,11 @@ class ToolLoopDetector:
45
45
 
46
46
  def __init__(self, history_size: int = 30, warning: int = 6,
47
47
  critical: int = 12, circuit_break: int = 20):
48
- # 阈值单调递增保证(warning < critical < circuit_break)
49
48
  self.warning = max(2, warning)
50
49
  self.critical = max(self.warning + 1, critical)
51
50
  self.circuit_break = max(self.critical + 1, circuit_break)
52
51
  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)) 非原子,需锁(线程安全铁律)。
52
+ self._last_verdict: LoopVerdict | None = None
56
53
  self._lock = threading.RLock()
57
54
 
58
55
  @staticmethod
@@ -72,18 +69,19 @@ class ToolLoopDetector:
72
69
  s = str(result)[:500]
73
70
  return hashlib.sha256(s.encode()).hexdigest()[:16]
74
71
 
75
- def record(self, tool_name: str, args: dict = None, result=None) -> LoopVerdict:
72
+ def record(self, tool_name: str, args: dict | None = None, result=None) -> LoopVerdict:
76
73
  """记录一次工具调用并即时检测。返回 LoopVerdict。"""
77
74
  sig = self._signature(tool_name, args or {})
78
75
  rhash = self._result_sig(result) if result is not None else ""
79
- with self._lock: # append + _detect 必须原子(并发 record 会交错)
76
+ with self._lock:
80
77
  self.history.append(_ToolCall(sig, tool_name, rhash))
81
- self._last_verdict = self._detect()
82
- return self._last_verdict
78
+ verdict = self._detect()
79
+ self._last_verdict = verdict
80
+ return verdict
83
81
 
84
82
  def _level_for(self, count: int) -> str:
85
83
  if count >= self.circuit_break:
86
- return "critical" # 达熔断阈值仍按 critical 等级(由 stuck+建议中止体现)
84
+ return "critical"
87
85
  if count >= self.critical:
88
86
  return "critical"
89
87
  if count >= self.warning:
@@ -95,7 +93,6 @@ class ToolLoopDetector:
95
93
  if len(h) < self.warning:
96
94
  return LoopVerdict()
97
95
 
98
- # 1. repeat / no_progress:末尾连续相同签名
99
96
  last_sig = h[-1].signature
100
97
  streak = 0
101
98
  for c in reversed(h):
@@ -105,8 +102,6 @@ class ToolLoopDetector:
105
102
  break
106
103
  if streak >= self.warning:
107
104
  lvl = self._level_for(streak)
108
- # no_progress 优先:同签名重复 且 (有结果的那些)结果都相同
109
- # (只比对非空 result_hash,避免混合有/无 result 时漏判轮询卡死)
110
105
  streak_calls = h[-streak:]
111
106
  with_result = [c.result_hash for c in streak_calls if c.result_hash]
112
107
  if len(with_result) >= 2 and len(set(with_result)) == 1:
@@ -114,13 +109,11 @@ class ToolLoopDetector:
114
109
  stuck=True, level=lvl, detector="no_progress", count=streak,
115
110
  message=(f"工具「{h[-1].tool_name}」轮询 {streak} 次结果无变化"
116
111
  + ("(已达熔断阈值,建议中止)" if streak >= self.circuit_break else "")))
117
- # 否则按 repeat
118
112
  return LoopVerdict(
119
113
  stuck=True, level=lvl, detector="repeat", count=streak,
120
114
  message=(f"工具「{h[-1].tool_name}」连续重复调用 {streak} 次"
121
115
  + ("(已达熔断阈值,建议中止)" if streak >= self.circuit_break else "")))
122
116
 
123
- # 3. ping_pong:末尾两签名交替(A→B→A→B)
124
117
  if len(h) >= 4:
125
118
  a, b = h[-1].signature, h[-2].signature
126
119
  if a != b:
@@ -24,7 +24,7 @@ AMAP_BASE = "https://restapi.amap.com/v3"
24
24
  class AMapClient:
25
25
  """高德 Web Service 客户端。无 Key 则 enabled=False,调用返回 None。"""
26
26
 
27
- def __init__(self, api_key: str = None):
27
+ def __init__(self, api_key: str | None = None):
28
28
  self.api_key = api_key or os.environ.get("AMAP_MAPS_API_KEY")
29
29
  self.enabled = bool(self.api_key)
30
30
 
@@ -40,12 +40,11 @@ class AMapClient:
40
40
  except Exception:
41
41
  return None
42
42
 
43
- # ── 地理编码 ──────────────────────────────────────────
44
43
  def geocode(self, address: str, city: str = "") -> Optional[tuple]:
45
44
  """地址 → (lng, lat)。"""
46
45
  data = self._get("geocode/geo", {"address": address, "city": city})
47
46
  if data and data.get("geocodes"):
48
- loc = data["geocodes"][0]["location"] # "lng,lat"
47
+ loc = data["geocodes"][0]["location"]
49
48
  lng, lat = loc.split(",")
50
49
  return float(lng), float(lat)
51
50
  return None
@@ -57,7 +56,6 @@ class AMapClient:
57
56
  return data["regeocode"].get("formatted_address")
58
57
  return None
59
58
 
60
- # ── 路线规划 ──────────────────────────────────────────
61
59
  def route(self, origin: str, destination: str, mode: str = "driving",
62
60
  city: str = "") -> Optional[dict]:
63
61
  """真实路线。origin/destination 为 'lng,lat'。
@@ -90,7 +88,6 @@ class AMapClient:
90
88
  except Exception:
91
89
  return None
92
90
 
93
- # ── POI / 周边 / 天气 ─────────────────────────────────
94
91
  def search_poi(self, keywords: str, city: str = "", offset: int = 10) -> Optional[list]:
95
92
  data = self._get("place/text",
96
93
  {"keywords": keywords, "city": city, "offset": offset})
@@ -19,12 +19,12 @@ TravelMode = Literal["walking", "driving", "transit", "cycling"]
19
19
  @dataclass
20
20
  class Waypoint:
21
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 # 停留时长(分钟)
22
+ name: str
23
+ address: str = ""
24
+ lng: Optional[float] = None
25
+ lat: Optional[float] = None
26
+ start_time: Optional[str] = None
27
+ duration_min: int = 0
28
28
  city: str = ""
29
29
  note: str = ""
30
30
  id: str = field(default_factory=lambda: f"wp_{uuid.uuid4().hex[:10]}")
@@ -58,7 +58,7 @@ class RouteLeg:
58
58
  mode: TravelMode = "driving"
59
59
  distance_km: float = 0.0
60
60
  duration_min: float = 0.0
61
- source: Literal["estimated", "amap"] = "estimated" # 估算 or 高德真实
61
+ source: Literal["estimated", "amap"] = "estimated"
62
62
  note: str = ""
63
63
 
64
64
  def to_dict(self) -> dict:
@@ -14,8 +14,6 @@ from __future__ import annotations
14
14
  from ..cognitive.reasoner import DomainReasoner, ReasonedVerdict, Evidence
15
15
  from .reasoning import ItineraryReasoning
16
16
 
17
- # "紧凑"阈值:某段空档 / 所需通勤 < 该比例的倍数即算逼近(未赶不及但很赶),
18
- # 交给可选 LLM 复核是否提醒用户。仅在启用 LLM 时生效。
19
17
  TIGHT_RATIO = 1.5
20
18
 
21
19
 
@@ -43,12 +41,11 @@ class ItineraryConflictReasoner(DomainReasoner):
43
41
  def gather_evidence(self, subject, obs: dict) -> list:
44
42
  waypoints, mode = subject
45
43
  mode = obs["mode"]
46
- conflicts = ItineraryReasoning.detect_conflicts(waypoints, mode) # 规则=裁决证据
44
+ conflicts = ItineraryReasoning.detect_conflicts(waypoints, mode)
47
45
  ev = [Evidence(factor=c["type"], detail=c["reason"], weight=1.0, raw=c)
48
46
  for c in conflicts]
49
47
  if not conflicts:
50
48
  ev.append(Evidence("no_conflict", "未检测到时间冲突或通勤不可达", 0.5))
51
- # informational(raw=None):顺路簇/密度,丰满证据链但不参与裁决
52
49
  try:
53
50
  clusters = ItineraryReasoning.cluster_by_proximity(waypoints)
54
51
  if clusters:
@@ -65,11 +62,9 @@ class ItineraryConflictReasoner(DomainReasoner):
65
62
  def arbitrate(self, subject, obs, hyps, ev) -> ReasonedVerdict:
66
63
  triggered = [e for e in ev if e.raw is not None]
67
64
  if triggered:
68
- # 【规则权威】检出硬冲突 → confirm,绝不被 LLM 推翻/隐藏
69
65
  return ReasonedVerdict(
70
66
  "confirm", ";".join(e.detail for e in triggered),
71
67
  ev, hyps, "rules")
72
- # 无硬冲突:可选让 LLM 就"紧凑排程"复核是否升级(只 approve→confirm)
73
68
  if DomainReasoner.llm_available(self.llm, self.llm_enabled) and self._is_tight(subject, obs):
74
69
  if DomainReasoner.llm_should_escalate(self.llm, *self._llm_prompt(subject, obs, ev)):
75
70
  return ReasonedVerdict("confirm", "排程紧凑,LLM 建议人工确认", ev, hyps, "llm")
@@ -7,28 +7,29 @@ Foxclaw 行程子系统 — 推理引擎(行程 Agent 的大脑,纯算法)
7
7
  import math
8
8
  from datetime import timedelta
9
9
  from itertools import permutations
10
+ from typing import Any, cast
10
11
 
11
- from .models import Waypoint, RouteLeg
12
+ from .models import RouteLeg, TravelMode, Waypoint
12
13
 
13
14
 
14
- # 各出行方式的估算平均速度(km/h),接高德后用真实数据替换
15
15
  MODE_SPEED = {"walking": 5, "cycling": 15, "transit": 25, "driving": 35}
16
16
 
17
17
 
18
18
  class ItineraryReasoning:
19
19
  """行程推理:把地点+时间变成可决策的行程智能。"""
20
20
 
21
- # ── 距离与可达性(直线估算)──────────────────────────
22
21
  @staticmethod
23
22
  def haversine_km(a: Waypoint, b: Waypoint) -> float:
24
23
  """两点直线距离(km)。Haversine 公式。"""
25
- if not (a.has_coord and b.has_coord):
24
+ lat_a, lng_a = a.lat, a.lng
25
+ lat_b, lng_b = b.lat, b.lng
26
+ if lat_a is None or lng_a is None or lat_b is None or lng_b is None:
26
27
  return 0.0
27
28
  R = 6371.0
28
- dlat = math.radians(b.lat - a.lat)
29
- dlng = math.radians(b.lng - a.lng)
29
+ dlat = math.radians(lat_b - lat_a)
30
+ dlng = math.radians(lng_b - lng_a)
30
31
  x = (math.sin(dlat/2)**2 +
31
- math.cos(math.radians(a.lat)) * math.cos(math.radians(b.lat)) *
32
+ math.cos(math.radians(lat_a)) * math.cos(math.radians(lat_b)) *
32
33
  math.sin(dlng/2)**2)
33
34
  return R * 2 * math.atan2(math.sqrt(x), math.sqrt(1-x))
34
35
 
@@ -36,16 +37,16 @@ class ItineraryReasoning:
36
37
  def estimate_leg(a: Waypoint, b: Waypoint, mode: str = "driving") -> RouteLeg:
37
38
  """估算两点路线段(直线距离 × 1.3 绕路系数 / 速度)。"""
38
39
  straight = ItineraryReasoning.haversine_km(a, b)
39
- road_km = straight * 1.3 # 绕路系数
40
+ road_km = straight * 1.3
40
41
  speed = MODE_SPEED.get(mode, 35)
41
42
  duration = (road_km / speed) * 60 if speed else 0
42
43
  return RouteLeg(
43
44
  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
+ mode=cast(TravelMode, mode if mode in MODE_SPEED else "driving"),
46
+ distance_km=round(road_km, 2),
45
47
  duration_min=round(duration, 1), source="estimated",
46
48
  )
47
49
 
48
- # ── 路线优化(TSP)────────────────────────────────────
49
50
  @staticmethod
50
51
  def optimize_order(waypoints: list, mode: str = "driving",
51
52
  fixed_start: bool = True) -> dict:
@@ -61,10 +62,10 @@ class ItineraryReasoning:
61
62
  for i in range(len(seq)-1))
62
63
 
63
64
  if len(pts) <= 8:
64
- # 精确解
65
65
  start = pts[0] if fixed_start else None
66
66
  rest = pts[1:] if fixed_start else pts
67
- best, best_d = None, float("inf")
67
+ best: list = []
68
+ best_d = float("inf")
68
69
  for perm in permutations(rest):
69
70
  seq = ([start] + list(perm)) if start else list(perm)
70
71
  d = total_dist(seq)
@@ -73,7 +74,6 @@ class ItineraryReasoning:
73
74
  method = "exact"
74
75
  ordered = best
75
76
  else:
76
- # 最近邻启发式
77
77
  remaining = pts[1:]
78
78
  ordered = [pts[0]]
79
79
  while remaining:
@@ -83,7 +83,6 @@ class ItineraryReasoning:
83
83
  remaining.remove(nxt)
84
84
  method = "nearest_neighbor"
85
85
 
86
- # 计算总距离/时间
87
86
  legs = [ItineraryReasoning.estimate_leg(ordered[i], ordered[i+1], mode)
88
87
  for i in range(len(ordered)-1)]
89
88
  return {
@@ -95,17 +94,15 @@ class ItineraryReasoning:
95
94
  "method": method,
96
95
  }
97
96
 
98
- # ── 时间冲突 + 可达性 ─────────────────────────────────
99
97
  @staticmethod
100
98
  def detect_conflicts(waypoints: list, mode: str = "driving") -> list:
101
99
  """按时间排序的行程,检测:① 时间重叠 ② 通勤赶不及。"""
102
100
  timed = [w for w in waypoints if w.dt is not None]
103
101
  timed.sort(key=lambda w: w.dt)
104
- issues = []
102
+ issues: list[dict[str, Any]] = []
105
103
  for i in range(len(timed) - 1):
106
104
  cur, nxt = timed[i], timed[i+1]
107
105
  cur_end = cur.dt + timedelta(minutes=cur.duration_min)
108
- # 时间重叠
109
106
  if cur_end > nxt.dt:
110
107
  issues.append({
111
108
  "type": "time_conflict",
@@ -115,7 +112,6 @@ class ItineraryReasoning:
115
112
  f"晚于「{nxt.name}」开始({nxt.dt.strftime('%H:%M')})",
116
113
  })
117
114
  continue
118
- # 可达性:中间通勤时间够不够
119
115
  if cur.has_coord and nxt.has_coord:
120
116
  leg = ItineraryReasoning.estimate_leg(cur, nxt, mode)
121
117
  gap_min = (nxt.dt - cur_end).total_seconds() / 60
@@ -131,7 +127,6 @@ class ItineraryReasoning:
131
127
  })
132
128
  return issues
133
129
 
134
- # ── 地理聚类(顺路归组)───────────────────────────────
135
130
  @staticmethod
136
131
  def cluster_by_proximity(waypoints: list, radius_km: float = 3.0) -> list:
137
132
  """把相近的行程点聚成簇,建议"顺路一起办"。"""
@@ -149,9 +144,8 @@ class ItineraryReasoning:
149
144
  cluster.append(other)
150
145
  used.add(other.id)
151
146
  clusters.append([c.name for c in cluster])
152
- return [c for c in clusters if len(c) > 1] # 只返回有2+点的簇
147
+ return [c for c in clusters if len(c) > 1]
153
148
 
154
- # ── 行程密度 ──────────────────────────────────────────
155
149
  @staticmethod
156
150
  def density(waypoints: list, mode: str = "driving") -> dict:
157
151
  """行程密度:总点数、总通勤、跨城。"""
@@ -6,16 +6,17 @@ Foxclaw 行程子系统 — Itinerary Runtime
6
6
 
7
7
  import csv
8
8
  from pathlib import Path
9
+ from typing import cast
9
10
 
10
- from .models import Waypoint, ItineraryVerdict
11
- from .reasoning import ItineraryReasoning
11
+ from .models import ItineraryVerdict, TravelMode, Waypoint
12
+ from .reasoning import MODE_SPEED, ItineraryReasoning
12
13
  from .amap_client import AMapClient
13
14
 
14
15
 
15
16
  class ItineraryRuntime:
16
17
  """行政 Agent 的行程内核。"""
17
18
 
18
- def __init__(self, amap_key: str = None, hmr=None):
19
+ def __init__(self, amap_key: str | None = None, hmr=None):
19
20
  self.waypoints: list = []
20
21
  self.reasoning = ItineraryReasoning()
21
22
  self.amap = AMapClient(amap_key)
@@ -25,13 +26,11 @@ class ItineraryRuntime:
25
26
  def amap_enabled(self) -> bool:
26
27
  return self.amap.enabled
27
28
 
28
- # ── 采集行程点 ────────────────────────────────────────
29
- def add_waypoint(self, name: str, address: str = "", lng: float = None,
30
- lat: float = None, start_time: str = None,
29
+ def add_waypoint(self, name: str, address: str = "", lng: float | None = None,
30
+ lat: float | None = None, start_time: str | None = None,
31
31
  duration_min: int = 0, city: str = "") -> dict:
32
32
  wp = Waypoint(name=name, address=address, lng=lng, lat=lat,
33
33
  start_time=start_time, duration_min=duration_min, city=city)
34
- # 没坐标但有地址 → 尝试高德地理编码
35
34
  if not wp.has_coord and address and self.amap_enabled:
36
35
  geo = self.amap.geocode(address, city)
37
36
  if geo:
@@ -62,7 +61,6 @@ class ItineraryRuntime:
62
61
  count += 1
63
62
  return count
64
63
 
65
- # ── 真实路线(高德优先,否则估算)─────────────────────
66
64
  def real_leg(self, a: Waypoint, b: Waypoint, mode: str = "driving"):
67
65
  """两点路线:高德可用且有坐标时用真实数据,否则直线估算。"""
68
66
  if self.amap_enabled and a.has_coord and b.has_coord:
@@ -70,12 +68,12 @@ class ItineraryRuntime:
70
68
  if r:
71
69
  from .models import RouteLeg
72
70
  return RouteLeg(from_id=a.id, to_id=b.id, from_name=a.name,
73
- to_name=b.name, mode=mode,
71
+ to_name=b.name,
72
+ mode=cast(TravelMode, mode if mode in MODE_SPEED else "driving"),
74
73
  distance_km=r["distance_km"],
75
74
  duration_min=r["duration_min"], source="amap")
76
75
  return self.reasoning.estimate_leg(a, b, mode)
77
76
 
78
- # ── 综合分析(行政 Agent 核心输出)────────────────────
79
77
  def plan(self, mode: str = "driving", optimize: bool = True) -> dict:
80
78
  """完整行程规划:优化顺序 + 冲突检测 + 聚类 + 守门人裁决。"""
81
79
  opt = (self.reasoning.optimize_order(self.waypoints, mode)
@@ -95,7 +93,6 @@ class ItineraryRuntime:
95
93
  "map_data": self._to_amap_mapdata(opt),
96
94
  }
97
95
 
98
- # ── 行程守门人 ────────────────────────────────────────
99
96
  def _gatekeeper(self, conflicts, clusters, dens) -> list:
100
97
  verdicts = []
101
98
  for c in conflicts:
@@ -124,7 +121,6 @@ class ItineraryRuntime:
124
121
  payload={"cluster": cl}))
125
122
  return verdicts
126
123
 
127
- # ── 导出高德可视化格式(MapTaskData)──────────────────
128
124
  def _to_amap_mapdata(self, opt) -> list:
129
125
  """转成高德 MapTaskData(POI + route),可喂给高德生成可视化地图。"""
130
126
  data = []
@@ -25,8 +25,8 @@ class CapabilityRegistry:
25
25
  def __init__(self):
26
26
  self._caps: dict[str, Any] = {}
27
27
  self._lazy: dict[str, Callable[[], Any]] = {}
28
- self._lock = threading.RLock() # 只护 _caps/_lazy 表(快照,持锁时间极短)
29
- self._build_locks: dict[str, threading.Lock] = {} # name -> 构造锁(per-name)
28
+ self._lock = threading.RLock()
29
+ self._build_locks: dict[str, threading.Lock] = {}
30
30
 
31
31
  def register(self, name: str, handle: Any) -> None:
32
32
  """注册一个已构造好的能力实例(覆盖同名)。"""
@@ -38,7 +38,7 @@ class CapabilityRegistry:
38
38
  with self._lock:
39
39
  self._lazy[name] = factory
40
40
 
41
- def get(self, name: str, default: Any = None) -> Any:
41
+ def get(self, name: str, default: Any | None = None) -> Any:
42
42
  """解析能力。已注册直接返回;惰性则首次构造并缓存;都没有返回 default。
43
43
 
44
44
  【F1 修复】慢惰性构造(如 search_runtime 首建要装 HKC+载模型)**不持全局锁**,
@@ -54,12 +54,11 @@ class CapabilityRegistry:
54
54
  blk = self._build_locks.get(name)
55
55
  if blk is None:
56
56
  blk = self._build_locks[name] = threading.Lock()
57
- # 出全局锁,用 per-name 锁串行化"同一能力"的构造
58
57
  with blk:
59
58
  with self._lock:
60
- if name in self._caps: # 双检:构造期间可能已被别的线程建好
59
+ if name in self._caps:
61
60
  return self._caps[name]
62
- inst = factory() # 慢构造:不持全局锁,不阻塞其它能力解析
61
+ inst = factory()
63
62
  with self._lock:
64
63
  self._caps[name] = inst
65
64
  return inst
@@ -22,12 +22,10 @@ from ..core.governance_audit import new_decision_id
22
22
  @dataclass
23
23
  class Verdict:
24
24
  allowed: bool
25
- level: str # allow | warn | deny | confirm
25
+ level: str
26
26
  reason: str = ""
27
- source: str = "" # harness | permission
28
- raw: Any = None # 原始 HarnessResult / PermissionDecision(调用方按需取用)
29
- # 【P3-4】每次裁决的唯一 ID(P1-2 治理审计的关联键)。放最后+默认值:既有位置参数
30
- # 构造(如 Verdict(True,"allow",...,"permission",None))不受影响。
27
+ source: str = ""
28
+ raw: Any = None
31
29
  decision_id: str = field(default="")
32
30
 
33
31
 
@@ -35,9 +33,7 @@ class PolicyGate:
35
33
  def __init__(self, spine=None, action_evaluator: Optional[Callable] = None,
36
34
  auditor=None):
37
35
  self._spine = spine
38
- self._eval = action_evaluator # skills.permission.evaluate,注入(避免 kernel→skills 环)
39
- # 【P1-2】统一治理审计(可空:standalone 无审计)。鸭子类型注入,避免 kernel→core 之外
40
- # 的装配环;core.governance_audit.GovernanceAuditor 是唯一预期实现。
36
+ self._eval = action_evaluator
41
37
  self._auditor = auditor
42
38
 
43
39
  @property
@@ -45,7 +41,6 @@ class PolicyGate:
45
41
  """SkillAdapter 经此取审计器,把执行终态关联到裁决 decision_id(P3-4 跨层关联)。"""
46
42
  return self._auditor
47
43
 
48
- # ── 推理准入(Harness)──────────────────────────────────────
49
44
  def check_reasoning(self, harness, node) -> Verdict:
50
45
  """经统一门做推理漂移准入。底层仍是那一次 harness.check(同一 HarnessResult),
51
46
  Harness 自身已 emit harness_block/warn 事件,门面**不重复 emit**。行为一致。
@@ -64,7 +59,6 @@ class PolicyGate:
64
59
  "dominant_dim": getattr(r, "dominant_dim", "")})
65
60
  return v
66
61
 
67
- # ── 动作准入(skill-guard)──────────────────────────────────
68
62
  def check_action(self, skill, args: dict, rules=None,
69
63
  confirmed: bool = False, mode: str = "strict",
70
64
  taint_level: int = 0, taint_threshold: int = 0) -> Verdict:
@@ -16,16 +16,16 @@ import threading
16
16
  import time
17
17
  import uuid
18
18
  from dataclasses import dataclass, field
19
+ from typing import Optional
19
20
 
20
21
 
21
- # 合法状态与允许的流转(状态机约束:防止非法跳转)
22
22
  _VALID_TRANSITIONS = {
23
23
  "created": {"queued", "failed"},
24
24
  "queued": {"scheduled", "failed"},
25
25
  "scheduled": {"running", "failed"},
26
26
  "running": {"completed", "failed"},
27
- "completed": set(), # 终态
28
- "failed": set(), # 终态
27
+ "completed": set(),
28
+ "failed": set(),
29
29
  }
30
30
  _TERMINAL = {"completed", "failed"}
31
31
 
@@ -33,15 +33,15 @@ _TERMINAL = {"completed", "failed"}
33
33
  @dataclass
34
34
  class ProcessRecord:
35
35
  task: str
36
- agent_id: str = None
36
+ agent_id: Optional[str] = None
37
37
  state: str = "created"
38
38
  process_id: str = field(default_factory=lambda: f"proc_{uuid.uuid4().hex[:8]}")
39
39
  priority: float = 0.0
40
- route_hint: str = None
40
+ route_hint: Optional[str] = None
41
41
  created_at: float = field(default_factory=time.time)
42
42
  updated_at: float = field(default_factory=time.time)
43
43
  state_history: list = field(default_factory=list)
44
- fail_reason: str = None
44
+ fail_reason: Optional[str] = None
45
45
 
46
46
  def to_dict(self) -> dict:
47
47
  return {
@@ -58,21 +58,13 @@ class ProcessManager:
58
58
 
59
59
  def __init__(self, bus=None, history_limit: int = 1000):
60
60
  self._processes: dict[str, ProcessRecord] = {}
61
- self.bus = bus # 可选:状态变更发事件
61
+ self.bus = bus
62
62
  self._history_limit = history_limit
63
- # 【深审优化】活跃(非终态)进程数的 O(1) 增量计数:create 时 +1、进入终态时 -1
64
- # (集中在 _transition)。供 CognitiveState 每次 lifecycle 事件 O(1) 取用,
65
- # 免去原先每事件 stats() 遍历全部进程(O(n),n≤1000)。
66
63
  self._active = 0
67
- # 并行子 Agent 会让多个线程并发 create/mark_*/_evict 同一个 _processes dict,
68
- # 且状态机流转是"读 state → 校验 → 写 state"的非原子序列。RLock 保证:
69
- # · dict 增删与遍历(list/stats/_evict)不撞"dict changed size";
70
- # · 同一进程的并发二次流转不会都通过终态校验后重复 transition。
71
- # 可重入:_emit→bus.publish 的处理器可能回调 pm 的方法(同线程重入安全)。
72
64
  self._lock = threading.RLock()
73
65
 
74
- def create(self, task: str, agent_id: str = None,
75
- schedule: dict = None) -> ProcessRecord:
66
+ def create(self, task: str, agent_id: str | None = None,
67
+ schedule: dict | None = None) -> ProcessRecord:
76
68
  """创建进程(任务进入系统)。携带调度决策。"""
77
69
  rec = ProcessRecord(
78
70
  task=task, agent_id=agent_id,
@@ -81,36 +73,33 @@ class ProcessManager:
81
73
  )
82
74
  with self._lock:
83
75
  rec.state_history.append("created")
84
- # 创建后立即入队 → 已调度(调度决策此时已有)
85
76
  self._transition(rec, "queued")
86
77
  if schedule:
87
78
  self._transition(rec, "scheduled")
88
79
  self._processes[rec.process_id] = rec
89
- self._active += 1 # 新进程为活跃(此刻处于 queued/scheduled 非终态)
80
+ self._active += 1
90
81
  self._evict_if_needed()
91
82
  self._emit(rec)
92
83
  return rec
93
84
 
94
- def mark_running(self, process_id: str) -> ProcessRecord:
85
+ def mark_running(self, process_id: str) -> Optional[ProcessRecord]:
95
86
  with self._lock:
96
87
  rec = self._processes.get(process_id)
97
88
  if rec is None:
98
89
  return None
99
- # 若仍在 queued(create 未带 schedule),先补 scheduled 再 running
100
90
  if rec.state == "queued":
101
91
  self._transition(rec, "scheduled")
102
92
  return self._safe_transition(process_id, "running")
103
93
 
104
- def mark_completed(self, process_id: str) -> ProcessRecord:
94
+ def mark_completed(self, process_id: str) -> Optional[ProcessRecord]:
105
95
  with self._lock:
106
96
  return self._safe_transition(process_id, "completed")
107
97
 
108
- def mark_failed(self, process_id: str, reason: str = "") -> ProcessRecord:
98
+ def mark_failed(self, process_id: str, reason: str = "") -> Optional[ProcessRecord]:
109
99
  with self._lock:
110
100
  rec = self._processes.get(process_id)
111
101
  if rec is None:
112
102
  return None
113
- # 已是终态 → 幂等忽略(重试/并发下的二次标记不算错误,不崩溃)
114
103
  if rec.state in _TERMINAL:
115
104
  return rec
116
105
  self._transition(rec, "failed")
@@ -118,11 +107,11 @@ class ProcessManager:
118
107
  self._emit(rec)
119
108
  return rec
120
109
 
121
- def get(self, process_id: str) -> ProcessRecord:
110
+ def get(self, process_id: str) -> Optional[ProcessRecord]:
122
111
  with self._lock:
123
112
  return self._processes.get(process_id)
124
113
 
125
- def list(self, state: str = None, limit: int = 50) -> list:
114
+ def list(self, state: str | None = None, limit: int = 50) -> list:
126
115
  with self._lock:
127
116
  items = list(self._processes.values())
128
117
  if state:
@@ -138,21 +127,18 @@ class ProcessManager:
138
127
 
139
128
  def stats(self) -> dict:
140
129
  with self._lock:
141
- counts = {}
130
+ counts: dict[str, int] = {}
142
131
  for r in self._processes.values():
143
132
  counts[r.state] = counts.get(r.state, 0) + 1
144
133
  return {"total": len(self._processes), "by_state": counts,
145
134
  "active": self._active}
146
135
 
147
- # ── 内部 ──
148
- def _safe_transition(self, process_id: str, next_state: str) -> ProcessRecord:
136
+ def _safe_transition(self, process_id: str, next_state: str) -> Optional[ProcessRecord]:
149
137
  rec = self._processes.get(process_id)
150
138
  if rec is None:
151
139
  return None
152
- # 已是终态 → 幂等忽略(不崩溃)
153
140
  if rec.state in _TERMINAL:
154
141
  return rec
155
- # 已在目标状态 → 幂等忽略(重复标记不报错)
156
142
  if rec.state == next_state:
157
143
  return rec
158
144
  self._transition(rec, next_state)
@@ -167,15 +153,12 @@ class ProcessManager:
167
153
  新增调用点直接调本方法,活跃计数会被重复扣减(可扣成负数,stats 失真)。
168
154
  这里是双保险。注意行为变更:终态 → 任何状态,从抛 ValueError 变为静默忽略。
169
155
  """
170
- # 已是终态 → 幂等忽略(不再 -1,不再改状态)
171
156
  if rec.state in _TERMINAL:
172
157
  return
173
158
  allowed = _VALID_TRANSITIONS.get(rec.state, set())
174
159
  if next_state not in allowed:
175
160
  raise ValueError(
176
161
  f"非法状态流转: {rec.state} → {next_state}(进程 {rec.process_id})")
177
- # 进入终态 → 活跃计数 -1(每进程恰好 create 时 +1、首次入终态时 -1;
178
- # 上面的终态守卫保证不会重复扣减)。
179
162
  if next_state in _TERMINAL:
180
163
  self._active -= 1
181
164
  rec.state = next_state
@@ -183,8 +166,6 @@ class ProcessManager:
183
166
  rec.state_history.append(next_state)
184
167
 
185
168
  def _emit(self, rec: ProcessRecord):
186
- # 【v2 ① Step3·Lifecycle 统一】emit 唯一生命周期词汇 task.lifecycle.<state>
187
- # (取代旧 process.state_changed;Scheduler 委托本 pm,不再自发另一套词汇)。
188
169
  if self.bus is not None:
189
170
  try:
190
171
  self.bus.publish(f"task.lifecycle.{rec.state}", {