foxclaw 0.1.2 → 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 (344) 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/CHANGELOG.md +57 -0
  6. package/python/CHANGELOG_CN.md +57 -0
  7. package/python/README.md +11 -2
  8. package/python/README_CN.md +32 -20
  9. package/python/bench/bm25_audit.py +1 -14
  10. package/python/bench/bm25_regression.py +0 -1
  11. package/python/bench/bm25_stress.py +0 -4
  12. package/python/bench/cd_audit.py +0 -23
  13. package/python/bench/chaos.py +6 -8
  14. package/python/bench/e2e_real_models.py +0 -1
  15. package/python/bench/llm_retry_test.py +2 -9
  16. package/python/bench/load_test.py +5 -8
  17. package/python/bench/vector_ivf_upgrade.py +1 -7
  18. package/python/docs/RELEASE.md +4 -3
  19. package/python/docs/RUN_CN.md +2 -2
  20. package/python/evals/governance/cases.py +0 -37
  21. package/python/evals/governance/run_evals.py +5 -37
  22. package/python/examples/governance_middleware_demo.py +1 -6
  23. package/python/examples/langgraph_governance_demo.py +7 -11
  24. package/python/foxclaw/__init__.py +34 -152
  25. package/python/foxclaw/agents/base.py +65 -136
  26. package/python/foxclaw/agents/impl/_helpers.py +2 -10
  27. package/python/foxclaw/agents/impl/finance.py +4 -21
  28. package/python/foxclaw/agents/impl/office.py +7 -24
  29. package/python/foxclaw/agents/impl/supervisor.py +12 -79
  30. package/python/foxclaw/agents/mixins/_context.py +0 -23
  31. package/python/foxclaw/agents/mixins/augment.py +10 -16
  32. package/python/foxclaw/agents/mixins/llm.py +7 -6
  33. package/python/foxclaw/agents/mixins/memory.py +7 -1
  34. package/python/foxclaw/agents/mixins/preference.py +58 -8
  35. package/python/foxclaw/agents/mixins/prompt.py +0 -6
  36. package/python/foxclaw/agents/mixins/skillbox.py +6 -6
  37. package/python/foxclaw/agents/mixins/tool_exec.py +16 -42
  38. package/python/foxclaw/api/__init__.py +18 -77
  39. package/python/foxclaw/api/_state.py +43 -0
  40. package/python/foxclaw/api/{app.py → bootstrap.py} +5 -32
  41. package/python/foxclaw/api/middleware.py +5 -28
  42. package/python/foxclaw/api/routes/ahs.py +60 -73
  43. package/python/foxclaw/api/routes/attachments.py +5 -6
  44. package/python/foxclaw/api/routes/browser.py +7 -11
  45. package/python/foxclaw/api/routes/domain.py +18 -26
  46. package/python/foxclaw/api/routes/knowledge.py +3 -21
  47. package/python/foxclaw/api/routes/llm_admin.py +21 -33
  48. package/python/foxclaw/api/routes/memory.py +28 -12
  49. package/python/foxclaw/api/routes/orchestration.py +3 -5
  50. package/python/foxclaw/api/routes/recovery.py +4 -3
  51. package/python/foxclaw/api/routes/review.py +2 -11
  52. package/python/foxclaw/api/routes/search.py +3 -7
  53. package/python/foxclaw/api/routes/system.py +33 -49
  54. package/python/foxclaw/api/routes/task.py +31 -49
  55. package/python/foxclaw/api/routes/ws.py +2 -6
  56. package/python/foxclaw/api/spine.py +1 -4
  57. package/python/foxclaw/browser/action/action_executor.py +1 -5
  58. package/python/foxclaw/browser/action/action_types.py +0 -1
  59. package/python/foxclaw/browser/action/url_policy.py +1 -5
  60. package/python/foxclaw/browser/adapter/web_adapter.py +1 -1
  61. package/python/foxclaw/browser/driver/playwright_driver.py +20 -16
  62. package/python/foxclaw/browser/perception/dom_extractor.py +0 -1
  63. package/python/foxclaw/browser/perception/element_indexer.py +1 -2
  64. package/python/foxclaw/browser/service.py +7 -11
  65. package/python/foxclaw/capability/embedding_service.py +4 -6
  66. package/python/foxclaw/channels/__init__.py +9 -11
  67. package/python/foxclaw/channels/discord.py +4 -13
  68. package/python/foxclaw/channels/dispatcher.py +2 -27
  69. package/python/foxclaw/channels/feishu.py +10 -20
  70. package/python/foxclaw/channels/openclaw_bridge.py +4 -9
  71. package/python/foxclaw/channels/signing.py +9 -9
  72. package/python/foxclaw/channels/telegram.py +3 -4
  73. package/python/foxclaw/channels/weixin.py +7 -17
  74. package/python/foxclaw/cli.py +42 -13
  75. package/python/foxclaw/cognitive/reasoner.py +9 -10
  76. package/python/foxclaw/cognitive/state.py +18 -24
  77. package/python/foxclaw/core/audit_chain.py +1 -3
  78. package/python/foxclaw/core/audit_sink.py +3 -3
  79. package/python/foxclaw/core/compaction.py +8 -26
  80. package/python/foxclaw/core/config.py +2 -2
  81. package/python/foxclaw/core/confirm_nonce.py +6 -12
  82. package/python/foxclaw/core/envfile.py +2 -2
  83. package/python/foxclaw/core/failure_policy.py +17 -26
  84. package/python/foxclaw/core/gateway.py +6 -10
  85. package/python/foxclaw/core/governance_audit.py +3 -7
  86. package/python/foxclaw/core/idempotency.py +15 -22
  87. package/python/foxclaw/core/injection_guard.py +10 -29
  88. package/python/foxclaw/core/memory_adapter.py +30 -57
  89. package/python/foxclaw/core/metrics.py +4 -5
  90. package/python/foxclaw/core/model_router.py +36 -148
  91. package/python/foxclaw/core/paths.py +1 -1
  92. package/python/foxclaw/core/recovery.py +21 -34
  93. package/python/foxclaw/core/scheduler.py +23 -26
  94. package/python/foxclaw/core/secrets.py +0 -3
  95. package/python/foxclaw/core/sessions.py +4 -6
  96. package/python/foxclaw/core/settings.py +16 -32
  97. package/python/foxclaw/core/shared_write.py +11 -23
  98. package/python/foxclaw/core/steering_queue.py +12 -11
  99. package/python/foxclaw/core/system_bus.py +1 -1
  100. package/python/foxclaw/core/thought_chain.py +30 -32
  101. package/python/foxclaw/core/token_meter.py +9 -13
  102. package/python/foxclaw/core/tools.py +4 -12
  103. package/python/foxclaw/core/tracing.py +25 -40
  104. package/python/foxclaw/core/truth_pipeline.py +5 -9
  105. package/python/foxclaw/core/update_check.py +149 -0
  106. package/python/foxclaw/core/vec_recall_adapter.py +11 -37
  107. package/python/foxclaw/core/verify.py +6 -14
  108. package/python/foxclaw/core/web_search.py +4 -28
  109. package/python/foxclaw/finance/evidence.py +14 -18
  110. package/python/foxclaw/finance/fx.py +1 -3
  111. package/python/foxclaw/finance/graph.py +9 -18
  112. package/python/foxclaw/finance/models.py +20 -24
  113. package/python/foxclaw/finance/nlparse.py +12 -72
  114. package/python/foxclaw/finance/policy.py +8 -36
  115. package/python/foxclaw/finance/reasoner.py +3 -11
  116. package/python/foxclaw/finance/reasoning.py +13 -34
  117. package/python/foxclaw/finance/reconcile.py +5 -10
  118. package/python/foxclaw/finance/runtime.py +19 -46
  119. package/python/foxclaw/governance/middleware.py +13 -21
  120. package/python/foxclaw/harness/_base.py +2 -8
  121. package/python/foxclaw/harness/_policy.py +19 -38
  122. package/python/foxclaw/harness/drift.py +1 -13
  123. package/python/foxclaw/harness/event_bus.py +2 -5
  124. package/python/foxclaw/harness/harness.py +9 -97
  125. package/python/foxclaw/harness/loop_detection.py +11 -18
  126. package/python/foxclaw/itinerary/amap_client.py +2 -5
  127. package/python/foxclaw/itinerary/models.py +7 -7
  128. package/python/foxclaw/itinerary/reasoner.py +1 -6
  129. package/python/foxclaw/itinerary/reasoning.py +15 -21
  130. package/python/foxclaw/itinerary/runtime.py +8 -12
  131. package/python/foxclaw/kernel/capabilities.py +5 -6
  132. package/python/foxclaw/kernel/policy_gate.py +4 -10
  133. package/python/foxclaw/kernel/process_manager.py +17 -36
  134. package/python/foxclaw/kernel/scheduler.py +3 -9
  135. package/python/foxclaw/kernel/sentinel.py +13 -23
  136. package/python/foxclaw/kernel/sentinel_client.py +12 -15
  137. package/python/foxclaw/kernel/spine.py +9 -9
  138. package/python/foxclaw/knowledge/__init__.py +4 -20
  139. package/python/foxclaw/knowledge/truth_crystallizer.py +5 -8
  140. package/python/foxclaw/learning/knowledge_graph.py +4 -9
  141. package/python/foxclaw/learning/models.py +13 -13
  142. package/python/foxclaw/learning/reasoner.py +1 -4
  143. package/python/foxclaw/learning/runtime.py +19 -31
  144. package/python/foxclaw/learning/spaced_repetition.py +2 -7
  145. package/python/foxclaw/mcp_server.py +2 -3
  146. package/python/foxclaw/orchestration/__init__.py +16 -33
  147. package/python/foxclaw/pipeline/cognitive_pipeline.py +45 -97
  148. package/python/foxclaw/pipeline/stage_meta.py +21 -0
  149. package/python/foxclaw/pipeline/supervisor_stages.py +13 -42
  150. package/python/foxclaw/planning/llm_planner.py +3 -3
  151. package/python/foxclaw/planning/planner.py +6 -11
  152. package/python/foxclaw/search/agent_port.py +7 -11
  153. package/python/foxclaw/search/evidence.py +0 -4
  154. package/python/foxclaw/search/internal_search.py +3 -5
  155. package/python/foxclaw/search/models.py +11 -13
  156. package/python/foxclaw/search/planner.py +3 -3
  157. package/python/foxclaw/search/retriever.py +1 -1
  158. package/python/foxclaw/search/runtime.py +1 -8
  159. package/python/foxclaw/skills/__init__.py +46 -100
  160. package/python/foxclaw/skills/archive.py +111 -0
  161. package/python/foxclaw/skills/fetch.py +226 -0
  162. package/python/foxclaw/skills/hooks_spec.py +21 -25
  163. package/python/foxclaw/skills/install_cmd.py +216 -0
  164. package/python/foxclaw/skills/lifecycle.py +16 -42
  165. package/python/foxclaw/skills/markdown_skill.py +10 -9
  166. package/python/foxclaw/skills/mcp_client.py +5 -11
  167. package/python/foxclaw/skills/mcp_store.py +0 -3
  168. package/python/foxclaw/skills/permission.py +13 -48
  169. package/python/foxclaw/skills/pipeline.py +21 -53
  170. package/python/foxclaw/skills/policy.py +4 -10
  171. package/python/foxclaw/skills/review.py +3 -3
  172. package/python/frontend/deck/auth.js +0 -7
  173. package/python/frontend/deck/chat_model_picker.js +0 -54
  174. package/python/frontend/deck/domain.js +0 -20
  175. package/python/frontend/deck/main.js +0 -144
  176. package/python/frontend/deck/markdown.js +0 -4
  177. package/python/frontend/deck/settings.js +1 -18
  178. package/python/frontend/deck/state.js +0 -1
  179. package/python/frontend/deck/util.js +0 -2
  180. package/python/frontend/deck/visuals.js +0 -22
  181. package/python/frontend/planet/planet.js +0 -201
  182. package/python/frontend/v3/assets/index-CQSBdxk4.js +34 -0
  183. package/python/frontend/v3/assets/index-D5nIsjms.css +1 -0
  184. package/python/frontend/v3/index.html +3 -3
  185. package/python/frontend/widget_runtime.js +0 -43
  186. package/python/frontend/widgets.js +0 -57
  187. package/python/frontend_src/index.html +1 -1
  188. package/python/frontend_src/package-lock.json +2 -2
  189. package/python/frontend_src/package.json +1 -1
  190. package/python/frontend_src/src/App.vue +3 -7
  191. package/python/frontend_src/src/api.js +0 -7
  192. package/python/frontend_src/src/components/ChatDock.vue +27 -39
  193. package/python/frontend_src/src/components/DocViewer.vue +0 -9
  194. package/python/frontend_src/src/components/Inspector.vue +0 -6
  195. package/python/frontend_src/src/components/LeftRail.vue +60 -39
  196. package/python/frontend_src/src/components/MenuPopover.vue +9 -11
  197. package/python/frontend_src/src/components/MessageStream.vue +18 -30
  198. package/python/frontend_src/src/components/ModelPicker.vue +0 -5
  199. package/python/frontend_src/src/components/QuickIngest.vue +0 -6
  200. package/python/frontend_src/src/components/Stage.vue +71 -25
  201. package/python/frontend_src/src/components/ToolMenu.vue +0 -3
  202. package/python/frontend_src/src/components/TopBar.vue +38 -10
  203. package/python/frontend_src/src/components/panels/ActivityPanel.vue +0 -2
  204. package/python/frontend_src/src/components/panels/AgentsPanel.vue +2 -5
  205. package/python/frontend_src/src/components/panels/KnowledgePanel.vue +0 -16
  206. package/python/frontend_src/src/components/panels/MemoryPanel.vue +0 -1
  207. package/python/frontend_src/src/components/panels/PanelBox.vue +6 -7
  208. package/python/frontend_src/src/components/panels/ReviewPanel.vue +0 -16
  209. package/python/frontend_src/src/components/panels/SettingsPanel.vue +142 -38
  210. package/python/frontend_src/src/components/panels/TasksPanel.vue +0 -1
  211. package/python/frontend_src/src/components/panels/ToolsPanel.vue +125 -19
  212. package/python/frontend_src/src/lib/fmt.js +0 -4
  213. package/python/frontend_src/src/lib/i18n.js +53 -0
  214. package/python/frontend_src/src/lib/locales/en.js +173 -0
  215. package/python/frontend_src/src/lib/locales/zh.js +174 -0
  216. package/python/frontend_src/src/lib/md.js +0 -14
  217. package/python/frontend_src/src/main.js +2 -0
  218. package/python/frontend_src/src/stores/app.js +17 -32
  219. package/python/frontend_src/src/stores/chat.js +0 -34
  220. package/python/frontend_src/src/stores/model.js +0 -8
  221. package/python/frontend_src/src/stores/panels.js +0 -41
  222. package/python/frontend_src/src/stores/profile.js +31 -0
  223. package/python/frontend_src/src/stores/system.js +25 -0
  224. package/python/frontend_src/vite.config.js +0 -7
  225. package/python/pyproject.toml +6 -3
  226. package/python/requirements.txt +311 -0
  227. package/python/tests/conftest.py +0 -22
  228. package/python/tests/run_all.py +3 -66
  229. package/python/tests/test_agent_lifecycle.py +4 -38
  230. package/python/tests/test_ahs_p2.py +4 -25
  231. package/python/tests/test_anthropic_tools.py +1 -15
  232. package/python/tests/test_api.py +19 -9
  233. package/python/tests/test_api_auth.py +1 -5
  234. package/python/tests/test_attachments.py +9 -25
  235. package/python/tests/test_audit_chain.py +2 -7
  236. package/python/tests/test_audit_sink.py +0 -6
  237. package/python/tests/test_browser.py +2 -6
  238. package/python/tests/test_browser_request_guard.py +0 -4
  239. package/python/tests/test_capabilities.py +0 -6
  240. package/python/tests/test_channel_signing.py +2 -9
  241. package/python/tests/test_channels.py +1 -11
  242. package/python/tests/test_config_layering.py +0 -4
  243. package/python/tests/test_confirm_nonce.py +1 -12
  244. package/python/tests/test_context_compaction_wiring.py +1 -7
  245. package/python/tests/test_dispatch.py +0 -6
  246. package/python/tests/test_domain_evals.py +11 -80
  247. package/python/tests/test_domain_import.py +0 -6
  248. package/python/tests/test_domain_reasoners.py +0 -14
  249. package/python/tests/test_e2e.py +0 -17
  250. package/python/tests/test_emb_cache.py +7 -17
  251. package/python/tests/test_embedding_singleton.py +1 -3
  252. package/python/tests/test_evals_smoke.py +0 -24
  253. package/python/tests/test_event_spine.py +10 -18
  254. package/python/tests/test_evidence_semantic.py +1 -14
  255. package/python/tests/test_evidence_sse.py +3 -6
  256. package/python/tests/test_file_ingest.py +0 -11
  257. package/python/tests/test_finance.py +0 -9
  258. package/python/tests/test_finance_ledger.py +5 -12
  259. package/python/tests/test_finance_persist.py +8 -12
  260. package/python/tests/test_gateway.py +3 -22
  261. package/python/tests/test_governance.py +5 -52
  262. package/python/tests/test_governance_audit.py +4 -39
  263. package/python/tests/test_governance_middleware.py +1 -7
  264. package/python/tests/test_hkc.py +6 -24
  265. package/python/tests/test_hkc_index_build_manager.py +0 -33
  266. package/python/tests/test_hkc_manifest_reuse.py +4 -27
  267. package/python/tests/test_hkc_search_event_sync.py +3 -54
  268. package/python/tests/test_import_path_guard.py +0 -6
  269. package/python/tests/test_injection_guard.py +18 -36
  270. package/python/tests/test_integration.py +5 -29
  271. package/python/tests/test_itinerary.py +0 -3
  272. package/python/tests/test_kee_semantic.py +0 -5
  273. package/python/tests/test_kernel.py +1 -11
  274. package/python/tests/test_learning.py +3 -12
  275. package/python/tests/test_llm_admin.py +4 -19
  276. package/python/tests/test_llm_planner.py +1 -23
  277. package/python/tests/test_markdown_skills.py +3 -14
  278. package/python/tests/test_markdown_skills_openclaw.py +4 -9
  279. package/python/tests/test_mcp_connectors.py +3 -15
  280. package/python/tests/test_mcp_guardian_discovery.py +2 -6
  281. package/python/tests/test_mcp_no_memory.py +3 -3
  282. package/python/tests/test_mcp_policy_gate.py +0 -1
  283. package/python/tests/test_mcp_server.py +6 -9
  284. package/python/tests/test_memory_recall.py +1 -4
  285. package/python/tests/test_metrics.py +0 -8
  286. package/python/tests/test_multi_chain.py +1 -13
  287. package/python/tests/test_multimodal.py +5 -18
  288. package/python/tests/test_negotiation.py +3 -13
  289. package/python/tests/test_orchestration.py +1 -11
  290. package/python/tests/test_orchestration_decoupled.py +0 -6
  291. package/python/tests/test_p3_p5_hardening.py +3 -17
  292. package/python/tests/test_parallel_dispatch.py +4 -12
  293. package/python/tests/test_pipeline.py +17 -58
  294. package/python/tests/test_plan_visible.py +4 -4
  295. package/python/tests/test_planner.py +0 -7
  296. package/python/tests/test_policy_gate.py +2 -3
  297. package/python/tests/test_profile.py +67 -0
  298. package/python/tests/test_readme_claims.py +163 -0
  299. package/python/tests/test_reasoner.py +2 -17
  300. package/python/tests/test_release_check.py +2 -13
  301. package/python/tests/test_resume_checkpoint.py +7 -15
  302. package/python/tests/test_routing.py +2 -12
  303. package/python/tests/test_scheduler.py +7 -10
  304. package/python/tests/test_search.py +1 -11
  305. package/python/tests/test_secrets.py +4 -10
  306. package/python/tests/test_semantic.py +1 -10
  307. package/python/tests/test_sessions.py +3 -4
  308. package/python/tests/test_settings.py +2 -18
  309. package/python/tests/test_shadow_learning.py +3 -43
  310. package/python/tests/test_shared_write.py +0 -7
  311. package/python/tests/test_skill_taint_gate.py +1 -10
  312. package/python/tests/test_skills.py +2 -18
  313. package/python/tests/test_skills_fetch.py +282 -0
  314. package/python/tests/test_stats_and_events.py +2 -5
  315. package/python/tests/test_streaming.py +2 -7
  316. package/python/tests/test_supervisor_pipeline.py +4 -5
  317. package/python/tests/test_supervisor_triage.py +4 -10
  318. package/python/tests/test_taint.py +2 -7
  319. package/python/tests/test_task_stream.py +1 -12
  320. package/python/tests/test_thinking_sse.py +0 -1
  321. package/python/tests/test_tool_center.py +1 -17
  322. package/python/tests/test_tool_parallel.py +2 -2
  323. package/python/tests/test_tool_params.py +1 -5
  324. package/python/tests/test_tool_use.py +0 -13
  325. package/python/tests/test_toplevel_concurrency.py +3 -4
  326. package/python/tests/test_tracing.py +7 -41
  327. package/python/tests/test_truth_crystallizer.py +4 -6
  328. package/python/tests/test_truth_flow.py +2 -7
  329. package/python/tests/test_update_check.py +95 -0
  330. package/python/tests/test_verify_evidence.py +8 -21
  331. package/python/tests/test_version_sync.py +35 -6
  332. package/python/tests/test_web_trigger.py +0 -17
  333. package/python/tools/check_faiss_type.py +0 -1
  334. package/python/tools/check_log.ps1 +0 -8
  335. package/python/tools/mypy_baseline.txt +12 -4
  336. package/python/tools/mypy_ratchet.py +52 -6
  337. package/python/tools/pack.py +0 -5
  338. package/python/tools/release_check.py +7 -7
  339. package/python/tools/restart_foxclaw.ps1 +5 -8
  340. package/python/tools/shared_write_audit.py +1 -15
  341. package/python/tools/size_check.py +1 -1
  342. package/python/tools/strip_comments.py +228 -0
  343. package/python/frontend/v3/assets/index-BD7VPbMV.js +0 -33
  344. package/python/frontend/v3/assets/index-CAvnYWGH.css +0 -1
