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
|
@@ -45,8 +45,6 @@ from functools import lru_cache
|
|
|
45
45
|
from pathlib import Path
|
|
46
46
|
from typing import Any, Iterable, Mapping, Sequence
|
|
47
47
|
|
|
48
|
-
# provider → 需要的环境变量(与 core/model_router.PROVIDER_ENV 保持一致;
|
|
49
|
-
# 这里重复一份是为了让 settings 不依赖 model_router —— 那会拖入 LLM SDK 的导入)。
|
|
50
48
|
PROVIDER_KEY_ENV: dict[str, str] = {
|
|
51
49
|
"anthropic": "ANTHROPIC_API_KEY",
|
|
52
50
|
"openai": "OPENAI_API_KEY",
|
|
@@ -58,8 +56,6 @@ PROVIDER_KEY_ENV: dict[str, str] = {
|
|
|
58
56
|
"generic": "LLM_API_KEY",
|
|
59
57
|
}
|
|
60
58
|
|
|
61
|
-
# 只检查这些前缀下的"未知键":它们属于本项目或本项目专用的厂商约定,
|
|
62
|
-
# 出现没人读的名字几乎必然是拼错。像 `PATH` / `HOME` 这类系统变量不在此列。
|
|
63
59
|
OWNED_PREFIXES = (
|
|
64
60
|
"FOXCLAW_", "BAIDU_", "AMAP_", "LLM_", "GENERIC_LLM_",
|
|
65
61
|
"MINIMAX_", "LOCAL_LLM_", "HKC_", "HMR_",
|
|
@@ -75,10 +71,10 @@ class Opt:
|
|
|
75
71
|
"""一个环境变量的声明。kind 只描述**解析方式**,不代表业务语义。"""
|
|
76
72
|
|
|
77
73
|
name: str
|
|
78
|
-
kind: str = "str"
|
|
74
|
+
kind: str = "str"
|
|
79
75
|
default: Any = ""
|
|
80
76
|
group: str = "其他"
|
|
81
|
-
hint: str = ""
|
|
77
|
+
hint: str = ""
|
|
82
78
|
choices: Sequence[str] = ()
|
|
83
79
|
secret: bool = False
|
|
84
80
|
lo: float | None = None
|
|
@@ -86,10 +82,6 @@ class Opt:
|
|
|
86
82
|
|
|
87
83
|
|
|
88
84
|
SPECS: tuple[Opt, ...] = (
|
|
89
|
-
# lo/hi 只给**调用点自己不兜底**的项。像 TRACE_RETAIN 已有 max(100, ...)、
|
|
90
|
-
# BAIDU_SEARCH_TIMEOUT 已有 max(5, ...):在那里再报一次范围错,等于把"clamp"
|
|
91
|
-
# 变成"整条配置作废退默认",反而更糟。
|
|
92
|
-
# ── 模型 ────────────────────────────────────────────────────────────────
|
|
93
85
|
Opt("LLM_PROVIDER", "choice", "", "模型",
|
|
94
86
|
"anthropic / openai / deepseek / google / qwen / local / minimax / generic",
|
|
95
87
|
tuple(PROVIDER_KEY_ENV)),
|
|
@@ -109,9 +101,10 @@ SPECS: tuple[Opt, ...] = (
|
|
|
109
101
|
Opt("LLM_API_KEY", "str", "", "模型", "generic(通用 openai 兼容)的 key", secret=True),
|
|
110
102
|
Opt("LLM_BASE_URL", "str", "", "模型", "generic 的 base_url,如 https://host/v1"),
|
|
111
103
|
Opt("LOCAL_LLM_BASE", "str", "", "模型", "本地推理服务的 base_url"),
|
|
112
|
-
# ── 服务 ────────────────────────────────────────────────────────────────
|
|
113
104
|
Opt("FOXCLAW_PORT", "int", 8000, "服务", "HTTP 监听端口", lo=1, hi=65535),
|
|
114
105
|
Opt("FOXCLAW_DATA_DIR", "str", "", "服务", "数据目录(默认 <仓库>/foxclaw_data)"),
|
|
106
|
+
Opt("FOXCLAW_CLAWHUB_REGISTRY", "str", "", "服务",
|
|
107
|
+
"ClawHub 地址(默认 https://clawhub.ai;内网镜像或自建 registry 填这里)"),
|
|
115
108
|
Opt("FOXCLAW_IMPORT_ROOTS", "csv", "", "服务",
|
|
116
109
|
"额外允许的导入目录(逗号分隔);不配则只有数据目录内的文件可被导入"),
|
|
117
110
|
Opt("FOXCLAW_API_KEY", "str", "", "服务",
|
|
@@ -127,7 +120,6 @@ SPECS: tuple[Opt, ...] = (
|
|
|
127
120
|
Opt("FOXCLAW_STREAM_RATE", "int", 60, "服务", "流式端点每分钟上限", lo=0),
|
|
128
121
|
Opt("FOXCLAW_STREAM_CONCURRENCY", "int", 4, "服务", "流式并发上限", lo=1),
|
|
129
122
|
Opt("FOXCLAW_SHADOW_ROUNDS", "int", 20, "服务", "影子推演轮数", lo=1),
|
|
130
|
-
# ── 工具 / 追踪 ─────────────────────────────────────────────────────────
|
|
131
123
|
Opt("FOXCLAW_TOOL_TIMEOUT", "float", 45.0, "工具",
|
|
132
124
|
"单个工具调用硬超时(秒);可小于 1(测试用),调用点会兜 >0"),
|
|
133
125
|
Opt("FOXCLAW_TRACE_SLOW_MS", "float", 1000.0, "追踪", "超过多少毫秒算慢请求", lo=0),
|
|
@@ -135,12 +127,10 @@ SPECS: tuple[Opt, ...] = (
|
|
|
135
127
|
"非慢请求的采样率 0~1;超出范围由调用点 clamp,不在这里报错"),
|
|
136
128
|
Opt("FOXCLAW_TRACE_RETAIN", "int", 5000, "追踪",
|
|
137
129
|
"内存里保留的 trace 条数;调用点已有 max(100, ...) 兜底"),
|
|
138
|
-
# ── 搜索 ────────────────────────────────────────────────────────────────
|
|
139
130
|
Opt("BAIDU_SEARCH_KEY", "str", "", "搜索", "百度 AI 搜索的 key", secret=True),
|
|
140
131
|
Opt("BAIDU_SEARCH_MODEL", "str", "", "搜索", "百度搜索用的模型名"),
|
|
141
132
|
Opt("BAIDU_SEARCH_BASE", "str", "", "搜索", "百度搜索的 base_url"),
|
|
142
133
|
Opt("BAIDU_SEARCH_TIMEOUT", "int", 45, "搜索", "秒;调用点已有 max(5, ...) 兜底"),
|
|
143
|
-
# ── 渠道 ────────────────────────────────────────────────────────────────
|
|
144
134
|
Opt("FOXCLAW_CHANNEL_TRUSTED_SENDERS", "csv", "", "渠道",
|
|
145
135
|
"逗号分隔的可信发送者:<id> / <channel>:<id> / <channel>:*"),
|
|
146
136
|
Opt("FOXCLAW_CHANNEL_TRUST_ALL", "str", "", "渠道",
|
|
@@ -148,27 +138,24 @@ SPECS: tuple[Opt, ...] = (
|
|
|
148
138
|
Opt("FOXCLAW_CHANNEL_SIGNING_SECRET", "str", "", "渠道",
|
|
149
139
|
"出站 HMAC 密钥;留空 = 不签", secret=True),
|
|
150
140
|
Opt("FOXCLAW_CHANNEL_SIGNING_KEY_ID", "str", "", "渠道", "密钥轮换标识"),
|
|
151
|
-
# ── 浏览器 ──────────────────────────────────────────────────────────────
|
|
152
141
|
Opt("FOXCLAW_BROWSER_ALLOW_PRIVATE", "str", "", "浏览器",
|
|
153
142
|
"1/true/yes 才允许访问内网地址"),
|
|
154
143
|
)
|
|
155
144
|
|
|
156
145
|
SPEC_BY_NAME: dict[str, Opt] = {o.name: o for o in SPECS}
|
|
157
146
|
|
|
158
|
-
# 会在启动/运行早期被读、且类型写错就会崩的少数几个 —— 供 serve 预检快速拦截。
|
|
159
147
|
FATAL_CODES = ("type", "choice", "range")
|
|
160
148
|
|
|
161
149
|
|
|
162
150
|
@dataclass
|
|
163
151
|
class Issue:
|
|
164
|
-
level: str
|
|
165
|
-
code: str
|
|
152
|
+
level: str
|
|
153
|
+
code: str
|
|
166
154
|
name: str
|
|
167
155
|
message: str
|
|
168
|
-
hint: str = ""
|
|
156
|
+
hint: str = ""
|
|
169
157
|
|
|
170
158
|
def __str__(self) -> str:
|
|
171
|
-
# 只用 ASCII 记号:Windows 控制台默认 GBK,✓/✗/⚠ 会抛 UnicodeEncodeError
|
|
172
159
|
mark = "ERROR" if self.level == "error" else " WARN"
|
|
173
160
|
head = f"[{mark}] {self.name}: {self.message}" if self.name else f"[{mark}] {self.message}"
|
|
174
161
|
return f"{head}\n → {self.hint}" if self.hint else head
|
|
@@ -178,7 +165,7 @@ def _caller() -> str:
|
|
|
178
165
|
"""当前 `get_*` 的调用点(文件:行)。只在出错路径上求值,不进热路径。"""
|
|
179
166
|
try:
|
|
180
167
|
f = inspect.currentframe()
|
|
181
|
-
for _ in range(3):
|
|
168
|
+
for _ in range(3):
|
|
182
169
|
if f is None:
|
|
183
170
|
return ""
|
|
184
171
|
f = f.f_back
|
|
@@ -210,16 +197,16 @@ def _parse(opt: Opt, raw: str, where: str = "") -> Any:
|
|
|
210
197
|
return value
|
|
211
198
|
|
|
212
199
|
if opt.kind == "choice":
|
|
213
|
-
|
|
214
|
-
if
|
|
200
|
+
choice = text.lower()
|
|
201
|
+
if choice not in opt.choices:
|
|
215
202
|
raise ConfigError(
|
|
216
203
|
f'值 "{raw}" 不在允许范围内。可选:{" / ".join(opt.choices)}{suffix}')
|
|
217
|
-
return
|
|
204
|
+
return choice
|
|
218
205
|
|
|
219
206
|
if opt.kind == "csv":
|
|
220
207
|
return [s.strip() for s in text.split(",") if s.strip()]
|
|
221
208
|
|
|
222
|
-
return raw
|
|
209
|
+
return raw
|
|
223
210
|
|
|
224
211
|
|
|
225
212
|
def _opt(name: str) -> Opt:
|
|
@@ -251,7 +238,7 @@ _warned: set[str] = set()
|
|
|
251
238
|
_log = logging.getLogger("foxclaw.config")
|
|
252
239
|
|
|
253
240
|
|
|
254
|
-
def get_safe(name: str, default: Any = None) -> Any:
|
|
241
|
+
def get_safe(name: str, default: Any | None = None) -> Any:
|
|
255
242
|
"""可降级的读取:值非法时**退默认值并要求日志警告一次**,不抛异常。
|
|
256
243
|
|
|
257
244
|
给"配置错不该拖垮主流程"的地方用(trace 采样、工具超时、搜索超时):这些点
|
|
@@ -262,12 +249,12 @@ def get_safe(name: str, default: Any = None) -> Any:
|
|
|
262
249
|
fallback = opt.default if default is None else default
|
|
263
250
|
raw = os.environ.get(name)
|
|
264
251
|
if raw is None or not raw.strip():
|
|
265
|
-
return fallback
|
|
252
|
+
return fallback
|
|
266
253
|
try:
|
|
267
254
|
return _parse(opt, raw, _caller())
|
|
268
255
|
except ConfigError as e:
|
|
269
256
|
if name not in _warned:
|
|
270
|
-
_warned.add(name)
|
|
257
|
+
_warned.add(name)
|
|
271
258
|
_log.warning("[config] %s", e)
|
|
272
259
|
return fallback
|
|
273
260
|
|
|
@@ -297,8 +284,6 @@ def known_keys() -> frozenset[str]:
|
|
|
297
284
|
只在 `check()` / `show()` 这类诊断路径调用,不进请求路径;结果缓存在本进程内。
|
|
298
285
|
"""
|
|
299
286
|
pkg = Path(__file__).resolve().parent.parent
|
|
300
|
-
# vendor/(HKC/HMR)在仓库根,不在包内:它们自己读 HMR_*/HKC_* 变量,不一起扫就会把
|
|
301
|
-
# 正常配置报成"拼错的键"。wheel 里没有 vendor/ → 扫描自然为空,不影响安装后的行为。
|
|
302
287
|
roots = [pkg] + ([pkg.parent / "vendor"] if (pkg.parent / "vendor").is_dir() else [])
|
|
303
288
|
found: set[str] = set(SPEC_BY_NAME)
|
|
304
289
|
files: list[Path] = []
|
|
@@ -308,7 +293,7 @@ def known_keys() -> frozenset[str]:
|
|
|
308
293
|
for path in files:
|
|
309
294
|
try:
|
|
310
295
|
tree = ast.parse(path.read_text(encoding="utf-8", errors="replace"))
|
|
311
|
-
except SyntaxError:
|
|
296
|
+
except SyntaxError:
|
|
312
297
|
continue
|
|
313
298
|
for node in ast.walk(tree):
|
|
314
299
|
if isinstance(node, ast.Call):
|
|
@@ -401,7 +386,6 @@ def check(env: Mapping[str, str] | None = None) -> list[Issue]:
|
|
|
401
386
|
except ConfigError as e:
|
|
402
387
|
code = "choice" if opt.kind == "choice" else (
|
|
403
388
|
"range" if ("下限" in str(e) or "上限" in str(e)) else "type")
|
|
404
|
-
# 密钥不回显原值,免得把半截口令打进日志
|
|
405
389
|
msg = str(e)
|
|
406
390
|
if is_secret(opt.name):
|
|
407
391
|
msg = f"值 {mask(raw)} 不合法(原始值已脱敏)"
|
|
@@ -42,21 +42,12 @@ from typing import Iterable, Optional
|
|
|
42
42
|
MODE_ENV = "FOXCLAW_SHARED_WRITE_MODE"
|
|
43
43
|
VALID_MODES = ("audit", "warn", "enforce")
|
|
44
44
|
|
|
45
|
-
# 与写入方无关、任何 Agent 都可写的 facet(设计上就该跨 Agent 共享的东西):
|
|
46
|
-
# preference —— 用户偏好:base.py 明确以 shared=True 写入,意在跨会话跨 Agent 生效
|
|
47
|
-
# truth_pipeline —— 真值管线沉淀(truth_pipeline.py),写入方是 agent_id="system"
|
|
48
45
|
GLOBAL_ALLOWED_FACETS = frozenset({"preference", "truth_pipeline"})
|
|
49
46
|
|
|
50
|
-
# 各 Agent 的「职责域」允许 facet。facet 取自一次写入的 {type:<记忆类型>} ∪ tags,
|
|
51
|
-
# 支持 fnmatch 通配(如 finance.* 可匹配 finance.budget)。
|
|
52
|
-
#
|
|
53
|
-
# 注意:这是**起始假设**,不是已验证的结论 —— 默认 audit 模式的目的正是用真实流量验证它。
|
|
54
47
|
DEFAULT_SHARED_WRITE_ALLOWED = {
|
|
55
|
-
"supervisor": {"*"},
|
|
56
|
-
"system": {"*"},
|
|
48
|
+
"supervisor": {"*"},
|
|
49
|
+
"system": {"*"},
|
|
57
50
|
"finance": {"finance.*"},
|
|
58
|
-
# 行政并入办公后,办公学习 Agent 同时拥有学习与行程两个写域;
|
|
59
|
-
# 助手已下放为技能(不再有独立 agent_id),不再单列 ACL 条目。
|
|
60
51
|
"office": {"learning.*", "itinerary.*", "schedule.*"},
|
|
61
52
|
}
|
|
62
53
|
|
|
@@ -90,8 +81,8 @@ class SharedWritePolicy:
|
|
|
90
81
|
无状态、无锁、纯函数式判定 —— 并发安全直接成立,也便于单独测试。
|
|
91
82
|
"""
|
|
92
83
|
|
|
93
|
-
def __init__(self, allowed: dict = None, mode: str = None,
|
|
94
|
-
global_facets: Iterable[str] = None):
|
|
84
|
+
def __init__(self, allowed: dict | None = None, mode: str | None = None,
|
|
85
|
+
global_facets: Iterable[str] | None = None):
|
|
95
86
|
self.allowed = {k: set(v) for k, v in
|
|
96
87
|
(allowed or DEFAULT_SHARED_WRITE_ALLOWED).items()}
|
|
97
88
|
self.global_facets = set(global_facets if global_facets is not None
|
|
@@ -99,13 +90,11 @@ class SharedWritePolicy:
|
|
|
99
90
|
self.mode = self._resolve_mode(mode)
|
|
100
91
|
|
|
101
92
|
@staticmethod
|
|
102
|
-
def _resolve_mode(mode: str = None) -> str:
|
|
93
|
+
def _resolve_mode(mode: str | None = None) -> str:
|
|
103
94
|
"""显式入参 > 环境变量 > 默认 audit。非法值一律回落到 audit(保守:不拦截)。"""
|
|
104
95
|
m = (mode or os.environ.get(MODE_ENV, "") or "audit").strip().lower()
|
|
105
96
|
return m if m in VALID_MODES else "audit"
|
|
106
97
|
|
|
107
|
-
# 纯标记类 tag:描述"这条记忆的归属/去向",不描述"内容是什么"。
|
|
108
|
-
# 让它们参与内容域匹配只会稀释策略表 —— 尤其 "shared" 在本闸门语境下恒为真,零信息量。
|
|
109
98
|
_MARKER_TAGS = ("agent:", "shared")
|
|
110
99
|
|
|
111
100
|
@staticmethod
|
|
@@ -126,9 +115,9 @@ class SharedWritePolicy:
|
|
|
126
115
|
out.append(t)
|
|
127
116
|
return tuple(dict.fromkeys(out))
|
|
128
117
|
|
|
129
|
-
def evaluate(self, agent_id: str = None, memory_type: str = "concept",
|
|
130
|
-
tags: Iterable[str] = None) -> Decision:
|
|
131
|
-
facets = self.facets_of(memory_type, tags)
|
|
118
|
+
def evaluate(self, agent_id: str | None = None, memory_type: str = "concept",
|
|
119
|
+
tags: Iterable[str] | None = None) -> Decision:
|
|
120
|
+
facets = self.facets_of(memory_type, tags or ())
|
|
132
121
|
who = agent_id or "unknown"
|
|
133
122
|
|
|
134
123
|
if any(fnmatch.fnmatchcase(f, p) for f in facets for p in self.global_facets):
|
|
@@ -142,9 +131,9 @@ class SharedWritePolicy:
|
|
|
142
131
|
return Decision(False, f"{who} 的职责域不含 {list(facets)}", self.mode, facets)
|
|
143
132
|
|
|
144
133
|
|
|
145
|
-
def guard_shared_write(backend, agent_id: str, content: str,
|
|
134
|
+
def guard_shared_write(backend, agent_id: str | None, content: str,
|
|
146
135
|
memory_type: str = "concept",
|
|
147
|
-
tags: Iterable[str] = None) -> Optional[Decision]:
|
|
136
|
+
tags: Iterable[str] | None = None) -> Optional[Decision]:
|
|
148
137
|
"""共享层写入的统一闸门 —— 必须在**真正落库之前**调用。
|
|
149
138
|
|
|
150
139
|
行为分三档:
|
|
@@ -170,7 +159,6 @@ def guard_shared_write(backend, agent_id: str, content: str,
|
|
|
170
159
|
"allowed": decision.allowed,
|
|
171
160
|
"reason": decision.reason,
|
|
172
161
|
"mode": decision.mode,
|
|
173
|
-
# 只留摘要哈希 + 短预览,不把共享层正文原样灌进事件流
|
|
174
162
|
"content_sha": hashlib.sha256(
|
|
175
163
|
str(content or "").encode("utf-8")).hexdigest()[:16],
|
|
176
164
|
"content_preview": str(content or "")[:60],
|
|
@@ -187,7 +175,7 @@ def guard_shared_write(backend, agent_id: str, content: str,
|
|
|
187
175
|
return decision
|
|
188
176
|
|
|
189
177
|
|
|
190
|
-
def attach(backend, policy: SharedWritePolicy = None, spine=None) -> SharedWritePolicy:
|
|
178
|
+
def attach(backend, policy: SharedWritePolicy | None = None, spine=None) -> SharedWritePolicy:
|
|
191
179
|
"""把策略挂到记忆后端(HMRAdapter / MockHMR 皆可)。返回生效的策略实例。
|
|
192
180
|
|
|
193
181
|
用挂载而非构造函数注入,是因为装配顺序:memory_adapter 在 event_spine **之前**创建,
|
|
@@ -21,17 +21,18 @@ import threading
|
|
|
21
21
|
import time
|
|
22
22
|
import uuid
|
|
23
23
|
from dataclasses import dataclass, field
|
|
24
|
+
from typing import Any
|
|
24
25
|
|
|
25
26
|
|
|
26
27
|
@dataclass
|
|
27
28
|
class SteeringItem:
|
|
28
29
|
item_id: str
|
|
29
|
-
requester: str
|
|
30
|
-
source_agent: str
|
|
31
|
-
payload: dict
|
|
30
|
+
requester: str
|
|
31
|
+
source_agent: str
|
|
32
|
+
payload: dict
|
|
32
33
|
created: float = field(default_factory=time.time)
|
|
33
|
-
leased_until: float = 0.0
|
|
34
|
-
lease_id: str = ""
|
|
34
|
+
leased_until: float = 0.0
|
|
35
|
+
lease_id: str = ""
|
|
35
36
|
|
|
36
37
|
@property
|
|
37
38
|
def is_leased(self) -> bool:
|
|
@@ -60,7 +61,7 @@ class SteeringQueue:
|
|
|
60
61
|
source_agent=source_agent, payload=payload)
|
|
61
62
|
return item_id
|
|
62
63
|
|
|
63
|
-
def lease(self, requester: str = None, max_items: int = 10) -> list[SteeringItem]:
|
|
64
|
+
def lease(self, requester: str | None = None, max_items: int = 10) -> list[SteeringItem]:
|
|
64
65
|
"""租走一批待处理结果(可按 requester 过滤)。
|
|
65
66
|
|
|
66
67
|
原子地:挑出未被租用(或租约已过期)的项,标记为 leased,返回。
|
|
@@ -69,7 +70,7 @@ class SteeringQueue:
|
|
|
69
70
|
with self._lock:
|
|
70
71
|
self._reclaim_expired()
|
|
71
72
|
now = time.time()
|
|
72
|
-
leased = []
|
|
73
|
+
leased: list[Any] = []
|
|
73
74
|
lease_id = uuid.uuid4().hex[:12]
|
|
74
75
|
for item in self._items.values():
|
|
75
76
|
if len(leased) >= max_items:
|
|
@@ -83,7 +84,7 @@ class SteeringQueue:
|
|
|
83
84
|
leased.append(item)
|
|
84
85
|
return leased
|
|
85
86
|
|
|
86
|
-
def ack(self, item_id: str, lease_id: str = None) -> bool:
|
|
87
|
+
def ack(self, item_id: str, lease_id: str | None = None) -> bool:
|
|
87
88
|
"""消费者确认处理完 → 移除该项。
|
|
88
89
|
|
|
89
90
|
校验 lease_id:只有持有当前租约的消费者能 ack(防止过期租约误删
|
|
@@ -94,7 +95,7 @@ class SteeringQueue:
|
|
|
94
95
|
if item is None:
|
|
95
96
|
return False
|
|
96
97
|
if lease_id is not None and item.lease_id != lease_id:
|
|
97
|
-
return False
|
|
98
|
+
return False
|
|
98
99
|
del self._items[item_id]
|
|
99
100
|
return True
|
|
100
101
|
|
|
@@ -114,7 +115,7 @@ class SteeringQueue:
|
|
|
114
115
|
item.leased_until = 0.0
|
|
115
116
|
item.lease_id = ""
|
|
116
117
|
|
|
117
|
-
def pending_count(self, requester: str = None) -> int:
|
|
118
|
+
def pending_count(self, requester: str | None = None) -> int:
|
|
118
119
|
"""待处理(未被有效租用)的项数。"""
|
|
119
120
|
with self._lock:
|
|
120
121
|
self._reclaim_expired()
|
|
@@ -145,7 +146,7 @@ class SteeringQueue:
|
|
|
145
146
|
if item.requester != requester:
|
|
146
147
|
continue
|
|
147
148
|
if item.is_leased:
|
|
148
|
-
continue
|
|
149
|
+
continue
|
|
149
150
|
out.append(item.payload)
|
|
150
151
|
to_del.append(item_id)
|
|
151
152
|
for item_id in to_del:
|
|
@@ -73,7 +73,7 @@ class SystemBus:
|
|
|
73
73
|
def publish(self, channel: str, payload: dict):
|
|
74
74
|
return self.spine.publish(channel, payload, source="system_bus")
|
|
75
75
|
|
|
76
|
-
def history(self, channel: str = None, limit: int = 50,
|
|
76
|
+
def history(self, channel: str | None = None, limit: int = 50,
|
|
77
77
|
exact: bool = False) -> list:
|
|
78
78
|
"""历史事件(dict 列表)。exact=True 时只返回 topic 完全匹配的事件
|
|
79
79
|
(默认前缀匹配,与 EventSpine 原生一致)。"""
|
|
@@ -16,16 +16,16 @@ from pathlib import Path
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
NodeType = Literal[
|
|
19
|
-
"goal",
|
|
20
|
-
"reasoning",
|
|
21
|
-
"memory_retrieval",
|
|
22
|
-
"tool_intent",
|
|
23
|
-
"tool_use",
|
|
24
|
-
"plan",
|
|
25
|
-
"decision",
|
|
26
|
-
"policy_update",
|
|
27
|
-
"consolidation",
|
|
28
|
-
"reflection",
|
|
19
|
+
"goal",
|
|
20
|
+
"reasoning",
|
|
21
|
+
"memory_retrieval",
|
|
22
|
+
"tool_intent",
|
|
23
|
+
"tool_use",
|
|
24
|
+
"plan",
|
|
25
|
+
"decision",
|
|
26
|
+
"policy_update",
|
|
27
|
+
"consolidation",
|
|
28
|
+
"reflection",
|
|
29
29
|
]
|
|
30
30
|
|
|
31
31
|
HarnessStatus = Literal["BASELINE", "PASS", "WARN", "BLOCK", "PENDING", "REGRESSION"]
|
|
@@ -39,15 +39,15 @@ class ThoughtNode:
|
|
|
39
39
|
agent_id: str
|
|
40
40
|
type: NodeType
|
|
41
41
|
content: str
|
|
42
|
-
conf: float
|
|
43
|
-
mem_refs: list[str]
|
|
44
|
-
parent_ids: list[str]
|
|
42
|
+
conf: float
|
|
43
|
+
mem_refs: list[str]
|
|
44
|
+
parent_ids: list[str]
|
|
45
45
|
harness_status: HarnessStatus
|
|
46
|
-
harness_reason: Optional[str]
|
|
47
|
-
harness_delta: Optional[float]
|
|
48
|
-
dominant_dim: Optional[str]
|
|
49
|
-
tool_name: Optional[str]
|
|
50
|
-
entities: list[str]
|
|
46
|
+
harness_reason: Optional[str]
|
|
47
|
+
harness_delta: Optional[float]
|
|
48
|
+
dominant_dim: Optional[str]
|
|
49
|
+
tool_name: Optional[str]
|
|
50
|
+
entities: list[str]
|
|
51
51
|
timestamp: str
|
|
52
52
|
metadata: dict
|
|
53
53
|
|
|
@@ -59,11 +59,11 @@ class ThoughtNode:
|
|
|
59
59
|
type: NodeType,
|
|
60
60
|
content: str,
|
|
61
61
|
conf: float = 1.0,
|
|
62
|
-
mem_refs: list[str] = None,
|
|
63
|
-
parent_ids: list[str] = None,
|
|
64
|
-
tool_name: str = None,
|
|
65
|
-
entities: list[str] = None,
|
|
66
|
-
metadata: dict = None,
|
|
62
|
+
mem_refs: list[str] | None = None,
|
|
63
|
+
parent_ids: list[str] | None = None,
|
|
64
|
+
tool_name: str | None = None,
|
|
65
|
+
entities: list[str] | None = None,
|
|
66
|
+
metadata: dict | None = None,
|
|
67
67
|
) -> "ThoughtNode":
|
|
68
68
|
return cls(
|
|
69
69
|
id=f"tn_{uuid.uuid4().hex[:12]}",
|
|
@@ -150,7 +150,7 @@ class ThoughtChain:
|
|
|
150
150
|
try:
|
|
151
151
|
conn.execute("PRAGMA journal_mode=WAL")
|
|
152
152
|
except sqlite3.OperationalError:
|
|
153
|
-
pass
|
|
153
|
+
pass
|
|
154
154
|
try:
|
|
155
155
|
yield conn
|
|
156
156
|
conn.commit()
|
|
@@ -183,9 +183,9 @@ class ThoughtChain:
|
|
|
183
183
|
self,
|
|
184
184
|
node_id: str,
|
|
185
185
|
status: HarnessStatus,
|
|
186
|
-
reason: str = None,
|
|
187
|
-
delta: float = None,
|
|
188
|
-
dominant_dim: str = None,
|
|
186
|
+
reason: str | None = None,
|
|
187
|
+
delta: float | None = None,
|
|
188
|
+
dominant_dim: str | None = None,
|
|
189
189
|
):
|
|
190
190
|
"""Harness 校验完成后更新节点标注"""
|
|
191
191
|
with self._conn() as conn:
|
|
@@ -202,7 +202,7 @@ class ThoughtChain:
|
|
|
202
202
|
).fetchone()
|
|
203
203
|
return self._row_to_node(row) if row else None
|
|
204
204
|
|
|
205
|
-
def get_chain(self, session_id: str, agent_id: str = None) -> list[ThoughtNode]:
|
|
205
|
+
def get_chain(self, session_id: str, agent_id: str | None = None) -> list[ThoughtNode]:
|
|
206
206
|
"""获取一个会话的完整推理链,按时间排序"""
|
|
207
207
|
with self._conn() as conn:
|
|
208
208
|
if agent_id:
|
|
@@ -217,7 +217,7 @@ class ThoughtChain:
|
|
|
217
217
|
).fetchall()
|
|
218
218
|
return [self._row_to_node(r) for r in rows]
|
|
219
219
|
|
|
220
|
-
def get_blocked_nodes(self, agent_id: str = None, limit: int = 50) -> list[ThoughtNode]:
|
|
220
|
+
def get_blocked_nodes(self, agent_id: str | None = None, limit: int = 50) -> list[ThoughtNode]:
|
|
221
221
|
"""获取被 Harness 截断的节点,用于审计"""
|
|
222
222
|
with self._conn() as conn:
|
|
223
223
|
if agent_id:
|
|
@@ -232,7 +232,7 @@ class ThoughtChain:
|
|
|
232
232
|
).fetchall()
|
|
233
233
|
return [self._row_to_node(r) for r in rows]
|
|
234
234
|
|
|
235
|
-
def search_nodes(self, keyword: str, agent_id: str = None,
|
|
235
|
+
def search_nodes(self, keyword: str, agent_id: str | None = None,
|
|
236
236
|
limit: int = 20) -> list[ThoughtNode]:
|
|
237
237
|
"""按关键词在推理节点内容里检索(LIKE 子串匹配)。供 HSR 的 AgentSourcePort
|
|
238
238
|
把「各 Agent 想过/判过什么」连通进内部搜索。只读。"""
|
|
@@ -307,8 +307,6 @@ class ThoughtChain:
|
|
|
307
307
|
"timestamp", "metadata"
|
|
308
308
|
]
|
|
309
309
|
d = dict(zip(cols, row))
|
|
310
|
-
# JSON 字段容错:单行某字段损坏时降级为默认值,而非让整个审计链读取崩溃。
|
|
311
|
-
# 审计系统的可靠性要求"一条坏数据不拖垮全部"。
|
|
312
310
|
for field, default in (("mem_refs", []), ("parent_ids", []),
|
|
313
311
|
("entities", []), ("metadata", {})):
|
|
314
312
|
try:
|
|
@@ -36,7 +36,6 @@ def estimate_tokens(text: str, model: str = "") -> int:
|
|
|
36
36
|
enc = tiktoken.get_encoding("cl100k_base")
|
|
37
37
|
return len(enc.encode(text))
|
|
38
38
|
except Exception:
|
|
39
|
-
# 字符估算:中文(非ASCII)按 1.1 字符/token,英文(ASCII)按 4 字符/token。
|
|
40
39
|
ascii_chars = sum(1 for c in text if ord(c) < 128)
|
|
41
40
|
cjk_chars = len(text) - ascii_chars
|
|
42
41
|
return int(ascii_chars / 4 + cjk_chars * 1.1) + 1
|
|
@@ -50,8 +49,8 @@ class CallRecord:
|
|
|
50
49
|
agent_id: str
|
|
51
50
|
input_tokens: int
|
|
52
51
|
output_tokens: int
|
|
53
|
-
estimated: bool
|
|
54
|
-
task_kind: str = ""
|
|
52
|
+
estimated: bool
|
|
53
|
+
task_kind: str = ""
|
|
55
54
|
|
|
56
55
|
@property
|
|
57
56
|
def total_tokens(self) -> int:
|
|
@@ -66,14 +65,14 @@ class CallRecord:
|
|
|
66
65
|
class TokenMeter:
|
|
67
66
|
"""Token 计量器。线程安全,可聚合,可落盘。"""
|
|
68
67
|
|
|
69
|
-
def __init__(self, log_path: str = None, max_records: int = 10000,
|
|
68
|
+
def __init__(self, log_path: str | None = None, max_records: int = 10000,
|
|
70
69
|
max_log_mb: float = 10.0):
|
|
71
70
|
self.log_path = log_path
|
|
72
71
|
self.max_records = max_records
|
|
73
72
|
self.max_log_bytes = int(max_log_mb * 1024 * 1024)
|
|
74
73
|
self._records: list[CallRecord] = []
|
|
75
74
|
self._lock = threading.RLock()
|
|
76
|
-
self._append_count = 0
|
|
75
|
+
self._append_count = 0
|
|
77
76
|
if log_path:
|
|
78
77
|
os.makedirs(os.path.dirname(log_path) or ".", exist_ok=True)
|
|
79
78
|
|
|
@@ -94,7 +93,7 @@ class TokenMeter:
|
|
|
94
93
|
|
|
95
94
|
def measure_call(self, *, system: str, messages: list, output: str,
|
|
96
95
|
provider: str, model: str, agent_id: str = "",
|
|
97
|
-
usage: dict = None, task_kind: str = "") -> CallRecord:
|
|
96
|
+
usage: dict | None = None, task_kind: str = "") -> CallRecord:
|
|
98
97
|
"""计量一次调用。优先用 API 返回的 usage,否则估算。
|
|
99
98
|
|
|
100
99
|
usage 形如 {"input_tokens": N, "output_tokens": M}(已由 router 归一化)。
|
|
@@ -104,7 +103,6 @@ class TokenMeter:
|
|
|
104
103
|
usage.get("input_tokens", 0),
|
|
105
104
|
usage.get("output_tokens", 0),
|
|
106
105
|
estimated=False, task_kind=task_kind)
|
|
107
|
-
# 估算:输入 = system + 所有 message content,输出 = output
|
|
108
106
|
input_text = (system or "") + "\n".join(
|
|
109
107
|
str(m.get("content", "")) for m in (messages or []))
|
|
110
108
|
in_tok = estimate_tokens(input_text, model)
|
|
@@ -112,7 +110,6 @@ class TokenMeter:
|
|
|
112
110
|
return self.record(provider, model, agent_id, in_tok, out_tok,
|
|
113
111
|
estimated=True, task_kind=task_kind)
|
|
114
112
|
|
|
115
|
-
# ── 聚合查询 ──────────────────────────────────────────
|
|
116
113
|
def summary(self) -> dict:
|
|
117
114
|
"""总体统计。"""
|
|
118
115
|
with self._lock:
|
|
@@ -211,8 +208,7 @@ class TokenMeter:
|
|
|
211
208
|
with open(self.log_path, "a", encoding="utf-8") as f:
|
|
212
209
|
f.write(json.dumps(rec.to_dict(), ensure_ascii=False) + "\n")
|
|
213
210
|
except Exception:
|
|
214
|
-
pass
|
|
215
|
-
# 每 100 次追加检查一次文件大小,超限则轮转(防磁盘无限膨胀)
|
|
211
|
+
pass
|
|
216
212
|
self._append_count += 1
|
|
217
213
|
if self._append_count % 100 == 0:
|
|
218
214
|
self._rotate_if_needed()
|
|
@@ -226,10 +222,10 @@ class TokenMeter:
|
|
|
226
222
|
return
|
|
227
223
|
with open(self.log_path, encoding="utf-8") as f:
|
|
228
224
|
lines = f.readlines()
|
|
229
|
-
keep = lines[len(lines) // 2:]
|
|
225
|
+
keep = lines[len(lines) // 2:]
|
|
230
226
|
tmp = self.log_path + ".tmp"
|
|
231
227
|
with open(tmp, "w", encoding="utf-8") as f:
|
|
232
228
|
f.writelines(keep)
|
|
233
|
-
os.replace(tmp, self.log_path)
|
|
229
|
+
os.replace(tmp, self.log_path)
|
|
234
230
|
except Exception:
|
|
235
|
-
pass
|
|
231
|
+
pass
|
|
@@ -15,6 +15,7 @@ Foxclaw — 工具注册表(ToolRegistry / P2 function-calling)
|
|
|
15
15
|
"""
|
|
16
16
|
from __future__ import annotations
|
|
17
17
|
import os
|
|
18
|
+
from typing import Any
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
def _tool(name: str, description: str, properties: dict, required: list) -> dict:
|
|
@@ -33,7 +34,6 @@ def _tool(name: str, description: str, properties: dict, required: list) -> dict
|
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
|
|
36
|
-
# ── 首批工具声明(高价值、正好替掉现有关键词坑)──────────────────────────────
|
|
37
37
|
WEB_SEARCH = _tool(
|
|
38
38
|
"web_search",
|
|
39
39
|
"联网搜索实时/时效性的**外部**信息:新闻、产品的版本与型号、发布动态、价格、汇率、"
|
|
@@ -113,15 +113,15 @@ class ToolRegistry:
|
|
|
113
113
|
for k in required:
|
|
114
114
|
if k not in args or args[k] in (None, ""):
|
|
115
115
|
errs.append(f"缺少必填参数 {k}")
|
|
116
|
-
_tmap = {"string": str, "integer": int, "number": (int, float),
|
|
116
|
+
_tmap: dict[str, Any] = {"string": str, "integer": int, "number": (int, float),
|
|
117
117
|
"boolean": bool, "array": list, "object": dict}
|
|
118
118
|
for k, v in args.items():
|
|
119
119
|
spec = props.get(k)
|
|
120
120
|
if not isinstance(spec, dict):
|
|
121
121
|
continue
|
|
122
|
-
|
|
122
|
+
stype = spec.get("type")
|
|
123
|
+
want = _tmap.get(stype) if isinstance(stype, str) else None
|
|
123
124
|
if not want or not isinstance(v, want):
|
|
124
|
-
# bool 是 int 的子类,须排除,否则 True 会被当成合法 integer
|
|
125
125
|
if not (want is not bool and isinstance(v, bool)):
|
|
126
126
|
errs.append(f"参数 {k} 类型应为 {spec.get('type')},"
|
|
127
127
|
f"实为 {type(v).__name__}")
|
|
@@ -143,20 +143,12 @@ def default_registry() -> ToolRegistry:
|
|
|
143
143
|
return ToolRegistry().register(WEB_SEARCH).register(RECALL_MEMORY)
|
|
144
144
|
|
|
145
145
|
|
|
146
|
-
# ── 工具能力分级(供 permission.evaluate 四阶段管线判定)────────────────────────
|
|
147
|
-
# 病根提醒:prompt 级隔离挡不住弱模型被块内强指令诱导(实测 flash ~2/3 被劫持),真正的
|
|
148
|
-
# 安全阀在**动作级**——工具执行前过权限管线,写/副作用/不可逆动作走人在回路(HITL)。
|
|
149
|
-
# 字段对齐 permission.evaluate:side_effect(read/write/send/pay)/ read_only / external / destructive。
|
|
150
|
-
# parallel_safe 不在权限语义里,只用于**同轮多工具是否可并发**:置 True 表示"纯读、可与其他
|
|
151
|
-
# parallel_safe 工具同时跑"(见 agents/mixins/tool_exec.py::_run_tools)。未声明 = 串行。
|
|
152
|
-
# **新增任何工具都应在此声明能力;未声明者 fail-closed 当作有副作用 → 强制 HITL(见 _DEFAULT_CAP)。**
|
|
153
146
|
TOOL_CAPABILITIES: dict[str, dict] = {
|
|
154
147
|
"web_search": {"side_effect": "read", "read_only": True, "external": False,
|
|
155
148
|
"destructive": False, "parallel_safe": True},
|
|
156
149
|
"recall_memory": {"side_effect": "read", "read_only": True, "external": False,
|
|
157
150
|
"destructive": False, "parallel_safe": True},
|
|
158
151
|
}
|
|
159
|
-
# 未声明工具的兜底:当有副作用 + 外部 → permission 管线判 ask → 强制人在回路。宁误拦不漏放。
|
|
160
152
|
_DEFAULT_CAP = {"side_effect": "write", "read_only": False, "external": True, "destructive": False}
|
|
161
153
|
|
|
162
154
|
|