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.
- 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/CHANGELOG.md +57 -0
- package/python/CHANGELOG_CN.md +57 -0
- package/python/README.md +11 -2
- package/python/README_CN.md +32 -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/RELEASE.md +4 -3
- 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 +34 -152
- package/python/foxclaw/agents/base.py +65 -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 +7 -1
- package/python/foxclaw/agents/mixins/preference.py +58 -8
- package/python/foxclaw/agents/mixins/prompt.py +0 -6
- package/python/foxclaw/agents/mixins/skillbox.py +6 -6
- package/python/foxclaw/agents/mixins/tool_exec.py +16 -42
- package/python/foxclaw/api/__init__.py +18 -77
- package/python/foxclaw/api/_state.py +43 -0
- package/python/foxclaw/api/{app.py → bootstrap.py} +5 -32
- 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 +28 -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/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/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 +30 -57
- package/python/foxclaw/core/metrics.py +4 -5
- package/python/foxclaw/core/model_router.py +36 -148
- 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/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 +0 -201
- package/python/frontend/v3/assets/index-CQSBdxk4.js +34 -0
- package/python/frontend/v3/assets/index-D5nIsjms.css +1 -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 +0 -1
- 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 +0 -41
- 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 +6 -3
- package/python/requirements.txt +311 -0
- package/python/tests/conftest.py +0 -22
- package/python/tests/run_all.py +3 -66
- package/python/tests/test_agent_lifecycle.py +4 -38
- 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_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_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_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/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/release_check.py +7 -7
- 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/frontend/v3/assets/index-BD7VPbMV.js +0 -33
- package/python/frontend/v3/assets/index-CAvnYWGH.css +0 -1
|
@@ -11,12 +11,13 @@ from dataclasses import fields
|
|
|
11
11
|
from datetime import datetime
|
|
12
12
|
from pathlib import Path
|
|
13
13
|
|
|
14
|
-
from .models import
|
|
14
|
+
from .models import Budget, EntityType, FinancialEntity, Transaction
|
|
15
15
|
from .reasoning import FinanceReasoning
|
|
16
16
|
from .policy import FinancePolicyEngine
|
|
17
17
|
from .graph import FinancialDataGraph
|
|
18
18
|
from .fx import FxTable
|
|
19
19
|
from .nlparse import CATEGORY_LABELS, parse as parse_expense_text
|
|
20
|
+
from typing import Any
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
def _period_of(txn, default: str = "") -> str:
|
|
@@ -46,27 +47,22 @@ class FinanceRuntime:
|
|
|
46
47
|
"""财务 Agent 的资金行为操作系统内核。"""
|
|
47
48
|
|
|
48
49
|
def __init__(self, policy_path: str = "./finance_policy.yaml", hmr=None,
|
|
49
|
-
data_path: str = None):
|
|
50
|
+
data_path: str | None = None):
|
|
50
51
|
self.transactions: list = []
|
|
51
52
|
self.budgets: list = []
|
|
52
|
-
self.entities: dict = {}
|
|
53
|
-
self.account_balances: dict = {}
|
|
53
|
+
self.entities: dict = {}
|
|
54
|
+
self.account_balances: dict = {}
|
|
54
55
|
self.policy = FinancePolicyEngine(policy_path)
|
|
55
56
|
self.reasoning = FinanceReasoning()
|
|
56
|
-
self.hmr = hmr
|
|
57
|
-
# 交易/预算/余额/实体的落盘点:默认与 policy 同目录(调用方传的就是 data_dir)。
|
|
58
|
-
# 此前这些只在内存,进程一重启就清零 → 财务分析永远是"0 笔交易"。
|
|
57
|
+
self.hmr = hmr
|
|
59
58
|
self._data_path = Path(data_path) if data_path else (
|
|
60
59
|
Path(policy_path).resolve().parent / "finance_data.json")
|
|
61
|
-
# 写侧串行:runtime 是单例,请求线程会并发 add/import,analyze 还会触发阈值自校准写盘。
|
|
62
60
|
self._lock = threading.RLock()
|
|
63
61
|
self.load_error: str = ""
|
|
64
62
|
self.save_error: str = ""
|
|
65
|
-
# 多币种:账本按本位币(CNY)算账,汇率表与账本同目录
|
|
66
63
|
self.fx = FxTable(str(self._data_path.parent / "finance_fx.json"))
|
|
67
64
|
self.load()
|
|
68
65
|
|
|
69
|
-
# ── 持久化(原子写 tmp→replace;损坏文件只降级、不阻断启动)──────
|
|
70
66
|
def load(self) -> int:
|
|
71
67
|
"""从落盘点恢复交易/预算/余额/实体,返回恢复的交易条数。
|
|
72
68
|
|
|
@@ -106,7 +102,7 @@ class FinanceRuntime:
|
|
|
106
102
|
except (TypeError, ValueError):
|
|
107
103
|
continue
|
|
108
104
|
if not self.entities and self.transactions:
|
|
109
|
-
self._rebuild_entities()
|
|
105
|
+
self._rebuild_entities()
|
|
110
106
|
return len(self.transactions)
|
|
111
107
|
|
|
112
108
|
def save(self) -> bool:
|
|
@@ -127,11 +123,9 @@ class FinanceRuntime:
|
|
|
127
123
|
encoding="utf-8")
|
|
128
124
|
tmp.replace(self._data_path)
|
|
129
125
|
self.save_error = ""
|
|
130
|
-
# 文件已被本进程的完整快照覆盖 → 上一轮"读不进来"的原因作废(自愈)
|
|
131
126
|
self.load_error = ""
|
|
132
127
|
return True
|
|
133
128
|
except Exception as e:
|
|
134
|
-
# 落盘失败不能假装成功:留在内存里 + 暴露错误,让调用方/界面能看到
|
|
135
129
|
self.save_error = f"{type(e).__name__}: {e}"
|
|
136
130
|
return False
|
|
137
131
|
|
|
@@ -140,7 +134,7 @@ class FinanceRuntime:
|
|
|
140
134
|
for txn in self.transactions:
|
|
141
135
|
ent = self.entities.get(txn.entity)
|
|
142
136
|
if not ent:
|
|
143
|
-
etype = "subscription" if txn.is_recurring else "merchant"
|
|
137
|
+
etype: EntityType = "subscription" if txn.is_recurring else "merchant"
|
|
144
138
|
ent = FinancialEntity(name=txn.entity, entity_type=etype)
|
|
145
139
|
self.entities[txn.entity] = ent
|
|
146
140
|
ent.txn_count += 1
|
|
@@ -149,17 +143,15 @@ class FinanceRuntime:
|
|
|
149
143
|
if txn.is_recurring:
|
|
150
144
|
ent.recurring_amount = abs(txn.amount)
|
|
151
145
|
|
|
152
|
-
# ── 数据录入 ──────────────────────────────────────────
|
|
153
146
|
def add_transaction(self, txn: Transaction,
|
|
154
147
|
run_gatekeeper: bool = True,
|
|
155
148
|
save: bool = True) -> dict:
|
|
156
149
|
"""录入一笔交易,经过守门人裁决,并落盘(save=False 供批量导入只写一次)。"""
|
|
157
150
|
with self._lock:
|
|
158
151
|
self.transactions.append(txn)
|
|
159
|
-
# 更新实体统计
|
|
160
152
|
ent = self.entities.get(txn.entity)
|
|
161
153
|
if not ent:
|
|
162
|
-
etype = "subscription" if txn.is_recurring else "merchant"
|
|
154
|
+
etype: EntityType = "subscription" if txn.is_recurring else "merchant"
|
|
163
155
|
ent = FinancialEntity(name=txn.entity, entity_type=etype)
|
|
164
156
|
self.entities[txn.entity] = ent
|
|
165
157
|
ent.txn_count += 1
|
|
@@ -168,7 +160,6 @@ class FinanceRuntime:
|
|
|
168
160
|
if txn.is_recurring:
|
|
169
161
|
ent.recurring_amount = abs(txn.amount)
|
|
170
162
|
|
|
171
|
-
# 写入 HMR(可选)
|
|
172
163
|
if self.hmr:
|
|
173
164
|
try:
|
|
174
165
|
self.hmr.ingest(
|
|
@@ -179,7 +170,6 @@ class FinanceRuntime:
|
|
|
179
170
|
except Exception:
|
|
180
171
|
pass
|
|
181
172
|
|
|
182
|
-
# 守门人裁决
|
|
183
173
|
verdicts = []
|
|
184
174
|
if run_gatekeeper:
|
|
185
175
|
bal = self.account_balances.get(txn.account)
|
|
@@ -190,7 +180,6 @@ class FinanceRuntime:
|
|
|
190
180
|
"gated": any(v.action in ("veto", "freeze", "require_confirmation")
|
|
191
181
|
for v in verdicts),
|
|
192
182
|
}
|
|
193
|
-
# 写盘放在锁外:避免一次磁盘 IO 把并发进来的读请求全堵住
|
|
194
183
|
if save:
|
|
195
184
|
self.save()
|
|
196
185
|
return result
|
|
@@ -206,9 +195,8 @@ class FinanceRuntime:
|
|
|
206
195
|
self.account_balances[account] = balance
|
|
207
196
|
self.save()
|
|
208
197
|
|
|
209
|
-
# ── 自然语言记账(第一线功能)────────────────────────────
|
|
210
198
|
def record_text(self, text: str, account: str = "default",
|
|
211
|
-
now: datetime = None, dry_run: bool = False) -> dict:
|
|
199
|
+
now: datetime | None = None, dry_run: bool = False) -> dict:
|
|
212
200
|
"""「今天买了一件衣服花了 105 块」→ 落一笔账,返回可读回执。
|
|
213
201
|
|
|
214
202
|
识别(金额/币种/时间/分类)全部走 `nlparse` 的确定性规则,LLM 不参与算钱。
|
|
@@ -279,7 +267,6 @@ class FinanceRuntime:
|
|
|
279
267
|
lines.append(f"· {day} {it.get('description') or it.get('entity') or ''}"
|
|
280
268
|
f" {money} · {label}{flag}")
|
|
281
269
|
|
|
282
|
-
# 汇总期跟着这笔账走:记的是上月 5 号的账,就别报"本月已花"
|
|
283
270
|
period = (items[0].get("timestamp") or "")[:7] or None
|
|
284
271
|
led = self.ledger(period)
|
|
285
272
|
head = "本月" if period == datetime.now().strftime("%Y-%m") else f"{period} "
|
|
@@ -298,7 +285,7 @@ class FinanceRuntime:
|
|
|
298
285
|
lines.append("提示:" + ";".join(warns))
|
|
299
286
|
return "\n".join(lines)
|
|
300
287
|
|
|
301
|
-
def ledger(self, period: str = None) -> dict:
|
|
288
|
+
def ledger(self, period: str | None = None) -> dict:
|
|
302
289
|
"""记账本:按月汇总(已发生 vs 计划、按天、按分类、按币种)。"""
|
|
303
290
|
period = period or datetime.now().strftime("%Y-%m")
|
|
304
291
|
with self._lock:
|
|
@@ -311,7 +298,9 @@ class FinanceRuntime:
|
|
|
311
298
|
expense = round(sum(t.amount for t in settled if t.amount < 0), 2)
|
|
312
299
|
income = round(sum(t.amount for t in settled if t.amount > 0), 2)
|
|
313
300
|
|
|
314
|
-
by_day,
|
|
301
|
+
by_day: dict[str, dict[str, Any]] = {}
|
|
302
|
+
by_cat: dict[str, dict[str, Any]] = {}
|
|
303
|
+
by_cur: dict[str, dict[str, Any]] = {}
|
|
315
304
|
for t in settled:
|
|
316
305
|
day = (t.timestamp or "")[:10]
|
|
317
306
|
d = by_day.setdefault(day, {"date": day, "expense": 0.0, "income": 0.0,
|
|
@@ -376,14 +365,12 @@ class FinanceRuntime:
|
|
|
376
365
|
def import_csv(self, path: str) -> int:
|
|
377
366
|
"""从 CSV 导入交易。列:amount,category,entity,account,timestamp,tags
|
|
378
367
|
真实银行 API 接口可在此处替换/扩展。"""
|
|
379
|
-
|
|
368
|
+
csv_path = Path(path)
|
|
380
369
|
count = 0
|
|
381
|
-
with open(
|
|
370
|
+
with open(csv_path, encoding="utf-8") as f:
|
|
382
371
|
reader = csv.DictReader(f)
|
|
383
372
|
for row in reader:
|
|
384
373
|
tags = [t.strip() for t in (row.get("tags") or "").split("|") if t.strip()]
|
|
385
|
-
# 可选的 currency 列:外币行按当天记账汇率折成本位币再入账,
|
|
386
|
-
# 否则「100 美元」会和「100 元」一起被求和。
|
|
387
374
|
cur = (row.get("currency") or row.get("币种") or "CNY").strip().upper()
|
|
388
375
|
raw = float(row["amount"])
|
|
389
376
|
base, rate = self.fx.to_base(abs(raw), cur)
|
|
@@ -401,11 +388,10 @@ class FinanceRuntime:
|
|
|
401
388
|
self.add_transaction(txn, run_gatekeeper=False, save=False)
|
|
402
389
|
count += 1
|
|
403
390
|
if count:
|
|
404
|
-
self.save()
|
|
391
|
+
self.save()
|
|
405
392
|
return count
|
|
406
393
|
|
|
407
|
-
|
|
408
|
-
def analyze(self, period: str = None, current_balance: float = None,
|
|
394
|
+
def analyze(self, period: str | None = None, current_balance: float | None = None,
|
|
409
395
|
calibrate: bool = True) -> dict:
|
|
410
396
|
"""一次性产出完整财务洞察:预算/风险/预测/守门人裁决。
|
|
411
397
|
|
|
@@ -413,17 +399,12 @@ class FinanceRuntime:
|
|
|
413
399
|
再裁决 —— 让阈值随消费水平自己长,而不是永远停在出厂拍的经验值上。
|
|
414
400
|
"""
|
|
415
401
|
period = period or datetime.now().strftime("%Y-%m")
|
|
416
|
-
# 快照:分析期间可能有并发录入(持久化后 import 端点就是可并发的写方),
|
|
417
|
-
# 直接遍历 self.transactions 会撞上 "list changed size during iteration"。
|
|
418
|
-
# 计划支出(明天/下个月)必须排除:还没发生的钱不能算进"本月已花",
|
|
419
|
-
# 否则「明天交房租 3000」会立刻把这个月的预算打爆。
|
|
420
402
|
txns = [t for t in self.transactions if not getattr(t, "is_planned", False)]
|
|
421
403
|
budgets = list(self.budgets)
|
|
422
404
|
bal = current_balance
|
|
423
405
|
if bal is None and self.account_balances:
|
|
424
406
|
bal = sum(dict(self.account_balances).values())
|
|
425
407
|
|
|
426
|
-
# 阈值自校准:必须在裁决**之前**,否则这次分析用的还是上一轮的旧阈值
|
|
427
408
|
calibration = (self.policy.apply_calibration(
|
|
428
409
|
self.reasoning.calibrate_thresholds(txns)) if calibrate else [])
|
|
429
410
|
|
|
@@ -435,21 +416,16 @@ class FinanceRuntime:
|
|
|
435
416
|
cashflow = (self.reasoning.cashflow_projection(txns, bal)
|
|
436
417
|
if bal is not None else None)
|
|
437
418
|
|
|
438
|
-
# 超支预测(对每个有预算的分类)—— 提到裁决之前算,好一并交给守门人
|
|
439
419
|
forecasts = [self.reasoning.overspend_forecast(txns, b, period)
|
|
440
420
|
for b in budgets]
|
|
441
421
|
renewals = self.reasoning.detect_upcoming_renewals(txns)
|
|
442
422
|
pending_refunds = self.reasoning.detect_pending_refunds(txns)
|
|
443
423
|
|
|
444
|
-
# 守门人对整体状态裁决
|
|
445
|
-
# duplicates/forecasts 此前已被算出来、却只进报告不进裁决(检测到了却不吭声);
|
|
446
|
-
# renewals/pending_refunds 为新增信号。现在一并交给守门人。
|
|
447
424
|
state_verdicts = self.policy.evaluate_state(
|
|
448
425
|
subscription_total=sub_creep["monthly_total"], anomalies=anomalies,
|
|
449
426
|
duplicates=duplicates, forecasts=forecasts,
|
|
450
427
|
renewals=renewals, pending_refunds=pending_refunds)
|
|
451
428
|
|
|
452
|
-
# 结构化证据链(可解释决策,可沉淀到 ThoughtChain)
|
|
453
429
|
from .evidence import FinanceExplainer
|
|
454
430
|
evidence_chains = [
|
|
455
431
|
FinanceExplainer.explain_budget_risk(budget_status).to_dict(),
|
|
@@ -467,11 +443,10 @@ class FinanceRuntime:
|
|
|
467
443
|
"burn_rate": burn,
|
|
468
444
|
"cashflow_projection": cashflow,
|
|
469
445
|
"gatekeeper_verdicts": [v.to_dict() for v in state_verdicts],
|
|
470
|
-
"threshold_calibration": calibration,
|
|
446
|
+
"threshold_calibration": calibration,
|
|
471
447
|
"evidence_chains": evidence_chains,
|
|
472
448
|
}
|
|
473
449
|
|
|
474
|
-
# ── 守门人审批流(不自动动钱)─────────────────────────
|
|
475
450
|
def request_action(self, action_type: str, detail: dict) -> dict:
|
|
476
451
|
"""请求一个资金操作(如取消订阅、转账)。
|
|
477
452
|
不自动执行——返回审批请求,标记需用户确认 / 需接真实支付 API。"""
|
|
@@ -489,7 +464,6 @@ class FinanceRuntime:
|
|
|
489
464
|
"entities": len(self.entities),
|
|
490
465
|
"accounts": self.account_balances,
|
|
491
466
|
"policy": self.policy.summary(),
|
|
492
|
-
# 落盘状态一并回给界面:否则"数据到底存哪了/为什么重启就没了"只能靠猜
|
|
493
467
|
"persist": {"path": str(self._data_path),
|
|
494
468
|
"exists": self._data_path.exists(),
|
|
495
469
|
"load_error": self.load_error,
|
|
@@ -513,7 +487,6 @@ class FinanceRuntime:
|
|
|
513
487
|
返回:匹配/差异分类 + 审计日志。
|
|
514
488
|
"""
|
|
515
489
|
from .reconcile import Reconciler, ExternalRecord
|
|
516
|
-
# 容忍 dict 输入,自动转 ExternalRecord
|
|
517
490
|
ext = []
|
|
518
491
|
for r in external_records:
|
|
519
492
|
if isinstance(r, ExternalRecord):
|
|
@@ -13,25 +13,25 @@ import functools
|
|
|
13
13
|
import tempfile
|
|
14
14
|
import uuid
|
|
15
15
|
from dataclasses import dataclass, field
|
|
16
|
-
from typing import Optional
|
|
16
|
+
from typing import Optional, cast
|
|
17
17
|
|
|
18
18
|
from ..harness.harness import Harness
|
|
19
19
|
from ..harness.loop_detection import ToolLoopDetector
|
|
20
|
-
from ..core.thought_chain import ThoughtChain, ThoughtNode
|
|
20
|
+
from ..core.thought_chain import NodeType, ThoughtChain, ThoughtNode
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
@dataclass
|
|
24
24
|
class GovResult:
|
|
25
25
|
"""一步治理判定的结果(框架无关,不泄露 Foxclaw 内部类型)。"""
|
|
26
|
-
status: str
|
|
26
|
+
status: str
|
|
27
27
|
blocked: bool
|
|
28
28
|
warned: bool
|
|
29
29
|
delta: float
|
|
30
|
-
dimension: str
|
|
30
|
+
dimension: str
|
|
31
31
|
reason: Optional[str]
|
|
32
32
|
node_id: str
|
|
33
|
-
scores: dict = field(default_factory=dict)
|
|
34
|
-
loop: Optional[dict] = None
|
|
33
|
+
scores: dict = field(default_factory=dict)
|
|
34
|
+
loop: Optional[dict] = None
|
|
35
35
|
|
|
36
36
|
@property
|
|
37
37
|
def tool_violation(self) -> bool:
|
|
@@ -74,8 +74,8 @@ class GovernanceSession:
|
|
|
74
74
|
|
|
75
75
|
def __init__(self, goal: str, allowed_tools=None, entities=None,
|
|
76
76
|
agent_id: str = "agent", shadow_rounds: int = 0,
|
|
77
|
-
db_path: str = None, policy_path: str = None,
|
|
78
|
-
embedding_model: str = None):
|
|
77
|
+
db_path: str | None = None, policy_path: str | None = None,
|
|
78
|
+
embedding_model: str | None = None):
|
|
79
79
|
self.agent_id = agent_id
|
|
80
80
|
self._session_id = f"gov_{uuid.uuid4().hex[:10]}"
|
|
81
81
|
self.thought_chain = ThoughtChain(
|
|
@@ -88,17 +88,13 @@ class GovernanceSession:
|
|
|
88
88
|
goal_text=goal,
|
|
89
89
|
allowed_tool_categories=set(allowed_tools or {"read"}),
|
|
90
90
|
baseline_entities=set(entities or []))
|
|
91
|
-
# shadow_rounds<=0:中间件默认**即时按默认阈值判定**(warn 0.15 / block 0.25),
|
|
92
|
-
# 不做"拿第一个样本自校准"(那会让首步逃逸并用单样本污染阈值)。想用真实分布
|
|
93
|
-
# 校准的调用方,显式传 shadow_rounds>0 走观测期。
|
|
94
91
|
if shadow_rounds <= 0:
|
|
95
92
|
self.harness._shadow_mode = False
|
|
96
93
|
self._loop = ToolLoopDetector()
|
|
97
94
|
self._blocked_count = 0
|
|
98
95
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
node_type: str = None, conf: float = 0.9,
|
|
96
|
+
def observe(self, content: str, tool: str | None = None, entities=None,
|
|
97
|
+
node_type: str | None = None, conf: float = 0.9,
|
|
102
98
|
semantic: bool = True) -> GovResult:
|
|
103
99
|
"""观测一个推理/工具步骤,返回治理判定。不抛异常(判定在结果里)。
|
|
104
100
|
|
|
@@ -106,7 +102,7 @@ class GovernanceSession:
|
|
|
106
102
|
工具调用串对目标的语义距离是噪声。"""
|
|
107
103
|
node = ThoughtNode.create(
|
|
108
104
|
session_id=self._session_id, agent_id=self.agent_id,
|
|
109
|
-
type=node_type or ("tool_intent" if tool else "reasoning"),
|
|
105
|
+
type=cast(NodeType, node_type or ("tool_intent" if tool else "reasoning")),
|
|
110
106
|
content=content, conf=conf, tool_name=tool, entities=entities or [])
|
|
111
107
|
self.thought_chain.add_node(node)
|
|
112
108
|
r = self.harness.check(node, ignore_semantic=not semantic)
|
|
@@ -121,14 +117,13 @@ class GovernanceSession:
|
|
|
121
117
|
delta=r.delta, dimension=r.dominant_dim, reason=r.reason,
|
|
122
118
|
node_id=node.id, scores=dict(r.scores), loop=loop_warn)
|
|
123
119
|
|
|
124
|
-
def guard(self, content: str, tool: str = None, entities=None) -> GovResult:
|
|
120
|
+
def guard(self, content: str, tool: str | None = None, entities=None) -> GovResult:
|
|
125
121
|
"""与 observe 相同,但被 BLOCK 时**抛 GovernanceBlocked**(fail-closed 风格)。"""
|
|
126
122
|
r = self.observe(content, tool=tool, entities=entities)
|
|
127
123
|
if r.blocked:
|
|
128
124
|
raise GovernanceBlocked(r)
|
|
129
125
|
return r
|
|
130
126
|
|
|
131
|
-
# ── 审计 ────────────────────────────────────────────────
|
|
132
127
|
def audit(self) -> list[dict]:
|
|
133
128
|
"""取回本会话的完整可审计推理链(带每步 Harness 判定)。"""
|
|
134
129
|
return [n.to_dict() for n in
|
|
@@ -148,7 +143,7 @@ class GovernanceSession:
|
|
|
148
143
|
return False
|
|
149
144
|
|
|
150
145
|
|
|
151
|
-
def governed(session: GovernanceSession, tool_name: str = None, entities=None):
|
|
146
|
+
def governed(session: GovernanceSession, tool_name: str | None = None, entities=None):
|
|
152
147
|
"""装饰器:把一个工具函数包成"调用前先过 Harness"。
|
|
153
148
|
|
|
154
149
|
命中越权/跑题/范围蔓延 → 抛 GovernanceBlocked,函数不执行。同步/异步函数都支持。
|
|
@@ -158,9 +153,6 @@ def governed(session: GovernanceSession, tool_name: str = None, entities=None):
|
|
|
158
153
|
def delete_row(...): ... # 调用即被拦(delete 不在 read 允许集)
|
|
159
154
|
"""
|
|
160
155
|
def _check(name):
|
|
161
|
-
# 工具装饰器只对"这个工具授权了吗"负责 → semantic=False 只按 tool/scope 判定
|
|
162
|
-
# (语义是 observe() 的职责;工具名对目标的语义距离是噪声,跨语言时更会饱和,
|
|
163
|
-
# 不能作为工具授权依据)。这样中间件语言无关、行为确定、审计不被语义噪声污染。
|
|
164
156
|
r = session.observe(f"tool call: {name}", tool=name, entities=entities,
|
|
165
157
|
semantic=False)
|
|
166
158
|
if r.blocked or r.tool_violation or r.scope_violation:
|
|
@@ -8,11 +8,6 @@ import numpy as np
|
|
|
8
8
|
from ..core.thought_chain import HarnessStatus
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
# 目标基线是**每任务**语义(register_goal 每次任务设一次),却存在 Harness 实例上——
|
|
12
|
-
# 并发顶层任务打同一 Agent 的 harness 时会互相覆盖,导致 check 对着错误的目标算漂移。
|
|
13
|
-
# 与 base.py 同构:放进单个 module 级 contextvar 承载的 dict、按实例 token 分槽。
|
|
14
|
-
# 累积学习态(weights/_policy/_delta_history/_run_count/_shadow_mode)仍留在实例上——
|
|
15
|
-
# 那本就该按 Agent 累积,且同步 check 内无 await,协作调度下读改写原子安全。
|
|
16
11
|
_harness_ctx: contextvars.ContextVar = contextvars.ContextVar(
|
|
17
12
|
"foxclaw_harness_ctx", default=None)
|
|
18
13
|
|
|
@@ -27,7 +22,6 @@ def _harness_ctx_set(token: str, baseline):
|
|
|
27
22
|
_harness_ctx.set(d)
|
|
28
23
|
|
|
29
24
|
|
|
30
|
-
# ── 工具类别白名单 ─────────────────────────────────────────
|
|
31
25
|
TOOL_CATEGORIES: dict[str, set[str]] = {
|
|
32
26
|
"read": {"read_file", "search", "fetch", "list", "get", "query"},
|
|
33
27
|
"write": {"write_file", "create", "update", "save", "append"},
|
|
@@ -43,8 +37,8 @@ TOOL_CATEGORIES: dict[str, set[str]] = {
|
|
|
43
37
|
class HarnessResult:
|
|
44
38
|
status: HarnessStatus
|
|
45
39
|
delta: float
|
|
46
|
-
dominant_dim: str
|
|
47
|
-
scores: dict
|
|
40
|
+
dominant_dim: str
|
|
41
|
+
scores: dict
|
|
48
42
|
reason: Optional[str] = None
|
|
49
43
|
|
|
50
44
|
@property
|
|
@@ -4,17 +4,30 @@
|
|
|
4
4
|
等实例状态协同工作。纯搬移、零行为变更。
|
|
5
5
|
"""
|
|
6
6
|
import os
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, TYPE_CHECKING
|
|
7
10
|
|
|
8
11
|
import numpy as np
|
|
9
12
|
import yaml
|
|
10
|
-
from datetime import datetime, timezone
|
|
11
13
|
|
|
12
14
|
from ..core.thought_chain import ThoughtNode
|
|
13
15
|
from ._base import HarnessResult
|
|
14
16
|
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from .event_bus import HarnessEventBus
|
|
19
|
+
|
|
15
20
|
|
|
16
21
|
class PolicyMixin:
|
|
17
|
-
|
|
22
|
+
agent_id: str
|
|
23
|
+
bus: "HarnessEventBus"
|
|
24
|
+
policy_path: Path
|
|
25
|
+
weights: dict[str, float]
|
|
26
|
+
promote_min_obs: int
|
|
27
|
+
_policy: dict
|
|
28
|
+
_baseline: Any
|
|
29
|
+
_delta_history: list[float]
|
|
30
|
+
|
|
18
31
|
|
|
19
32
|
def _reindex_learned_actions(self):
|
|
20
33
|
"""把 self._policy['rules'] 里已学习的 action 编译成 check() 用得上的结构。
|
|
@@ -35,7 +48,6 @@ class PolicyMixin:
|
|
|
35
48
|
elif atype == "restrict_scope":
|
|
36
49
|
lim = action.get("entity_expansion_limit")
|
|
37
50
|
if isinstance(lim, int):
|
|
38
|
-
# 多条 restrict 取最严(最小)上限
|
|
39
51
|
scope_limit = lim if scope_limit is None else min(scope_limit, lim)
|
|
40
52
|
self._penalized_tools = penalized
|
|
41
53
|
self._scope_limit = scope_limit
|
|
@@ -46,11 +58,7 @@ class PolicyMixin:
|
|
|
46
58
|
return False
|
|
47
59
|
return any(tool_name == p or p in tool_name for p in self._penalized_tools)
|
|
48
60
|
|
|
49
|
-
# ── Policy 学习(P0-1:影子验证 + 回滚 + 边界)────────────────
|
|
50
61
|
|
|
51
|
-
# 【P0-1 边界】可生成影子规则的维度。semantic 可学但**仅权重级**(不生成规则——
|
|
52
|
-
# 原 increase_semantic_threshold 规则本就无规则级应用,判定行为不变);
|
|
53
|
-
# 不在 weights 里的未知维度 → 只记录不学习(fail-closed,防未来新维度被默认自学)。
|
|
54
62
|
_LEARNABLE_RULE_DIMS = frozenset({"tool", "scope"})
|
|
55
63
|
|
|
56
64
|
def _trigger_policy_learning(self, node: ThoughtNode, result: HarnessResult):
|
|
@@ -64,16 +72,14 @@ class PolicyMixin:
|
|
|
64
72
|
"""
|
|
65
73
|
dim = result.dominant_dim
|
|
66
74
|
|
|
67
|
-
# ── 边界:未知维度 fail-closed,只记录不学习(权重也不动)──
|
|
68
75
|
if dim not in self.weights:
|
|
69
76
|
self._record_history("skip", "",
|
|
70
77
|
note=f"未知维度 '{dim}' 禁止自学习(fail-closed)")
|
|
71
78
|
self._save_policy()
|
|
72
79
|
return
|
|
73
80
|
|
|
74
|
-
# ── 规则级:tool/scope 生成影子候选;semantic 仅权重级 ──
|
|
75
81
|
if dim in self._LEARNABLE_RULE_DIMS:
|
|
76
|
-
new_rule = {
|
|
82
|
+
new_rule: dict[str, Any] = {
|
|
77
83
|
"id": f"rule_{node.id}",
|
|
78
84
|
"trigger": {
|
|
79
85
|
"harness_status": "BLOCK",
|
|
@@ -84,8 +90,6 @@ class PolicyMixin:
|
|
|
84
90
|
"created_at": datetime.now(timezone.utc).isoformat(),
|
|
85
91
|
"source": "harness_learned",
|
|
86
92
|
"agent_id": self.agent_id,
|
|
87
|
-
# 影子账本:observations=适用次数;would_block=若生效会多拦;
|
|
88
|
-
# would_pass=若生效会多放。三者是 promote/discard 的全部证据。
|
|
89
93
|
"shadow": {
|
|
90
94
|
"created_at": datetime.now(timezone.utc).isoformat(),
|
|
91
95
|
"observations": 0, "would_block": 0, "would_pass": 0,
|
|
@@ -95,19 +99,12 @@ class PolicyMixin:
|
|
|
95
99
|
self._policy.setdefault("shadow_rules", []).append(new_rule)
|
|
96
100
|
self._record_history("propose", new_rule["id"],
|
|
97
101
|
note=f"{dim} 维度 BLOCK → 影子观察")
|
|
98
|
-
# 不 _reindex_learned_actions —— 影子规则**不得**影响判定
|
|
99
102
|
self.bus.emit("shadow_rule_proposed", {
|
|
100
103
|
"agent_id": self.agent_id, "rule_id": new_rule["id"],
|
|
101
104
|
"dominant_dim": dim,
|
|
102
105
|
"action_type": new_rule["action"].get("type", ""),
|
|
103
106
|
})
|
|
104
107
|
|
|
105
|
-
# 更新权重:触发截断的维度权重 +δ
|
|
106
|
-
# 必须**先把新权重算完整再整体赋值**,不能"原地 +0.05 再归一化":
|
|
107
|
-
# 后者会让 self.weights 出现「各维之和 = 1.05」的瞬时中间态,而此刻若有人读到它
|
|
108
|
-
# (get_policy_summary / policy_updated 的监听者),拿到的就是未归一化的错值。
|
|
109
|
-
# 实测:同一份 summary 持有的 weights 引用会被读出 0.35,而稍后再取是 0.333 —— 正是
|
|
110
|
-
# 这个中间态被观察到。
|
|
111
108
|
bumped = dict(self.weights)
|
|
112
109
|
bumped[dim] = min(bumped[dim] + 0.05, 0.8)
|
|
113
110
|
total = sum(bumped.values())
|
|
@@ -121,7 +118,6 @@ class PolicyMixin:
|
|
|
121
118
|
"new_weights": dict(self.weights),
|
|
122
119
|
})
|
|
123
120
|
|
|
124
|
-
# ── 影子观察(双轨判定)──────────────────────────────────
|
|
125
121
|
|
|
126
122
|
def _observe_shadow_rules(self, node: ThoughtNode, result: HarnessResult,
|
|
127
123
|
real_scores: dict):
|
|
@@ -146,7 +142,6 @@ class PolicyMixin:
|
|
|
146
142
|
|
|
147
143
|
sh["observations"] = sh.get("observations", 0) + 1
|
|
148
144
|
|
|
149
|
-
# 双轨:把该规则的 action 应用到 scores 副本,用**同一套权重与阈值**判定
|
|
150
145
|
shadow_status = self._status_with_action(rule, node, real_scores)
|
|
151
146
|
real_blocked = result.is_blocked
|
|
152
147
|
shadow_blocked = shadow_status == "BLOCK"
|
|
@@ -163,7 +158,7 @@ class PolicyMixin:
|
|
|
163
158
|
self._discard_rule(rule, "影子证据不达标")
|
|
164
159
|
need_save = True
|
|
165
160
|
elif sh["observations"] % 10 == 0:
|
|
166
|
-
need_save = True
|
|
161
|
+
need_save = True
|
|
167
162
|
if need_save:
|
|
168
163
|
self._save_policy()
|
|
169
164
|
|
|
@@ -176,13 +171,9 @@ class PolicyMixin:
|
|
|
176
171
|
if atype == "penalize_tool":
|
|
177
172
|
pat = action.get("tool_pattern", "")
|
|
178
173
|
tool_name = getattr(node, "tool_name", "") or ""
|
|
179
|
-
# 与 _is_penalized_tool 同语义:**仅当前工具命中 pattern 才抬分**。
|
|
180
|
-
# 若不看匹配、凡 tool 分非零就抬,会把「毫不相干工具的场景」误记成
|
|
181
|
-
# would_block —— 影子证据完全失真,好规则会被冤判成误伤 discard。
|
|
182
174
|
if pat and tool_name and (tool_name == pat or pat in tool_name):
|
|
183
175
|
scores["tool"] = 1.0
|
|
184
176
|
elif atype == "restrict_scope":
|
|
185
|
-
# 与 check() 里 restrict_scope 的判定同构:新增实体超限则抬 scope
|
|
186
177
|
lim = action.get("entity_expansion_limit")
|
|
187
178
|
baseline = self._baseline
|
|
188
179
|
if isinstance(lim, int) and baseline is not None and baseline.baseline_entities:
|
|
@@ -202,10 +193,8 @@ class PolicyMixin:
|
|
|
202
193
|
if obs < self.promote_min_obs:
|
|
203
194
|
return None
|
|
204
195
|
wb, wp = sh.get("would_block", 0), sh.get("would_pass", 0)
|
|
205
|
-
# 会多拦很多(可能误伤)→ 不 promote
|
|
206
196
|
if wb / max(obs, 1) > 0.3:
|
|
207
197
|
return False
|
|
208
|
-
# 几乎不改变判定 → promote 没意义
|
|
209
198
|
if wb == 0 and wp == 0:
|
|
210
199
|
return False
|
|
211
200
|
return True
|
|
@@ -214,7 +203,7 @@ class PolicyMixin:
|
|
|
214
203
|
"""影子 → 生效:移入 rules、重建索引、记 history、发事件。"""
|
|
215
204
|
rule.get("shadow", {})["status"] = "promoted"
|
|
216
205
|
self._policy.setdefault("shadow_rules", []).remove(rule)
|
|
217
|
-
rule.pop("shadow", None)
|
|
206
|
+
rule.pop("shadow", None)
|
|
218
207
|
self._policy.setdefault("rules", []).append(rule)
|
|
219
208
|
self._reindex_learned_actions()
|
|
220
209
|
self._record_history("promote", rule["id"],
|
|
@@ -303,7 +292,6 @@ class PolicyMixin:
|
|
|
303
292
|
"delta": -0.05,
|
|
304
293
|
}
|
|
305
294
|
|
|
306
|
-
# ── 阈值校准 ───────────────────────────────────────────
|
|
307
295
|
|
|
308
296
|
def _calibrate_thresholds(self):
|
|
309
297
|
"""Shadow mode 结束 — 用真实 Δ 分布校准阈值。
|
|
@@ -311,20 +299,15 @@ class PolicyMixin:
|
|
|
311
299
|
只有真正离群的(如完全跑题)才被拦截。
|
|
312
300
|
否则用中位数附近的分位数会拦掉一半正常流量。
|
|
313
301
|
"""
|
|
314
|
-
arr = np.array(sorted(self._delta_history), dtype=
|
|
302
|
+
arr = np.array(sorted(self._delta_history), dtype=np.float64)
|
|
315
303
|
if arr.size == 0:
|
|
316
|
-
# 空历史(理论上不会发生,因调用前每次 check 都 append)→ 用默认阈值,避免 nan
|
|
317
304
|
self._shadow_mode = False
|
|
318
305
|
return
|
|
319
306
|
mean = float(arr.mean())
|
|
320
307
|
std = float(arr.std())
|
|
321
308
|
p90 = float(np.percentile(arr, 90))
|
|
322
|
-
# BLOCK 阈值取"高分位 / 均值+1.5σ"的较大者,再留 10% 余量,
|
|
323
|
-
# 保证正常任务(集中在均值附近)不被误拦
|
|
324
309
|
block = max(p90, mean + 1.5 * std) * 1.10
|
|
325
|
-
# WARN 阈值取 block 的 0.8 倍,给出渐进预警区
|
|
326
310
|
warn = block * 0.8
|
|
327
|
-
# 下限保护:阈值不低于默认值,避免样本噪声把阈值压得过紧
|
|
328
311
|
self._block_threshold = max(block, 0.25)
|
|
329
312
|
self._warn_threshold = max(warn, 0.18)
|
|
330
313
|
self._shadow_mode = False
|
|
@@ -337,13 +320,11 @@ class PolicyMixin:
|
|
|
337
320
|
"std": round(std, 3),
|
|
338
321
|
})
|
|
339
322
|
|
|
340
|
-
# ── Policy 持久化 ──────────────────────────────────────
|
|
341
323
|
|
|
342
324
|
def _load_policy(self) -> dict:
|
|
343
325
|
if self.policy_path.exists():
|
|
344
326
|
with open(self.policy_path, encoding="utf-8") as f:
|
|
345
327
|
return yaml.safe_load(f) or {}
|
|
346
|
-
# 默认 Policy 骨架(格式从第一天锁定)
|
|
347
328
|
return {
|
|
348
329
|
"version": 1,
|
|
349
330
|
"agent_id": self.agent_id,
|
|
@@ -7,18 +7,12 @@ from sklearn.metrics.pairwise import cosine_similarity
|
|
|
7
7
|
from ._base import TOOL_CATEGORIES
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
# 文本向量由 foxclaw.capability.embedding_service 统一提供(进程内共享,同名只加载一次)。
|
|
11
|
-
# 不可用时该服务返回 None,SemanticDrift 自动降级为 n-gram 哈希嵌入。
|
|
12
|
-
|
|
13
|
-
|
|
14
10
|
class SemanticDrift:
|
|
15
11
|
"""维度一:语义距离。优先 sentence-transformers,不可用则降级 n-gram 哈希嵌入。"""
|
|
16
12
|
|
|
17
13
|
DIM = 256
|
|
18
14
|
|
|
19
15
|
def __init__(self, model_name: str = "all-MiniLM-L6-v2"):
|
|
20
|
-
# P0:走进程内共享 EmbeddingService,同名模型全系统只加载一次
|
|
21
|
-
# (原来每个 Harness 各 load 一份;5 Agent = 5 份 → 现 1 份共享,只读并发 encode 安全)。
|
|
22
16
|
from ..capability.embedding_service import get_text_embedder
|
|
23
17
|
self._model = get_text_embedder(model_name)
|
|
24
18
|
self._mode = "transformer" if self._model is not None else "hash"
|
|
@@ -58,8 +52,6 @@ class SemanticDrift:
|
|
|
58
52
|
return 0.0
|
|
59
53
|
node_vec = self._encode(content)
|
|
60
54
|
sim = cosine_similarity([baseline_vec], [node_vec])[0][0]
|
|
61
|
-
# 余弦相似度范围 [-1, 1],哈希嵌入带符号累加可能产生负相似度,
|
|
62
|
-
# 1 - sim 会超过 1.0 → 必须双向裁剪到 [0, 1],否则漂移分超出阈值范围使判定失真。
|
|
63
55
|
return float(min(1.0, max(0.0, 1.0 - sim)))
|
|
64
56
|
|
|
65
57
|
|
|
@@ -72,17 +64,13 @@ class ToolIntentDrift:
|
|
|
72
64
|
for cat, patterns in TOOL_CATEGORIES.items():
|
|
73
65
|
if any(tool_name.startswith(p) or p in tool_name for p in patterns):
|
|
74
66
|
return 0.0 if cat in allowed_categories else 1.0
|
|
75
|
-
return 0.3
|
|
67
|
+
return 0.3
|
|
76
68
|
|
|
77
69
|
|
|
78
70
|
class ScopeCreepDrift:
|
|
79
71
|
"""维度三:范围蔓延"""
|
|
80
72
|
|
|
81
73
|
def score(self, node_entities: set[str], baseline_entities: set[str]) -> float:
|
|
82
|
-
# 空基线返回 0 是有意设计:范围蔓延是"相对基线的偏离",没有基线参照就无从
|
|
83
|
-
# 谈蔓延。若此处对空基线返回高分,会让所有尚未建立实体基线的早期任务被误判为
|
|
84
|
-
# 范围越界(误报)。范围检测的有效性依赖基线实体提取——若该维度长期为 0,
|
|
85
|
-
# 应排查实体提取(register_goal 的 entities)是否为空,而非改动本函数。
|
|
86
74
|
if not baseline_entities:
|
|
87
75
|
return 0.0
|
|
88
76
|
new_entities = node_entities - baseline_entities
|
|
@@ -5,25 +5,22 @@ from typing import Callable
|
|
|
5
5
|
class HarnessEventBus:
|
|
6
6
|
"""阶段一:事件总线(观测优先)"""
|
|
7
7
|
|
|
8
|
-
# 高频且纯内部的信号不上脊柱,避免 WS 噪音(见审计"哪些事件不应该广播")
|
|
9
8
|
_SPINE_SKIP = {"goal_registered"}
|
|
10
9
|
|
|
11
10
|
def __init__(self, spine=None, source: str = ""):
|
|
12
11
|
self._handlers: dict[str, list[Callable]] = {}
|
|
13
|
-
self._spine = spine
|
|
14
|
-
self._source = source
|
|
12
|
+
self._spine = spine
|
|
13
|
+
self._source = source
|
|
15
14
|
|
|
16
15
|
def on(self, event: str, handler: Callable):
|
|
17
16
|
self._handlers.setdefault(event, []).append(handler)
|
|
18
17
|
|
|
19
18
|
def emit(self, event: str, data: dict):
|
|
20
|
-
# 1) 本地订阅者(向后兼容:agent.harness.bus.on / harness 自订阅 / governance)
|
|
21
19
|
for h in self._handlers.get(event, []):
|
|
22
20
|
try:
|
|
23
21
|
h(data)
|
|
24
22
|
except Exception as e:
|
|
25
23
|
print(f"[Harness EventBus] handler error: {e}")
|
|
26
|
-
# 2) 汇入全局脊柱:harness 事件与系统事件走同一条脊柱、被同一 WS 投影观测
|
|
27
24
|
if self._spine is not None and event not in self._SPINE_SKIP:
|
|
28
25
|
try:
|
|
29
26
|
self._spine.publish(event, data, source=self._source or "harness")
|