@@ -21,17 +21,18 @@ import threading
21
21
  import time
22
22
  import uuid
23
23
  from dataclasses import dataclass, field
24
+ from typing import Any
24
25
 
25
26
 
26
27
  @dataclass
27
28
  class SteeringItem:
28
29
  item_id: str
29
- requester: str # 谁在等这个结果(主任务/会话标识)
30
- source_agent: str # 哪个子 Agent 产出的
31
- payload: dict # 结果内容
30
+ requester: str
31
+ source_agent: str
32
+ payload: dict
32
33
  created: float = field(default_factory=time.time)
33
- leased_until: float = 0.0 # 0 = 未租出;>0 = 租约到期时间戳
34
- lease_id: str = "" # 当前租约持有者标识
34
+ leased_until: float = 0.0
35
+ lease_id: str = ""
35
36
 
36
37
  @property
37
38
  def is_leased(self) -> bool:
@@ -60,7 +61,7 @@ class SteeringQueue:
60
61
  source_agent=source_agent, payload=payload)
61
62
  return item_id
62
63
 
63
- def lease(self, requester: str = None, max_items: int = 10) -> list[SteeringItem]:
64
+ def lease(self, requester: str | None = None, max_items: int = 10) -> list[SteeringItem]:
64
65
  """租走一批待处理结果(可按 requester 过滤)。
65
66
 
66
67
  原子地:挑出未被租用(或租约已过期)的项,标记为 leased,返回。
@@ -69,7 +70,7 @@ class SteeringQueue:
69
70
  with self._lock:
70
71
  self._reclaim_expired()
71
72
  now = time.time()
72
- leased = []
73
+ leased: list[Any] = []
73
74
  lease_id = uuid.uuid4().hex[:12]
74
75
  for item in self._items.values():
75
76
  if len(leased) >= max_items:
@@ -83,7 +84,7 @@ class SteeringQueue:
83
84
  leased.append(item)
84
85
  return leased
85
86
 
86
- def ack(self, item_id: str, lease_id: str = None) -> bool:
87
+ def ack(self, item_id: str, lease_id: str | None = None) -> bool:
87
88
  """消费者确认处理完 → 移除该项。
