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.
- package/index.js +1 -8
- package/package.json +1 -1
- package/python/.env.example +8 -0
- package/python/.github/workflows/ci.yml +47 -2
- package/python/.github/workflows/release.yml +5 -0
- package/python/CHANGELOG.md +100 -0
- package/python/CHANGELOG_CN.md +100 -0
- package/python/README.md +17 -2
- package/python/README_CN.md +38 -20
- package/python/bench/bm25_audit.py +1 -14
- package/python/bench/bm25_regression.py +0 -1
- package/python/bench/bm25_stress.py +0 -4
- package/python/bench/cd_audit.py +0 -23
- package/python/bench/chaos.py +6 -8
- package/python/bench/e2e_real_models.py +0 -1
- package/python/bench/llm_retry_test.py +2 -9
- package/python/bench/load_test.py +5 -8
- package/python/bench/vector_ivf_upgrade.py +1 -7
- package/python/docs/CONTENT_GUARD.md +189 -0
- package/python/docs/CONTENT_GUARD_CN.md +189 -0
- package/python/docs/RELEASE.md +78 -37
- package/python/docs/RUN_CN.md +2 -2
- package/python/evals/governance/cases.py +0 -37
- package/python/evals/governance/run_evals.py +5 -37
- package/python/examples/governance_middleware_demo.py +1 -6
- package/python/examples/langgraph_governance_demo.py +7 -11
- package/python/foxclaw/__init__.py +74 -152
- package/python/foxclaw/agents/base.py +82 -136
- package/python/foxclaw/agents/impl/_helpers.py +2 -10
- package/python/foxclaw/agents/impl/finance.py +4 -21
- package/python/foxclaw/agents/impl/office.py +7 -24
- package/python/foxclaw/agents/impl/supervisor.py +12 -79
- package/python/foxclaw/agents/mixins/_context.py +0 -23
- package/python/foxclaw/agents/mixins/augment.py +10 -16
- package/python/foxclaw/agents/mixins/llm.py +7 -6
- package/python/foxclaw/agents/mixins/memory.py +23 -3
- package/python/foxclaw/agents/mixins/preference.py +58 -8
- package/python/foxclaw/agents/mixins/prompt.py +59 -12
- package/python/foxclaw/agents/mixins/skillbox.py +6 -6
- package/python/foxclaw/agents/mixins/tool_exec.py +16 -42
- package/python/foxclaw/agents/rules/finance/RULES.md +131 -0
- package/python/foxclaw/api/__init__.py +18 -77
- package/python/foxclaw/api/_state.py +43 -0
- package/python/foxclaw/api/bootstrap.py +114 -0
- package/python/foxclaw/api/middleware.py +5 -28
- package/python/foxclaw/api/routes/ahs.py +60 -73
- package/python/foxclaw/api/routes/attachments.py +5 -6
- package/python/foxclaw/api/routes/browser.py +7 -11
- package/python/foxclaw/api/routes/domain.py +18 -26
- package/python/foxclaw/api/routes/knowledge.py +3 -21
- package/python/foxclaw/api/routes/llm_admin.py +21 -33
- package/python/foxclaw/api/routes/memory.py +50 -12
- package/python/foxclaw/api/routes/orchestration.py +3 -5
- package/python/foxclaw/api/routes/recovery.py +4 -3
- package/python/foxclaw/api/routes/review.py +2 -11
- package/python/foxclaw/api/routes/search.py +3 -7
- package/python/foxclaw/api/routes/system.py +33 -49
- package/python/foxclaw/api/routes/task.py +31 -49
- package/python/foxclaw/api/routes/ws.py +2 -6
- package/python/foxclaw/api/spine.py +1 -4
- package/python/foxclaw/behavior/__init__.py +20 -0
- package/python/foxclaw/behavior/behavior.yaml +62 -0
- package/python/foxclaw/behavior/spec.py +287 -0
- package/python/foxclaw/browser/action/action_executor.py +1 -5
- package/python/foxclaw/browser/action/action_types.py +0 -1
- package/python/foxclaw/browser/action/url_policy.py +1 -5
- package/python/foxclaw/browser/adapter/web_adapter.py +1 -1
- package/python/foxclaw/browser/driver/playwright_driver.py +20 -16
- package/python/foxclaw/browser/perception/dom_extractor.py +0 -1
- package/python/foxclaw/browser/perception/element_indexer.py +1 -2
- package/python/foxclaw/browser/service.py +7 -11
- package/python/foxclaw/capability/embedding_service.py +4 -6
- package/python/foxclaw/channels/__init__.py +9 -11
- package/python/foxclaw/channels/discord.py +4 -13
- package/python/foxclaw/channels/dispatcher.py +2 -27
- package/python/foxclaw/channels/feishu.py +10 -20
- package/python/foxclaw/channels/openclaw_bridge.py +4 -9
- package/python/foxclaw/channels/signing.py +9 -9
- package/python/foxclaw/channels/telegram.py +3 -4
- package/python/foxclaw/channels/weixin.py +7 -17
- package/python/foxclaw/cli.py +42 -13
- package/python/foxclaw/cognitive/reasoner.py +9 -10
- package/python/foxclaw/cognitive/state.py +18 -24
- package/python/foxclaw/core/audit_chain.py +1 -3
- package/python/foxclaw/core/audit_sink.py +3 -3
- package/python/foxclaw/core/compaction.py +8 -26
- package/python/foxclaw/core/config.py +2 -2
- package/python/foxclaw/core/confirm_nonce.py +6 -12
- package/python/foxclaw/core/content_guard.py +341 -0
- package/python/foxclaw/core/envfile.py +2 -2
- package/python/foxclaw/core/failure_policy.py +17 -26
- package/python/foxclaw/core/gateway.py +6 -10
- package/python/foxclaw/core/governance_audit.py +3 -7
- package/python/foxclaw/core/idempotency.py +15 -22
- package/python/foxclaw/core/injection_guard.py +10 -29
- package/python/foxclaw/core/memory_adapter.py +53 -60
- package/python/foxclaw/core/metrics.py +4 -5
- package/python/foxclaw/core/model_router.py +74 -150
- package/python/foxclaw/core/paths.py +1 -1
- package/python/foxclaw/core/recovery.py +21 -34
- package/python/foxclaw/core/scheduler.py +23 -26
- package/python/foxclaw/core/secrets.py +0 -3
- package/python/foxclaw/core/sensitive_write.py +283 -0
- package/python/foxclaw/core/sessions.py +4 -6
- package/python/foxclaw/core/settings.py +16 -32
- package/python/foxclaw/core/shared_write.py +11 -23
- package/python/foxclaw/core/steering_queue.py +12 -11
- package/python/foxclaw/core/system_bus.py +1 -1
- package/python/foxclaw/core/thought_chain.py +30 -32
- package/python/foxclaw/core/token_meter.py +9 -13
- package/python/foxclaw/core/tools.py +4 -12
- package/python/foxclaw/core/tracing.py +25 -40
- package/python/foxclaw/core/truth_pipeline.py +5 -9
- package/python/foxclaw/core/update_check.py +149 -0
- package/python/foxclaw/core/vec_recall_adapter.py +11 -37
- package/python/foxclaw/core/verify.py +6 -14
- package/python/foxclaw/core/web_search.py +4 -28
- package/python/foxclaw/finance/evidence.py +14 -18
- package/python/foxclaw/finance/fx.py +1 -3
- package/python/foxclaw/finance/graph.py +9 -18
- package/python/foxclaw/finance/models.py +20 -24
- package/python/foxclaw/finance/nlparse.py +12 -72
- package/python/foxclaw/finance/policy.py +8 -36
- package/python/foxclaw/finance/reasoner.py +3 -11
- package/python/foxclaw/finance/reasoning.py +13 -34
- package/python/foxclaw/finance/reconcile.py +5 -10
- package/python/foxclaw/finance/runtime.py +19 -46
- package/python/foxclaw/governance/middleware.py +13 -21
- package/python/foxclaw/harness/_base.py +2 -8
- package/python/foxclaw/harness/_policy.py +19 -38
- package/python/foxclaw/harness/drift.py +1 -13
- package/python/foxclaw/harness/event_bus.py +2 -5
- package/python/foxclaw/harness/harness.py +9 -97
- package/python/foxclaw/harness/loop_detection.py +11 -18
- package/python/foxclaw/itinerary/amap_client.py +2 -5
- package/python/foxclaw/itinerary/models.py +7 -7
- package/python/foxclaw/itinerary/reasoner.py +1 -6
- package/python/foxclaw/itinerary/reasoning.py +15 -21
- package/python/foxclaw/itinerary/runtime.py +8 -12
- package/python/foxclaw/kernel/capabilities.py +5 -6
- package/python/foxclaw/kernel/policy_gate.py +4 -10
- package/python/foxclaw/kernel/process_manager.py +17 -36
- package/python/foxclaw/kernel/scheduler.py +3 -9
- package/python/foxclaw/kernel/sentinel.py +13 -23
- package/python/foxclaw/kernel/sentinel_client.py +12 -15
- package/python/foxclaw/kernel/spine.py +9 -9
- package/python/foxclaw/knowledge/__init__.py +4 -20
- package/python/foxclaw/knowledge/truth_crystallizer.py +5 -8
- package/python/foxclaw/learning/knowledge_graph.py +4 -9
- package/python/foxclaw/learning/models.py +13 -13
- package/python/foxclaw/learning/reasoner.py +1 -4
- package/python/foxclaw/learning/runtime.py +19 -31
- package/python/foxclaw/learning/spaced_repetition.py +2 -7
- package/python/foxclaw/mcp_server.py +2 -3
- package/python/foxclaw/orchestration/__init__.py +16 -33
- package/python/foxclaw/pipeline/cognitive_pipeline.py +45 -97
- package/python/foxclaw/pipeline/stage_meta.py +21 -0
- package/python/foxclaw/pipeline/supervisor_stages.py +13 -42
- package/python/foxclaw/planning/llm_planner.py +3 -3
- package/python/foxclaw/planning/planner.py +6 -11
- package/python/foxclaw/search/agent_port.py +7 -11
- package/python/foxclaw/search/evidence.py +0 -4
- package/python/foxclaw/search/internal_search.py +3 -5
- package/python/foxclaw/search/models.py +11 -13
- package/python/foxclaw/search/planner.py +3 -3
- package/python/foxclaw/search/retriever.py +1 -1
- package/python/foxclaw/search/runtime.py +1 -8
- package/python/foxclaw/skills/__init__.py +46 -100
- package/python/foxclaw/skills/archive.py +111 -0
- package/python/foxclaw/skills/fetch.py +226 -0
- package/python/foxclaw/skills/hooks_spec.py +21 -25
- package/python/foxclaw/skills/install_cmd.py +216 -0
- package/python/foxclaw/skills/lifecycle.py +16 -42
- package/python/foxclaw/skills/markdown_skill.py +10 -9
- package/python/foxclaw/skills/mcp_client.py +5 -11
- package/python/foxclaw/skills/mcp_store.py +0 -3
- package/python/foxclaw/skills/permission.py +13 -48
- package/python/foxclaw/skills/pipeline.py +21 -53
- package/python/foxclaw/skills/policy.py +4 -10
- package/python/foxclaw/skills/review.py +3 -3
- package/python/frontend/deck/auth.js +0 -7
- package/python/frontend/deck/chat_model_picker.js +0 -54
- package/python/frontend/deck/domain.js +0 -20
- package/python/frontend/deck/main.js +0 -144
- package/python/frontend/deck/markdown.js +0 -4
- package/python/frontend/deck/settings.js +1 -18
- package/python/frontend/deck/state.js +0 -1
- package/python/frontend/deck/util.js +0 -2
- package/python/frontend/deck/visuals.js +0 -22
- package/python/frontend/planet/planet.js +18 -201
- package/python/frontend/v3/assets/index-JKfHlb-a.css +1 -0
- package/python/frontend/v3/assets/index-tN90dpUH.js +34 -0
- package/python/frontend/v3/index.html +3 -3
- package/python/frontend/widget_runtime.js +0 -43
- package/python/frontend/widgets.js +0 -57
- package/python/frontend_src/index.html +1 -1
- package/python/frontend_src/package-lock.json +2 -2
- package/python/frontend_src/package.json +1 -1
- package/python/frontend_src/src/App.vue +3 -7
- package/python/frontend_src/src/api.js +0 -7
- package/python/frontend_src/src/components/ChatDock.vue +27 -39
- package/python/frontend_src/src/components/DocViewer.vue +0 -9
- package/python/frontend_src/src/components/Inspector.vue +0 -6
- package/python/frontend_src/src/components/LeftRail.vue +60 -39
- package/python/frontend_src/src/components/MenuPopover.vue +9 -11
- package/python/frontend_src/src/components/MessageStream.vue +18 -30
- package/python/frontend_src/src/components/ModelPicker.vue +0 -5
- package/python/frontend_src/src/components/QuickIngest.vue +0 -6
- package/python/frontend_src/src/components/Stage.vue +71 -25
- package/python/frontend_src/src/components/ToolMenu.vue +0 -3
- package/python/frontend_src/src/components/TopBar.vue +38 -10
- package/python/frontend_src/src/components/panels/ActivityPanel.vue +0 -2
- package/python/frontend_src/src/components/panels/AgentsPanel.vue +2 -5
- package/python/frontend_src/src/components/panels/KnowledgePanel.vue +0 -16
- package/python/frontend_src/src/components/panels/MemoryPanel.vue +42 -3
- package/python/frontend_src/src/components/panels/PanelBox.vue +6 -7
- package/python/frontend_src/src/components/panels/ReviewPanel.vue +0 -16
- package/python/frontend_src/src/components/panels/SettingsPanel.vue +142 -38
- package/python/frontend_src/src/components/panels/TasksPanel.vue +0 -1
- package/python/frontend_src/src/components/panels/ToolsPanel.vue +125 -19
- package/python/frontend_src/src/lib/fmt.js +0 -4
- package/python/frontend_src/src/lib/i18n.js +53 -0
- package/python/frontend_src/src/lib/locales/en.js +173 -0
- package/python/frontend_src/src/lib/locales/zh.js +174 -0
- package/python/frontend_src/src/lib/md.js +0 -14
- package/python/frontend_src/src/main.js +2 -0
- package/python/frontend_src/src/stores/app.js +17 -32
- package/python/frontend_src/src/stores/chat.js +0 -34
- package/python/frontend_src/src/stores/model.js +0 -8
- package/python/frontend_src/src/stores/panels.js +16 -44
- package/python/frontend_src/src/stores/profile.js +31 -0
- package/python/frontend_src/src/stores/system.js +25 -0
- package/python/frontend_src/vite.config.js +0 -7
- package/python/pyproject.toml +17 -3
- package/python/requirements.txt +311 -0
- package/python/tests/conftest.py +0 -22
- package/python/tests/run_all.py +4 -66
- package/python/tests/test_agent_lifecycle.py +4 -38
- package/python/tests/test_agent_rules.py +81 -0
- package/python/tests/test_ahs_p2.py +4 -25
- package/python/tests/test_anthropic_tools.py +1 -15
- package/python/tests/test_api.py +19 -9
- package/python/tests/test_api_auth.py +1 -5
- package/python/tests/test_attachments.py +9 -25
- package/python/tests/test_audit_chain.py +2 -7
- package/python/tests/test_audit_sink.py +0 -6
- package/python/tests/test_behavior_spec.py +198 -0
- package/python/tests/test_browser.py +2 -6
- package/python/tests/test_browser_request_guard.py +0 -4
- package/python/tests/test_capabilities.py +0 -6
- package/python/tests/test_channel_signing.py +2 -9
- package/python/tests/test_channels.py +1 -11
- package/python/tests/test_config_layering.py +0 -4
- package/python/tests/test_confirm_nonce.py +1 -12
- package/python/tests/test_conflict_dismiss.py +143 -0
- package/python/tests/test_content_guard.py +177 -0
- package/python/tests/test_context_compaction_wiring.py +1 -7
- package/python/tests/test_dispatch.py +0 -6
- package/python/tests/test_domain_evals.py +11 -80
- package/python/tests/test_domain_import.py +0 -6
- package/python/tests/test_domain_reasoners.py +0 -14
- package/python/tests/test_e2e.py +0 -17
- package/python/tests/test_emb_cache.py +7 -17
- package/python/tests/test_embedding_singleton.py +1 -3
- package/python/tests/test_evals_smoke.py +0 -24
- package/python/tests/test_event_spine.py +10 -18
- package/python/tests/test_evidence_semantic.py +1 -14
- package/python/tests/test_evidence_sse.py +3 -6
- package/python/tests/test_file_ingest.py +0 -11
- package/python/tests/test_finance.py +0 -9
- package/python/tests/test_finance_ledger.py +5 -12
- package/python/tests/test_finance_persist.py +8 -12
- package/python/tests/test_gateway.py +3 -22
- package/python/tests/test_governance.py +5 -52
- package/python/tests/test_governance_audit.py +4 -39
- package/python/tests/test_governance_middleware.py +1 -7
- package/python/tests/test_hkc.py +6 -24
- package/python/tests/test_hkc_index_build_manager.py +0 -33
- package/python/tests/test_hkc_manifest_reuse.py +4 -27
- package/python/tests/test_hkc_search_event_sync.py +3 -54
- package/python/tests/test_import_path_guard.py +0 -6
- package/python/tests/test_injection_guard.py +18 -36
- package/python/tests/test_integration.py +5 -29
- package/python/tests/test_itinerary.py +0 -3
- package/python/tests/test_kee_semantic.py +0 -5
- package/python/tests/test_kernel.py +1 -11
- package/python/tests/test_learning.py +3 -12
- package/python/tests/test_llm_admin.py +4 -19
- package/python/tests/test_llm_planner.py +1 -23
- package/python/tests/test_markdown_skills.py +3 -14
- package/python/tests/test_markdown_skills_openclaw.py +4 -9
- package/python/tests/test_mcp_connectors.py +3 -15
- package/python/tests/test_mcp_guardian_discovery.py +2 -6
- package/python/tests/test_mcp_no_memory.py +3 -3
- package/python/tests/test_mcp_policy_gate.py +0 -1
- package/python/tests/test_mcp_server.py +6 -9
- package/python/tests/test_memory_recall.py +1 -4
- package/python/tests/test_metrics.py +0 -8
- package/python/tests/test_multi_chain.py +1 -13
- package/python/tests/test_multimodal.py +5 -18
- package/python/tests/test_negotiation.py +3 -13
- package/python/tests/test_orchestration.py +1 -11
- package/python/tests/test_orchestration_decoupled.py +0 -6
- package/python/tests/test_p3_p5_hardening.py +3 -17
- package/python/tests/test_parallel_dispatch.py +4 -12
- package/python/tests/test_pipeline.py +17 -58
- package/python/tests/test_plan_visible.py +4 -4
- package/python/tests/test_planner.py +0 -7
- package/python/tests/test_policy_gate.py +2 -3
- package/python/tests/test_profile.py +67 -0
- package/python/tests/test_readme_claims.py +163 -0
- package/python/tests/test_reasoner.py +2 -17
- package/python/tests/test_release_check.py +2 -13
- package/python/tests/test_resume_checkpoint.py +7 -15
- package/python/tests/test_routing.py +2 -12
- package/python/tests/test_scheduler.py +7 -10
- package/python/tests/test_search.py +1 -11
- package/python/tests/test_secrets.py +4 -10
- package/python/tests/test_semantic.py +1 -10
- package/python/tests/test_sensitive_write.py +181 -0
- package/python/tests/test_sessions.py +3 -4
- package/python/tests/test_settings.py +2 -18
- package/python/tests/test_shadow_learning.py +3 -43
- package/python/tests/test_shared_write.py +0 -7
- package/python/tests/test_skill_taint_gate.py +1 -10
- package/python/tests/test_skills.py +2 -18
- package/python/tests/test_skills_fetch.py +282 -0
- package/python/tests/test_stats_and_events.py +2 -5
- package/python/tests/test_streaming.py +2 -7
- package/python/tests/test_supervisor_pipeline.py +4 -5
- package/python/tests/test_supervisor_triage.py +4 -10
- package/python/tests/test_taint.py +2 -7
- package/python/tests/test_task_stream.py +1 -12
- package/python/tests/test_thinking_sse.py +0 -1
- package/python/tests/test_tool_center.py +1 -17
- package/python/tests/test_tool_parallel.py +2 -2
- package/python/tests/test_tool_params.py +1 -5
- package/python/tests/test_tool_use.py +0 -13
- package/python/tests/test_toplevel_concurrency.py +3 -4
- package/python/tests/test_tracing.py +7 -41
- package/python/tests/test_truth_crystallizer.py +4 -6
- package/python/tests/test_truth_flow.py +2 -7
- package/python/tests/test_update_check.py +95 -0
- package/python/tests/test_verify_evidence.py +8 -21
- package/python/tests/test_version_sync.py +35 -6
- package/python/tests/test_web_trigger.py +0 -17
- package/python/tools/build_content_lexicon.py +218 -0
- package/python/tools/bundle_web.py +98 -0
- package/python/tools/check_faiss_type.py +0 -1
- package/python/tools/check_log.ps1 +0 -8
- package/python/tools/mypy_baseline.txt +12 -4
- package/python/tools/mypy_ratchet.py +52 -6
- package/python/tools/pack.py +0 -5
- package/python/tools/recheck_conflicts.py +156 -0
- package/python/tools/release_check.py +24 -15
- package/python/tools/restart_foxclaw.ps1 +5 -8
- package/python/tools/shared_write_audit.py +1 -15
- package/python/tools/size_check.py +1 -1
- package/python/tools/strip_comments.py +228 -0
- package/python/vendor/hkc/hkc_api/routes/ability.py +31 -0
- package/python/vendor/hkc/hkc_api/schemas.py +11 -0
- package/python/vendor/hkc/hkc_kee/kee.py +124 -3
- package/python/vendor/hkc/hkc_sdk/base.py +22 -0
- package/python/vendor/hkc/hkc_sdk/direct_client.py +10 -0
- package/python/vendor/hkc/hkc_sdk/http_client.py +19 -0
- package/python/foxclaw/api/app.py +0 -123
- package/python/frontend/v3/assets/index-BD7VPbMV.js +0 -33
- package/python/frontend/v3/assets/index-CAvnYWGH.css +0 -1
|
@@ -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
|
|
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()
|
|
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)
|
|
181
|
+
self._persist(tr)
|
|
191
182
|
try:
|
|
192
|
-
OTLP.submit(tr)
|
|
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,
|
|
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)
|
|
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,
|
|
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
|
|
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",
|
|
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}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""可选的新版本检查(默认关)。
|
|
2
|
+
|
|
3
|
+
为什么默认关:foxclaw 的定位是本地优先 —— 默认外呼会让「断网也能用」多一个
|
|
4
|
+
例外,也给安全审计多一个出网点。**只有** 设置 ``FOXCLAW_UPDATE_CHECK=1`` 时才会
|
|
5
|
+
访问 PyPI,其余情况 ``status()`` 里 ``enabled=False``、``update_available=False``。
|
|
6
|
+
|
|
7
|
+
失败一律静默:查版本是锦上添花,不能因为它让 ``/api/system/info`` 变慢或报错。
|
|
8
|
+
- 结果按 24h 缓存到 ``<FOXCLAW_DATA_DIR>/update_check.json``;
|
|
9
|
+
- 查询在 daemon 线程里跑,不阻塞请求;
|
|
10
|
+
- 查失败时保留上次成功的结果(只是不刷新时间),避免一次网络抖动让提示消失。
|
|
11
|
+
|
|
12
|
+
只发一个 GET ``https://pypi.org/pypi/foxclaw/json``,不带任何本机信息。
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import threading
|
|
20
|
+
import time
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
_PYPI_URL = "https://pypi.org/pypi/foxclaw/json"
|
|
25
|
+
_TTL = 24 * 3600
|
|
26
|
+
_TIMEOUT = 3.0
|
|
27
|
+
|
|
28
|
+
_lock = threading.Lock()
|
|
29
|
+
_inflight = False
|
|
30
|
+
_last_error: str | None = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _enabled() -> bool:
|
|
34
|
+
return os.environ.get("FOXCLAW_UPDATE_CHECK", "").strip().lower() in {"1", "true", "yes", "on"}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _current() -> str:
|
|
38
|
+
from foxclaw import __version__
|
|
39
|
+
|
|
40
|
+
return __version__
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _data_dir() -> Path:
|
|
44
|
+
return Path(os.environ.get("FOXCLAW_DATA_DIR") or "./foxclaw_data").expanduser()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _cache_path() -> Path:
|
|
48
|
+
return _data_dir() / "update_check.json"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def parse_version(value: str) -> tuple[int, ...]:
|
|
52
|
+
"""``"0.1.10"`` → ``(0, 1, 10)``。只取各段的前导数字,预发布/本地后缀丢掉。
|
|
53
|
+
|
|
54
|
+
不引入 ``packaging``:这里只需要「谁更新」,自己比数字段足够,也少一个依赖。
|
|
55
|
+
"""
|
|
56
|
+
parts: list[int] = []
|
|
57
|
+
for chunk in str(value or "").strip().split("."):
|
|
58
|
+
digits = ""
|
|
59
|
+
for ch in chunk:
|
|
60
|
+
if not ch.isdigit():
|
|
61
|
+
break
|
|
62
|
+
digits += ch
|
|
63
|
+
if not digits:
|
|
64
|
+
break
|
|
65
|
+
parts.append(int(digits))
|
|
66
|
+
return tuple(parts)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def is_newer(latest: str, current: str) -> bool:
|
|
70
|
+
"""latest 是否严格新于 current(逐段比,缺的段补 0)。"""
|
|
71
|
+
left, right = parse_version(latest), parse_version(current)
|
|
72
|
+
width = max(len(left), len(right))
|
|
73
|
+
return left + (0,) * (width - len(left)) > right + (0,) * (width - len(right))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _read_cache() -> dict[str, Any]:
|
|
77
|
+
try:
|
|
78
|
+
data = json.loads(_cache_path().read_text(encoding="utf-8"))
|
|
79
|
+
except Exception:
|
|
80
|
+
return {}
|
|
81
|
+
return data if isinstance(data, dict) else {}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _write_cache(data: dict[str, Any]) -> None:
|
|
85
|
+
try:
|
|
86
|
+
path = _cache_path()
|
|
87
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
88
|
+
path.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
|
|
89
|
+
except Exception:
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _fetch_latest() -> str:
|
|
94
|
+
import urllib.request
|
|
95
|
+
|
|
96
|
+
req = urllib.request.Request(
|
|
97
|
+
_PYPI_URL,
|
|
98
|
+
headers={"Accept": "application/json", "User-Agent": f"foxclaw/{_current()}"},
|
|
99
|
+
)
|
|
100
|
+
with urllib.request.urlopen(req, timeout=_TIMEOUT) as resp: # noqa: S310 - 地址是常量 https
|
|
101
|
+
payload = json.loads(resp.read().decode("utf-8", "replace"))
|
|
102
|
+
version = ((payload or {}).get("info") or {}).get("version") or ""
|
|
103
|
+
if not version:
|
|
104
|
+
raise ValueError("PyPI 响应里没有 info.version")
|
|
105
|
+
return str(version)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _refresh() -> None:
|
|
109
|
+
global _inflight, _last_error
|
|
110
|
+
try:
|
|
111
|
+
latest = _fetch_latest()
|
|
112
|
+
_write_cache({"latest": latest, "checked_at": time.time(), "current": _current()})
|
|
113
|
+
_last_error = None
|
|
114
|
+
except Exception as exc:
|
|
115
|
+
_last_error = f"{type(exc).__name__}: {exc}"
|
|
116
|
+
finally:
|
|
117
|
+
with _lock:
|
|
118
|
+
_inflight = False
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def maybe_refresh() -> None:
|
|
122
|
+
"""启用且缓存过期 → 后台查一次。幂等、非阻塞,可放心在每个 system/info 里调。"""
|
|
123
|
+
global _inflight
|
|
124
|
+
if not _enabled():
|
|
125
|
+
return
|
|
126
|
+
checked = _read_cache().get("checked_at")
|
|
127
|
+
if isinstance(checked, (int, float)) and time.time() - checked < _TTL:
|
|
128
|
+
return
|
|
129
|
+
with _lock:
|
|
130
|
+
if _inflight:
|
|
131
|
+
return
|
|
132
|
+
_inflight = True
|
|
133
|
+
threading.Thread(target=_refresh, name="foxclaw-update-check", daemon=True).start()
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def status() -> dict[str, Any]:
|
|
137
|
+
"""给 ``/api/system/info`` 的只读快照。未启用时 latest 仍是缓存值,但不报可更新。"""
|
|
138
|
+
enabled = _enabled()
|
|
139
|
+
current = _current()
|
|
140
|
+
cache = _read_cache()
|
|
141
|
+
latest = str(cache.get("latest") or "")
|
|
142
|
+
return {
|
|
143
|
+
"enabled": enabled,
|
|
144
|
+
"current": current,
|
|
145
|
+
"latest": latest or None,
|
|
146
|
+
"update_available": bool(enabled and latest and is_newer(latest, current)),
|
|
147
|
+
"checked_at": cache.get("checked_at"),
|
|
148
|
+
"error": _last_error,
|
|
149
|
+
}
|
|
@@ -31,9 +31,8 @@ from pathlib import Path
|
|
|
31
31
|
from types import SimpleNamespace
|
|
32
32
|
|
|
33
33
|
from .shared_write import guard_shared_write
|
|
34
|
+
from typing import Any
|
|
34
35
|
|
|
35
|
-
# Foxclaw 的 type 白名单 → vecRecall 的 memory_type(episodic/semantic/procedural)。
|
|
36
|
-
# 分错不影响读写,但影响 vecRecall 的遗忘曲线与 procedural 精确匹配。
|
|
37
36
|
_FOXCLAW_TO_VR = {
|
|
38
37
|
"concept": "semantic",
|
|
39
38
|
"project": "semantic",
|
|
@@ -52,7 +51,6 @@ _VR_TO_FOXCLAW = {
|
|
|
52
51
|
}
|
|
53
52
|
_VALID_TYPES = set(_FOXCLAW_TO_VR)
|
|
54
53
|
|
|
55
|
-
# 偏好类记忆走 topic_key(精确查询),因为"总是注入的偏好"不该做语义模糊匹配。
|
|
56
54
|
_PREFERENCE_TAG = "preference"
|
|
57
55
|
|
|
58
56
|
|
|
@@ -77,7 +75,7 @@ class _HKCSharedEmbedding:
|
|
|
77
75
|
def __init__(self, model_name: str):
|
|
78
76
|
self.model_name = model_name
|
|
79
77
|
self._model = None
|
|
80
|
-
self._dim = None
|
|
78
|
+
self._dim: int | None = None
|
|
81
79
|
|
|
82
80
|
def _load(self):
|
|
83
81
|
if self._model is None:
|
|
@@ -85,8 +83,6 @@ class _HKCSharedEmbedding:
|
|
|
85
83
|
from hkc_search.embedding_backends import get_st_model
|
|
86
84
|
model = get_st_model(self.model_name)
|
|
87
85
|
if model is None:
|
|
88
|
-
# 不回退到直连 SentenceTransformer:那会绕过 HKC 的进程级共享缓存、
|
|
89
|
-
# 多加载一份模型(bge-m3 常驻约 1GB)。失败就明确报错让工厂回退 HMR。
|
|
90
86
|
raise RuntimeError(
|
|
91
87
|
f"embedding 模型不可用: {self.model_name}(HKC 共享缓存加载失败)")
|
|
92
88
|
self._model = model
|
|
@@ -105,7 +101,6 @@ class _HKCSharedEmbedding:
|
|
|
105
101
|
if self._dim is None:
|
|
106
102
|
try:
|
|
107
103
|
model = self._load()
|
|
108
|
-
# ST 6.x 把 get_sentence_embedding_dimension 改名了,两个都试
|
|
109
104
|
getter = (getattr(model, "get_embedding_dimension", None)
|
|
110
105
|
or getattr(model, "get_sentence_embedding_dimension", None))
|
|
111
106
|
self._dim = (int(getter() or 0) or None) if getter else None
|
|
@@ -120,7 +115,7 @@ class VecRecallAdapter:
|
|
|
120
115
|
backend = "vecRecall"
|
|
121
116
|
|
|
122
117
|
def __init__(self, storage_path: str = "./vecrecall_data", wing: str = "foxclaw",
|
|
123
|
-
use_chain: bool = False, embedding_model: str = None,
|
|
118
|
+
use_chain: bool = False, embedding_model: str | None = None,
|
|
124
119
|
dedup: bool = True, embedding_backend=None, vector_backend=None):
|
|
125
120
|
"""`embedding_backend` / `vector_backend` 用于注入(测试传轻量实现)。
|
|
126
121
|
|
|
@@ -133,9 +128,6 @@ class VecRecallAdapter:
|
|
|
133
128
|
self.wing = wing
|
|
134
129
|
self.storage_path = str(storage_path)
|
|
135
130
|
self.version = self._resolve_version()
|
|
136
|
-
# vecRecall 本体没有任何锁(只有 REST server 层有 RLock),而它用的是
|
|
137
|
-
# check_same_thread=False 的单条 sqlite 连接 + 内存 dict 索引。并行子 Agent
|
|
138
|
-
# 同时 add/search 属于未定义行为,和 HMR 是同一类坑 —— 这里沿用同一套串行化。
|
|
139
131
|
self._lock = threading.RLock()
|
|
140
132
|
|
|
141
133
|
self._emb = embedding_backend or _HKCSharedEmbedding(
|
|
@@ -152,7 +144,6 @@ class VecRecallAdapter:
|
|
|
152
144
|
**extra,
|
|
153
145
|
)
|
|
154
146
|
|
|
155
|
-
# ── 构造辅助 ──────────────────────────────────────────
|
|
156
147
|
@staticmethod
|
|
157
148
|
def _resolve_version() -> str:
|
|
158
149
|
try:
|
|
@@ -162,7 +153,7 @@ class VecRecallAdapter:
|
|
|
162
153
|
return "unknown"
|
|
163
154
|
|
|
164
155
|
@staticmethod
|
|
165
|
-
def _resolve_model(embedding_model: str = None) -> str:
|
|
156
|
+
def _resolve_model(embedding_model: str | None = None) -> str:
|
|
166
157
|
"""与 HMRAdapter 同一条解析链,保证两套后端用同一个模型(分数与维度可比)。"""
|
|
167
158
|
return (embedding_model
|
|
168
159
|
or os.environ.get("EMBEDDING_MODEL")
|
|
@@ -188,14 +179,13 @@ class VecRecallAdapter:
|
|
|
188
179
|
except Exception:
|
|
189
180
|
return
|
|
190
181
|
dims = {int(r[0]) for r in rows}
|
|
191
|
-
want = getattr(self._emb, "dim", None)
|
|
182
|
+
want = getattr(self._emb, "dim", None)
|
|
192
183
|
if dims and want and dims != {int(want)}:
|
|
193
184
|
raise RuntimeError(
|
|
194
185
|
f"已有向量维度 {sorted(dims)} 与当前嵌入维度 {want} 不一致。"
|
|
195
186
|
f"换 embedding 模型必须重建索引:删除 {self.storage_path} 后重启"
|
|
196
187
|
)
|
|
197
188
|
|
|
198
|
-
# ── 摄入 ──────────────────────────────────────────────
|
|
199
189
|
_AUTO_TITLE = re.compile(r"^[A-Za-z_]+\s+\d{1,2}:\d{2}$")
|
|
200
190
|
|
|
201
191
|
@staticmethod
|
|
@@ -214,8 +204,8 @@ class VecRecallAdapter:
|
|
|
214
204
|
s = re.split(r"[。!?!?\n\r]", s, 1)[0].strip()
|
|
215
205
|
return " ".join(s.split())[:80]
|
|
216
206
|
|
|
217
|
-
def ingest(self, content: str, type: str = "concept", agent_id: str = None,
|
|
218
|
-
title: str = None, tags: list = None, shared: bool = False, **kwargs):
|
|
207
|
+
def ingest(self, content: str, type: str = "concept", agent_id: str | None = None,
|
|
208
|
+
title: str | None = None, tags: list | None = None, shared: bool = False, **kwargs):
|
|
219
209
|
"""统一摄入。签名与 `HMRAdapter.ingest` 一致。
|
|
220
210
|
|
|
221
211
|
隔离承载:`agent_id` → owner,`shared=True` → visibility="shared"。
|
|
@@ -231,8 +221,6 @@ class VecRecallAdapter:
|
|
|
231
221
|
if not title:
|
|
232
222
|
title = self._derive_title(content) or ""
|
|
233
223
|
|
|
234
|
-
# 【P0-3】共享层写入闸门:必须在落库之前判定,语义与 HMR 路径完全一致,
|
|
235
|
-
# 否则同一个策略在两个后端上表现不同。
|
|
236
224
|
if shared:
|
|
237
225
|
guard_shared_write(self, agent_id, content, mtype, meta_tags)
|
|
238
226
|
|
|
@@ -240,8 +228,6 @@ class VecRecallAdapter:
|
|
|
240
228
|
if "confidence" in kwargs:
|
|
241
229
|
metadata["confidence"] = kwargs["confidence"]
|
|
242
230
|
|
|
243
|
-
# execution 是审计证据:被遗忘曲线衰减或被去重合并,就无法再证明"当时确实
|
|
244
|
-
# 这么做了"。vecRecall 的 immutable 同时免于衰减与去重,正好对上这个语义。
|
|
245
231
|
retention = "immutable" if mtype == "execution" else "decay"
|
|
246
232
|
topic_key = kwargs.get("topic_key") or (
|
|
247
233
|
_PREFERENCE_TAG if _PREFERENCE_TAG in meta_tags else "")
|
|
@@ -260,8 +246,7 @@ class VecRecallAdapter:
|
|
|
260
246
|
topic_key=topic_key,
|
|
261
247
|
)
|
|
262
248
|
|
|
263
|
-
|
|
264
|
-
def _visible(self, mem, agent_id: str, include_shared: bool) -> bool:
|
|
249
|
+
def _visible(self, mem, agent_id: str | None, include_shared: bool) -> bool:
|
|
265
250
|
"""可见性判定,与 vecRecall `_visible_to` 规则保持同源。
|
|
266
251
|
|
|
267
252
|
- shared 记忆:仅当 include_shared 时可见
|
|
@@ -276,7 +261,7 @@ class VecRecallAdapter:
|
|
|
276
261
|
return include_shared
|
|
277
262
|
return owner == agent_id or not owner
|
|
278
263
|
|
|
279
|
-
def recall(self, query: str = "", agent_id: str = None, top_k: int = 5,
|
|
264
|
+
def recall(self, query: str = "", agent_id: str | None = None, top_k: int = 5,
|
|
280
265
|
include_shared: bool = True, **kwargs):
|
|
281
266
|
"""统一召回,返回 list[dict]。
|
|
282
267
|
|
|
@@ -300,9 +285,6 @@ class VecRecallAdapter:
|
|
|
300
285
|
results = list(getattr(bundle, "l2_topic_context", []) or []) + \
|
|
301
286
|
list(getattr(bundle, "l3_deep_results", []) or [])
|
|
302
287
|
if not results and (query or "").strip():
|
|
303
|
-
# L2 有 0.55 阈值:命中偏弱时整层为空。HMR 的 recall 是无条件
|
|
304
|
-
# 返回 top_k,为不改变上层"总能拿到几条"的预期,退化到全量语义
|
|
305
|
-
# 检索,再在下面用 _visible 补上隔离(search 本身不隔离)。
|
|
306
288
|
results = list(self._vr.search(query, n=fetch_k) or [])
|
|
307
289
|
except Exception:
|
|
308
290
|
return []
|
|
@@ -384,8 +366,7 @@ class VecRecallAdapter:
|
|
|
384
366
|
row["_ts"] = getattr(m, "timestamp", "") or ""
|
|
385
367
|
rows.append(row)
|
|
386
368
|
|
|
387
|
-
rows.sort(key=lambda x: str(x.get("_ts")), reverse=True)
|
|
388
|
-
# 同一偏好反复表达会存成多条,去重保留最新,否则会挤满 limit
|
|
369
|
+
rows.sort(key=lambda x: str(x.get("_ts")), reverse=True)
|
|
389
370
|
deduped, seen_c = [], set()
|
|
390
371
|
for x in rows:
|
|
391
372
|
if x["content"] in seen_c:
|
|
@@ -401,7 +382,6 @@ class VecRecallAdapter:
|
|
|
401
382
|
except Exception:
|
|
402
383
|
return False
|
|
403
384
|
|
|
404
|
-
# ── 展示 ──────────────────────────────────────────────
|
|
405
385
|
def recent(self, limit: int = 20) -> list:
|
|
406
386
|
"""最近的记忆条目(新→旧),供前端右栏"记忆信息条"。"""
|
|
407
387
|
rows = []
|
|
@@ -435,11 +415,6 @@ class VecRecallAdapter:
|
|
|
435
415
|
})
|
|
436
416
|
return rows
|
|
437
417
|
|
|
438
|
-
# ── HMR 专有接口的占位 ────────────────────────────────
|
|
439
|
-
# vecRecall 没有"会话工作区 / 运行时快照"这两个概念。留占位而不是让调用方
|
|
440
|
-
# AttributeError:上层有统一的"后端接口"假设,缺方法会炸在一个与被调用能力
|
|
441
|
-
# 无关的地方。agents/base.py 取到 workspace 后并不读其字段(active_goal 的
|
|
442
|
-
# 写入已移除),所以空壳没有实际成本。
|
|
443
418
|
def get_workspace(self, agent_id: str):
|
|
444
419
|
return SimpleNamespace(agent_id=agent_id, active_goal="", status="idle",
|
|
445
420
|
push_task=lambda _t: None)
|
|
@@ -453,14 +428,13 @@ class VecRecallAdapter:
|
|
|
453
428
|
def restore_runtime_state(self):
|
|
454
429
|
return None
|
|
455
430
|
|
|
456
|
-
# ── 状态 ──────────────────────────────────────────────
|
|
457
431
|
def get_system_status(self):
|
|
458
432
|
return {"backend": self.backend, "version": self.version, "wing": self.wing}
|
|
459
433
|
|
|
460
434
|
def stats(self) -> dict:
|
|
461
435
|
"""记忆统计:前端记忆中心展示真实数字。字段与 HMRAdapter.stats 一致。"""
|
|
462
436
|
total = shared_count = relations = 0
|
|
463
|
-
by_agent = {}
|
|
437
|
+
by_agent: dict[str, Any] = {}
|
|
464
438
|
embedding = type(self._emb).__name__
|
|
465
439
|
try:
|
|
466
440
|
with self._lock:
|
|
@@ -25,17 +25,15 @@ from dataclasses import dataclass, field
|
|
|
25
25
|
from urllib.parse import urlparse
|
|
26
26
|
|
|
27
27
|
from . import failure_policy as _policy
|
|
28
|
+
from typing import Callable
|
|
28
29
|
|
|
29
30
|
VERIFIED = "verified"
|
|
30
31
|
UNVERIFIED = "unverified"
|
|
31
32
|
FAILED = "failed"
|
|
32
33
|
|
|
33
|
-
#: 每条工具结果最多挂几条证据(再多就成了噪声,用户不会看)
|
|
34
34
|
MAX_EVIDENCE = 8
|
|
35
35
|
|
|
36
36
|
_URL_RE = re.compile(r"https?://[^\s<>\"'))\]}]+")
|
|
37
|
-
#: 只保留"真实存在"的路径 —— 正则从文本里认出来的路径九成是误报(URL 片段、示例路径),
|
|
38
|
-
#: 只有能在磁盘上 stat 到的才算证据。这不是限制,是"证据"二字的含义。
|
|
39
37
|
_WIN_PATH_RE = re.compile(r"[A-Za-z]:[\\/][^\s\"'<>|*?,。;]+")
|
|
40
38
|
_POSIX_PATH_RE = re.compile(r"(?<![\w:])/(?:[\w.\-]+/)+[\w.\-]+")
|
|
41
39
|
|
|
@@ -44,10 +42,10 @@ _POSIX_PATH_RE = re.compile(r"(?<![\w:])/(?:[\w.\-]+/)+[\w.\-]+")
|
|
|
44
42
|
class Evidence:
|
|
45
43
|
"""一条可核对的凭据:一个链接、一个真实存在的文件、或一次工具的原始命中。"""
|
|
46
44
|
|
|
47
|
-
kind: str
|
|
48
|
-
ref: str
|
|
49
|
-
label: str
|
|
50
|
-
source: str
|
|
45
|
+
kind: str
|
|
46
|
+
ref: str
|
|
47
|
+
label: str
|
|
48
|
+
source: str
|
|
51
49
|
at: float = field(default_factory=time.time)
|
|
52
50
|
|
|
53
51
|
def to_dict(self) -> dict:
|
|
@@ -121,14 +119,10 @@ def extract_evidence(source: str, text: str, limit: int = MAX_EVIDENCE) -> list[
|
|
|
121
119
|
return out
|
|
122
120
|
|
|
123
121
|
|
|
124
|
-
|
|
125
|
-
#: 没登记的工具走通用规则(见 verify),不会被当成"已验证"。
|
|
126
|
-
_CHECKERS: dict[str, "callable"] = {
|
|
127
|
-
# 联网搜索:结论必须能指回来源,否则就是"模型自己写的" —— 标未核验而不是验证通过
|
|
122
|
+
_CHECKERS: dict[str, Callable[..., tuple[bool, str]]] = {
|
|
128
123
|
"web_search": lambda args, text: (
|
|
129
124
|
(True, "") if _URL_RE.search(text or "")
|
|
130
125
|
else (False, "搜索结果没有可核对的来源链接")),
|
|
131
|
-
# 个人记忆:召回为空是合法结果(确实没记过),非空即有凭据
|
|
132
126
|
"recall_memory": lambda args, text: (
|
|
133
127
|
(True, "") if (text or "").strip().startswith("【召回的相关记忆】")
|
|
134
128
|
else (False, "未返回记忆条目")),
|
|
@@ -157,9 +151,7 @@ def verify(name: str, args: dict | None, text: str,
|
|
|
157
151
|
|
|
158
152
|
read_only = bool((capability or {}).get("read_only"))
|
|
159
153
|
if read_only:
|
|
160
|
-
# 只读 + 有返回 = 这一步确实发生过(内容是否可信由污点/证据层管)
|
|
161
154
|
return Verification(VERIFIED, evidence=ev)
|
|
162
|
-
# 有副作用但没登记后置条件:不能证明效果已发生 → 未核验(不许声称完成)
|
|
163
155
|
return Verification(UNVERIFIED, reason="该工具未声明可观察的后置条件", evidence=ev)
|
|
164
156
|
|
|
165
157
|
|