88
89
 
89
90
  校验 lease_id:只有持有当前租约的消费者能 ack(防止过期租约误删
@@ -94,7 +95,7 @@ class SteeringQueue:
94
95
  if item is None:
95
96
  return False
96
97
  if lease_id is not None and item.lease_id != lease_id:
97
- return False # 租约不匹配(可能已过期被别人租走)
98
+ return False
98
99
  del self._items[item_id]
99
100
  return True
100
101
 
@@ -114,7 +115,7 @@ class SteeringQueue:
114
115
  item.leased_until = 0.0
115
116
  item.lease_id = ""
116
117
 
117
- def pending_count(self, requester: str = None) -> int:
118
+ def pending_count(self, requester: str | None = None) -> int:
118
119
  """待处理(未被有效租用)的项数。"""
119
120
  with self._lock:
120
121
  self._reclaim_expired()
@@ -145,7 +146,7 @@ class SteeringQueue:
145
146
  if item.requester != requester:
146
147
  continue
147
148
  if item.is_leased:
148
- continue # 正被租用 → 留给租约消费者,drain 不碰
149
+ continue
149
150
  out.append(item.payload)
150
151
  to_del.append(item_id)
151
152
  for item_id in to_del:
@@ -73,7 +73,7 @@ class SystemBus:
73
73
  def publish(self, channel: str, payload: dict):
74
74
  return self.spine.publish(channel, payload, source="system_bus")
75
75
 
76
- def history(self, channel: str = None, limit: int = 50,
76
+ def history(self, channel: str | None = None, limit: int = 50,
77
77
  exact: bool = False) -> list:
78
78
  """历史事件(dict 列表)。exact=True 时只返回 topic 完全匹配的事件
79
79
  (默认前缀匹配,与 EventSpine 原生一致)。"""
@@ -16,16 +16,16 @@ from pathlib import Path
16
16
 
17
17
 
18
18
  NodeType = Literal[
19
- "goal", # 目标基线节点(根)
20
- "reasoning", # 推理步骤
21
- "memory_retrieval", # 记忆检索
22
- "tool_intent", # 工具意图
23
- "tool_use", # 工具实际调用(augment.py 已在用;此前漏登记,类型面是欠账)
24
- "plan", # 显式计划(steps/flow 落在 metadata,供回显与审计)
25
- "decision", # 行动决策
26
- "policy_update", # Policy 候选更新
27
- "consolidation", # 记忆巩固写回
28
- "reflection", # 执行后反思
19
+ "goal",
20
+ "reasoning",
21
+ "memory_retrieval",
22
+ "tool_intent",
23
+ "tool_use",
24
+ "plan",
25
+ "decision",
26
+ "policy_update",
27
+ "consolidation",
28
+ "reflection",
29
29
  ]
30
30
 
31
31
  HarnessStatus = Literal["BASELINE", "PASS", "WARN", "BLOCK", "PENDING", "REGRESSION"]
@@ -39,15 +39,15 @@ class ThoughtNode:
39
39
  agent_id: str
40
40
  type: NodeType
41
41
  content: str
42
- conf: float # 置信度 0.0-1.0
43
- mem_refs: list[str] # 引用的 HMR 记忆 ID
44
- parent_ids: list[str] # 父节点 ID(因果依赖)
42
+ conf: float
43
+ mem_refs: list[str]
44
+ parent_ids: list[str]
45
45
  harness_status: HarnessStatus
46
- harness_reason: Optional[str] # BLOCK/WARN 时的原因
47
- harness_delta: Optional[float] # 目标漂移量
48
- dominant_dim: Optional[str] # 主导漂移维度
49
- tool_name: Optional[str] # 如果是 tool_intent 节点
50
- entities: list[str] # 涉及的实体集合
46
+ harness_reason: Optional[str]
47
+ harness_delta: Optional[float]
48
+ dominant_dim: Optional[str]
49
+ tool_name: Optional[str]
50
+ entities: list[str]
51
51
  timestamp: str
52
52
  metadata: dict
53
53
 
@@ -59,11 +59,11 @@ class ThoughtNode:
59
59
  type: NodeType,
60
60
  content: str,
61
61
  conf: float = 1.0,
62
- mem_refs: list[str] = None,
63
- parent_ids: list[str] = None,
64
- tool_name: str = None,
65
- entities: list[str] = None,
66
- metadata: dict = None,
62
+ mem_refs: list[str] | None = None,
63
+ parent_ids: list[str] | None = None,
64
+ tool_name: str | None = None,
65
+ entities: list[str] | None = None,
66
+ metadata: dict | None = None,
67
67
  ) -> "ThoughtNode":
68
68
  return cls(
69
69
  id=f"tn_{uuid.uuid4().hex[:12]}",
@@ -150,7 +150,7 @@ class ThoughtChain:
150
150
  try:
151
151
  conn.execute("PRAGMA journal_mode=WAL")
152
152
  except sqlite3.OperationalError:
153
- pass # 个别文件系统不支持 WAL(如网络盘)→ 退回默认日志模式,timeout 仍生效
153
+ pass
154
154
  try:
155
155
  yield conn
156
156
  conn.commit()
@@ -183,9 +183,9 @@ class ThoughtChain:
183
183
  self,
184
184
  node_id: str,
185
185
  status: HarnessStatus,
186
- reason: str = None,
187
- delta: float = None,
188
- dominant_dim: str = None,
186
+ reason: str | None = None,
187
+ delta: float | None = None,
188
+ dominant_dim: str | None = None,
189
189
  ):
190
190
  """Harness 校验完成后更新节点标注"""
191
191
  with self._conn() as conn:
@@ -202,7 +202,7 @@ class ThoughtChain:
202
202
  ).fetchone()
203
203
  return self._row_to_node(row) if row else None
204
204
 
205
- def get_chain(self, session_id: str, agent_id: str = None) -> list[ThoughtNode]:
205
+ def get_chain(self, session_id: str, agent_id: str | None = None) -> list[ThoughtNode]:
206
206
  """获取一个会话的完整推理链,按时间排序"""
207
207
  with self._conn() as conn:
208
208
  if agent_id:
@@ -217,7 +217,7 @@ class ThoughtChain:
217
217
  ).fetchall()
218
218
  return [self._row_to_node(r) for r in rows]
219
219
 
220
- def get_blocked_nodes(self, agent_id: str = None, limit: int = 50) -> list[ThoughtNode]:
220
+ def get_blocked_nodes(self, agent_id: str | None = None, limit: int = 50) -> list[ThoughtNode]:
221
221
  """获取被 Harness 截断的节点,用于审计"""
222
222
  with self._conn() as conn:
223
223
  if agent_id:
@@ -232,7 +232,7 @@ class ThoughtChain:
232
232
  ).fetchall()
233
233
  return [self._row_to_node(r) for r in rows]
234
234
 
235
- def search_nodes(self, keyword: str, agent_id: str = None,
235
+ def search_nodes(self, keyword: str, agent_id: str | None = None,
236
236
  limit: int = 20) -> list[ThoughtNode]:
237
237
  """按关键词在推理节点内容里检索(LIKE 子串匹配)。供 HSR 的 AgentSourcePort
238
238
  把「各 Agent 想过/判过什么」连通进内部搜索。只读。"""
@@ -307,8 +307,6 @@ class ThoughtChain:
307
307
  "timestamp", "metadata"
308
308
  ]
309
309
  d = dict(zip(cols, row))
310
- # JSON 字段容错:单行某字段损坏时降级为默认值,而非让整个审计链读取崩溃。
311
- # 审计系统的可靠性要求"一条坏数据不拖垮全部"。
312
310
  for field, default in (("mem_refs", []), ("parent_ids", []),
313
311
  ("entities", []), ("metadata", {})):
314
312
  try:
@@ -36,7 +36,6 @@ def estimate_tokens(text: str, model: str = "") -> int:
36
36
  enc = tiktoken.get_encoding("cl100k_base")
37
37
  return len(enc.encode(text))
38
38
  except Exception:
39
- # 字符估算:中文(非ASCII)按 1.1 字符/token,英文(ASCII)按 4 字符/token。
40
39
  ascii_chars = sum(1 for c in text if ord(c) < 128)
41
40
  cjk_chars = len(text) - ascii_chars
42
41
  return int(ascii_chars / 4 + cjk_chars * 1.1) + 1
@@ -50,8 +49,8 @@ class CallRecord:
50
49
  agent_id: str
51
50
  input_tokens: int
52
51
  output_tokens: int
53
- estimated: bool # True=估算,False=API真实usage
54
- task_kind: str = "" # 可选:single/multi/...
52
+ estimated: bool
53
+ task_kind: str = ""
55
54
 
56
55
  @property
57
56
  def total_tokens(self) -> int:
@@ -66,14 +65,14 @@ class CallRecord:
66
65
  class TokenMeter:
67
66
  """Token 计量器。线程安全,可聚合,可落盘。"""
68
67
 
69
- def __init__(self, log_path: str = None, max_records: int = 10000,
68
+ def __init__(self, log_path: str | None = None, max_records: int = 10000,
70
69
  max_log_mb: float = 10.0):
71
70
  self.log_path = log_path
72
71
  self.max_records = max_records
73
72
  self.max_log_bytes = int(max_log_mb * 1024 * 1024)
74
73
  self._records: list[CallRecord] = []
75
74
  self._lock = threading.RLock()
76
- self._append_count = 0 # 触发轮转检查的计数器
75
+ self._append_count = 0
77
76
  if log_path:
78
77
  os.makedirs(os.path.dirname(log_path) or ".", exist_ok=True)
79
78
 
@@ -94,7 +93,7 @@ class TokenMeter:
94
93
 
95
94
  def measure_call(self, *, system: str, messages: list, output: str,
96
95
  provider: str, model: str, agent_id: str = "",
97
- usage: dict = None, task_kind: str = "") -> CallRecord:
96
+ usage: dict | None = None, task_kind: str = "") -> CallRecord:
98
97
  """计量一次调用。优先用 API 返回的 usage,否则估算。
99
98
 
100
99
  usage 形如 {"input_tokens": N, "output_tokens": M}(已由 router 归一化)。
@@ -104,7 +103,6 @@ class TokenMeter:
104
103
  usage.get("input_tokens", 0),
105
104
  usage.get("output_tokens", 0),
106
105
  estimated=False, task_kind=task_kind)
107
- # 估算:输入 = system + 所有 message content,输出 = output
108
106
  input_text = (system or "") + "\n".join(
109
107
  str(m.get("content", "")) for m in (messages or []))
110
108
  in_tok = estimate_tokens(input_text, model)
@@ -112,7 +110,6 @@ class TokenMeter:
112
110
  return self.record(provider, model, agent_id, in_tok, out_tok,
113
111
  estimated=True, task_kind=task_kind)
114
112
 
115
- # ── 聚合查询 ──────────────────────────────────────────
116
113
  def summary(self) -> dict:
117
114
  """总体统计。"""
118
115
  with self._lock:
@@ -211,8 +208,7 @@ class TokenMeter:
211
208
  with open(self.log_path, "a", encoding="utf-8") as f:
212
209
  f.write(json.dumps(rec.to_dict(), ensure_ascii=False) + "\n")
213
210
  except Exception:
214
- pass # 计量落盘失败不影响主流程
215
- # 每 100 次追加检查一次文件大小,超限则轮转(防磁盘无限膨胀)
211
+ pass
216
212
  self._append_count += 1
217
213
  if self._append_count % 100 == 0:
218
214
  self._rotate_if_needed()
@@ -226,10 +222,10 @@ class TokenMeter:
226
222
  return
227
223
  with open(self.log_path, encoding="utf-8") as f:
228
224
  lines = f.readlines()
229
- keep = lines[len(lines) // 2:] # 保留较新的一半
225
+ keep = lines[len(lines) // 2:]
230
226
  tmp = self.log_path + ".tmp"
231
227
  with open(tmp, "w", encoding="utf-8") as f:
232
228
  f.writelines(keep)
233
- os.replace(tmp, self.log_path) # 原子替换
229
+ os.replace(tmp, self.log_path)
234
230
  except Exception:
235
- pass # 轮转失败不影响主流程
231
+ pass
@@ -15,6 +15,7 @@ Foxclaw — 工具注册表(ToolRegistry / P2 function-calling)
15
15
  """
16
16
  from __future__ import annotations
17
17
  import os
18
+ from typing import Any
18
19
 
19
20
 
20
21
  def _tool(name: str, description: str, properties: dict, required: list) -> dict:
@@ -33,7 +34,6 @@ def _tool(name: str, description: str, properties: dict, required: list) -> dict
33
34
  }
34
35
 
35
36
 
36
- # ── 首批工具声明(高价值、正好替掉现有关键词坑)──────────────────────────────
37
37
  WEB_SEARCH = _tool(
38
38
  "web_search",
39
39
  "联网搜索实时/时效性的**外部**信息:新闻、产品的版本与型号、发布动态、价格、汇率、"
@@ -113,15 +113,15 @@ class ToolRegistry:
113
113
  for k in required:
114
114
  if k not in args or args[k] in (None, ""):
115
115
  errs.append(f"缺少必填参数 {k}")
116
- _tmap = {"string": str, "integer": int, "number": (int, float),
116
+ _tmap: dict[str, Any] = {"string": str, "integer": int, "number": (int, float),
117
117
  "boolean": bool, "array": list, "object": dict}
118
118
  for k, v in args.items():
119
119
  spec = props.get(k)
120
120
  if not isinstance(spec, dict):
121
121
  continue
122
- want = _tmap.get(spec.get("type"))
122
+ stype = spec.get("type")
123
+ want = _tmap.get(stype) if isinstance(stype, str) else None
123
124
  if not want or not isinstance(v, want):
124
- # bool 是 int 的子类,须排除,否则 True 会被当成合法 integer
125
125
  if not (want is not bool and isinstance(v, bool)):
126
126
  errs.append(f"参数 {k} 类型应为 {spec.get('type')},"
127
127
  f"实为 {type(v).__name__}")
@@ -143,20 +143,12 @@ def default_registry() -> ToolRegistry:
143
143
  return ToolRegistry().register(WEB_SEARCH).register(RECALL_MEMORY)
144
144
 
145
145
 
146
- # ── 工具能力分级(供 permission.evaluate 四阶段管线判定)────────────────────────
147
- # 病根提醒:prompt 级隔离挡不住弱模型被块内强指令诱导(实测 flash ~2/3 被劫持),真正的
148
- # 安全阀在**动作级**——工具执行前过权限管线,写/副作用/不可逆动作走人在回路(HITL)。
149
- # 字段对齐 permission.evaluate:side_effect(read/write/send/pay)/ read_only / external / destructive。
150
- # parallel_safe 不在权限语义里,只用于**同轮多工具是否可并发**:置 True 表示"纯读、可与其他
151
- # parallel_safe 工具同时跑"(见 agents/mixins/tool_exec.py::_run_tools)。未声明 = 串行。
152
- # **新增任何工具都应在此声明能力;未声明者 fail-closed 当作有副作用 → 强制 HITL(见 _DEFAULT_CAP)。**
153
146
  TOOL_CAPABILITIES: dict[str, dict] = {
154
147
  "web_search": {"side_effect": "read", "read_only": True, "external": False,
155
148
  "destructive": False, "parallel_safe": True},
156
149
  "recall_memory": {"side_effect": "read", "read_only": True, "external": False,
157
150
  "destructive": False, "parallel_safe": True},
158
151
  }
159
- # 未声明工具的兜底:当有副作用 + 外部 → permission 管线判 ask → 强制人在回路。宁误拦不漏放。
160
152
  _DEFAULT_CAP = {"side_effect": "write", "read_only": False, "external": True, "destructive": False}
161
153
 
162
154
 
@@ -29,14 +29,11 @@ import contextvars
29
29
  from collections import deque
30
30
  from contextlib import contextmanager
31
31
  from . import settings
32
+ from typing import Any
32
33
 
33
34
  logger = logging.getLogger(__name__)
34
35
 
35
36
 
36
- # ── 采样策略 ────────────────────────────────────────────────
37
- # 内存环存只留最近 N 条、重启即失 → 查不了"昨天那次慢请求"。持久化解决它,但不能无脑全存:
38
- # 正常请求的 trace 数量大、价值低。故:**慢的 / 出错的必存**(那才是你要查的),
39
- # 其余按采样率(默认 0 —— 个人 OS 留下的每条都值得看)。
40
37
  def _slow_ms() -> float:
41
38
  return float(settings.get_safe("FOXCLAW_TRACE_SLOW_MS"))
42
39
 
@@ -50,14 +47,9 @@ def _retain() -> int:
50
47
 
51
48
  _current: contextvars.ContextVar = contextvars.ContextVar("foxclaw_trace", default=None)
52
49
 
53
- MAX_SPANS_PER_TRACE = 200 # 单条 trace 的 span 上限,防病态循环把内存撑爆
50
+ MAX_SPANS_PER_TRACE = 200
54
51
 
55
52
 
56
- # ── W3C Trace Context(零依赖跨进程传播)────────────────────────
57
- # 为什么不上 opentelemetry-sdk:Foxclaw 是单进程 + local-first,为它装 SDK/exporter/grpc、
58
- # 再要求跑个 Jaeger,代价远大于收益。但它确实有一条真实跨进程边界——**MCP 双向**。
59
- # traceparent 是 W3C 标准、纯字符串、零依赖,恰好覆盖这条边界:
60
- # 00-{trace_id:32hex}-{span_id:16hex}-{flags:2hex}
61
53
  _TP_RE = __import__("re").compile(r"^00-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$")
62
54
 
63
55
 
@@ -69,12 +61,12 @@ def parse_traceparent(header: str):
69
61
  if not m:
70
62
  return None
71
63
  tid = m.group(1)
72
- if tid == "0" * 32: # 全零非法(规范要求)
64
+ if tid == "0" * 32:
73
65
  return None
74
66
  return tid
75
67
 
76
68
 
77
- def to_traceparent(trace_id: str, span_id: str = None) -> str:
69
+ def to_traceparent(trace_id: str, span_id: str | None = None) -> str:
78
70
  """给出站请求生成 traceparent。span_id 缺省随机(本进程作为对端的 parent)。"""
79
71
  tid = (trace_id or uuid.uuid4().hex)[:32].rjust(32, "0")
80
72
  sid = (span_id or uuid.uuid4().hex[:16])[:16].rjust(16, "0")
@@ -88,13 +80,12 @@ def current_traceparent():
88
80
 
89
81
 
90
82
  class Trace:
91
- def __init__(self, name: str, trace_id: str = None):
92
- # 32 hex:直接符合 W3C traceparent 的 trace-id 规格(跨进程可对齐)
83
+ def __init__(self, name: str, trace_id: str | None = None):
93
84
  self.trace_id = trace_id or uuid.uuid4().hex
94
85
  self.name = name
95
86
  self.start_ts = time.time()
96
87
  self._t0 = time.perf_counter()
97
- self.duration_ms = None
88
+ self.duration_ms: float | None = None
98
89
  self.spans: list = []
99
90
  self._lock = threading.Lock()
100
91
  self._dropped = 0
@@ -135,9 +126,8 @@ class TraceStore:
135
126
  def __init__(self, limit: int = 200):
136
127
  self._d: deque = deque(maxlen=limit)
137
128
  self._lock = threading.Lock()
138
- self._db_path = None
129
+ self._db_path: str | None = None
139
130
 
140
- # ── 持久化 ───────────────────────────────────────────
141
131
  def attach_db(self, path):
142
132
  """开启持久化并建表。连接**即建即弃、不跨线程复用**(与 ThoughtChain 同款):
143
133
  SQLite 连接不可跨线程共享,而 trace 写入发生在请求线程/to_thread 工作线程。"""
@@ -154,19 +144,21 @@ class TraceStore:
154
144
  self._db_path = None
155
145
 
156
146
  def _conn(self):
147
+ if self._db_path is None:
148
+ raise RuntimeError("trace 持久化未启用:先 attach_db")
157
149
  conn = sqlite3.connect(self._db_path, timeout=5.0)
158
150
  try:
159
- conn.execute("PRAGMA journal_mode=WAL") # 单写多读,降锁竞争
151
+ conn.execute("PRAGMA journal_mode=WAL")
160
152
  except sqlite3.OperationalError:
161
153
  pass
162
154
  return conn
163
155
 
164
156
  def _should_persist(self, tr: Trace) -> bool:
165
157
  if tr.has_error():
166
- return True # 出错的必留
158
+ return True
167
159
  if (tr.duration_ms or 0) >= _slow_ms():
168
- return True # 慢的必留(正是要查的)
169
- return random.random() < _sample_rate() # 其余按采样率(默认 0)
160
+ return True
161
+ return random.random() < _sample_rate()
170
162
 
171
163
  def _persist(self, tr: Trace):
172
164
  d = tr.to_dict()
@@ -176,7 +168,6 @@ class TraceStore:
176
168
  (tr.trace_id, tr.name, tr.start_ts, tr.duration_ms,
177
169
  d["span_count"], 0 if tr.has_error() else 1,
178
170
  json.dumps(d["spans"], ensure_ascii=False)))
179
- # 保留上限:超出删最旧 → 有界增长(不会把磁盘吃光)
180
171
  c.execute("DELETE FROM traces WHERE trace_id IN ("
181
172
  "SELECT trace_id FROM traces ORDER BY start_ts DESC "
182
173
  "LIMIT -1 OFFSET ?)", (_retain(),))
@@ -187,10 +178,10 @@ class TraceStore:
187
178
  with self._lock:
188
179
  self._d.append(tr)
189
180
  if self._db_path and self._should_persist(tr):
190
- self._persist(tr) # 锁外落库(慢 I/O 不挡内存环)
181
+ self._persist(tr)
191
182
  try:
192
- OTLP.submit(tr) # 可选导出:未配 endpoint 即 no-op;
193
- except Exception: # 队列满则丢,绝不阻塞请求
183
+ OTLP.submit(tr)
184
+ except Exception:
194
185
  pass
195
186
 
196
187
  def get(self, trace_id: str):
@@ -219,7 +210,7 @@ class TraceStore:
219
210
  "duration_ms": row[3], "span_count": row[4],
220
211
  "spans": json.loads(row[5] or "[]"), "persisted": True}
221
212
 
222
- def query(self, limit: int = 30, slow_ms: float = None, errors_only: bool = False):
213
+ def query(self, limit: int = 30, slow_ms: float | None = None, errors_only: bool = False):
223
214
  """查已落库的历史 trace(内存环之外)。这才是"找出昨天那次慢请求"的入口。"""
224
215
  if not self._db_path:
225
216
  return []
@@ -250,10 +241,6 @@ class TraceStore:
250
241
  self._d.clear()
251
242
 
252
243
 
253
- # ── OTLP/HTTP-JSON 导出(可选、零依赖)────────────────────────
254
- # 想接 Jaeger/Tempo/collector 的人设 FOXCLAW_OTLP_ENDPOINT 即可;不设则完全不启用、零成本。
255
- # 刻意**不引 opentelemetry-sdk**:OTLP 有 HTTP+JSON 传输,stdlib urllib 就能发。
256
- # 铁律:遥测**绝不能拖慢请求** —— 后台守护线程 + 有界队列,满了就丢(宁丢遥测不拖请求)。
257
244
  def _otlp_endpoint() -> str:
258
245
  return os.environ.get("FOXCLAW_OTLP_ENDPOINT", "").strip().rstrip("/")
259
246
 
@@ -272,16 +259,16 @@ def _trace_to_otlp(tr: "Trace") -> dict:
272
259
  root_id = uuid.uuid4().hex[:16]
273
260
  dur = (tr.duration_ms or 0.0) / 1000.0
274
261
  spans = [{
275
- "traceId": tr.trace_id, "spanId": root_id, "name": tr.name, "kind": 2, # SERVER
262
+ "traceId": tr.trace_id, "spanId": root_id, "name": tr.name, "kind": 2,
276
263
  "startTimeUnixNano": t_ns(tr.start_ts),
277
264
  "endTimeUnixNano": t_ns(tr.start_ts + dur),
278
265
  "status": {"code": 2 if tr.has_error() else 1},
279
266
  }]
280
267
  for s in tr.to_dict()["spans"]:
281
- st = s.get("ts", tr.start_ts) # 缺 ts(手工 add_span)→ 兜底
268
+ st = s.get("ts", tr.start_ts)
282
269
  spans.append({
283
270
  "traceId": tr.trace_id, "spanId": uuid.uuid4().hex[:16], "parentSpanId": root_id,
284
- "name": s.get("name", "span"), "kind": 1, # INTERNAL
271
+ "name": s.get("name", "span"), "kind": 1,
285
272
  "startTimeUnixNano": t_ns(st),
286
273
  "endTimeUnixNano": t_ns(st + (s.get("ms", 0.0) / 1000.0)),
287
274
  "attributes": _attrs(s.get("meta")),
@@ -298,8 +285,8 @@ class _OtlpExporter:
298
285
 
299
286
  def __init__(self, maxsize: int = 512):
300
287
  import queue
301
- self._q = queue.Queue(maxsize=maxsize)
302
- self._th = None
288
+ self._q: queue.Queue[Any] = queue.Queue(maxsize=maxsize)
289
+ self._th: threading.Thread | None = None
303
290
  self._lock = threading.Lock()
304
291
  self.dropped = 0
305
292
  self.sent = 0
@@ -319,7 +306,7 @@ class _OtlpExporter:
319
306
  try:
320
307
  self._q.put_nowait(tr)
321
308
  except queue.Full:
322
- self.dropped += 1 # 宁可丢遥测,也不让导出拖住请求线程
309
+ self.dropped += 1
323
310
 
324
311
  def _run(self):
325
312
  import urllib.request
@@ -338,7 +325,7 @@ class _OtlpExporter:
338
325
  self.sent += 1
339
326
  except Exception as e:
340
327
  self.failed += 1
341
- if self.failed <= 3: # 限量告警,别把日志刷爆
328
+ if self.failed <= 3:
342
329
  logger.warning("OTLP 导出失败(不影响请求): %s", e)
343
330
 
344
331
  def stats(self) -> dict:
@@ -351,7 +338,7 @@ OTLP = _OtlpExporter()
351
338
  TRACES = TraceStore()
352
339
 
353
340
 
354
- def start_trace(name: str, trace_id: str = None) -> Trace:
341
+ def start_trace(name: str, trace_id: str | None = None) -> Trace:
355
342
  """开一条 trace 并绑到当前上下文(由 API 中间件按请求调用)。"""
356
343
  tr = Trace(name, trace_id)
357
344
  _current.set(tr)
@@ -375,8 +362,6 @@ def span(name: str, **meta):
375
362
  yield None
376
363
  return
377
364
  t0 = time.perf_counter()
378
- # ts:span 开始的 epoch 秒。内部只需 ms 就够,但 OTLP 要求每个 span 有 start/end 绝对时刻,
379
- # 故一并记下(开销可忽略;缺失时导出侧会优雅兜底)。
380
365
  rec = {"name": name, "ts": time.time(), "ok": True,
381
366
  **({"meta": meta} if meta else {})}
382
367
  try:
@@ -16,6 +16,7 @@
16
16
  """
17
17
  from __future__ import annotations
18
18
  from datetime import datetime
19
+ from typing import Any
19
20
 
20
21
 
21
22
  class TruthPipeline:
@@ -34,13 +35,10 @@ class TruthPipeline:
34
35
 
35
36
  def __init__(self, bus, hmr=None):
36
37
  self.bus = bus
37
- self.hmr = hmr # 真实 HMR 记忆后端(可空,空则跳过记忆沉淀)
38
+ self.hmr = hmr
38
39
  self.last_trace = None
39
40
  self.last_memory_update = None
40
41
 
41
- # 订阅事件链。【P4-1】三个都是精确 channel 名 → exact=True:
42
- # 门面化后 SystemBus 默认是前缀匹配,不锁定的话 "execution.completed.extra"
43
- # 之类的前缀事件也会触发真值链(语义漂移)。
44
42
  bus.subscribe("execution.completed", self._on_execution_completed, exact=True)
45
43
  bus.subscribe("trace.recorded", self._on_trace_recorded, exact=True)
46
44
  bus.subscribe("truth.appended", self._on_truth_appended, exact=True)
@@ -72,7 +70,6 @@ class TruthPipeline:
72
70
  self.last_memory_update = {"persisted": False, "reason": "blocked"}
73
71
  self.bus.publish("truth.skipped", {**p, "reason": "blocked"})
74
72
  return
75
- # 真值 = "任务被如此处理"的事实 + 产出要点(信息量足够供日后召回)
76
73
  result_brief = str(p.get("result", ""))[:200]
77
74
  truth = {
78
75
  "fact": (f"任务「{p.get('task')}」由 {p.get('agent_id')} 处理。"
@@ -93,18 +90,17 @@ class TruthPipeline:
93
90
  【P3-5】双保险:_on_trace_recorded 已挡 blocked,这里再检查一次。
94
91
  """
95
92
  p = msg.payload
96
- update = {"persisted": False}
93
+ update: dict[str, Any] = {"persisted": False}
97
94
  if self.hmr is not None and not p.get("blocked"):
98
95
  try:
99
- # 沉淀到 HMR 共享层(所有 Agent 可见),用真实的 ingest 接口
100
96
  content = p.get("truth", {}).get("fact", "")
101
97
  origin_agent = p.get("agent_id", "unknown")
102
98
  self.hmr.ingest(
103
99
  content,
104
100
  type="execution",
105
- agent_id="system", # 【P3-0】系统级写入(非来源 Agent)
101
+ agent_id="system",
106
102
  title=f"真值_{datetime.now().strftime('%Y%m%d_%H%M%S')}",
107
- tags=["truth_pipeline", f"origin:{origin_agent}"], # 来源可追溯
103
+ tags=["truth_pipeline", f"origin:{origin_agent}"],
108
104
  shared=True,
109
105
  )
110
106
  update = {"persisted": True, "content": content}