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
|
@@ -50,25 +50,22 @@ def _pid_alive(pid: int) -> bool:
|
|
|
50
50
|
handle = kernel32.OpenProcess(
|
|
51
51
|
PROCESS_QUERY_LIMITED_INFORMATION, False, pid)
|
|
52
52
|
if not handle:
|
|
53
|
-
return False
|
|
53
|
+
return False
|
|
54
54
|
try:
|
|
55
55
|
code = wintypes.DWORD()
|
|
56
56
|
if not kernel32.GetExitCodeProcess(handle, ctypes.byref(code)):
|
|
57
57
|
return False
|
|
58
|
-
# STILL_ACTIVE=259 表示仍在运行。极少数进程恰以 259 退出会被误判存活,
|
|
59
|
-
# 但这是"只读探测"的可接受代价——绝不误杀才是首要目标(psutil 同此取舍)。
|
|
60
58
|
return code.value == STILL_ACTIVE
|
|
61
59
|
finally:
|
|
62
60
|
kernel32.CloseHandle(handle)
|
|
63
61
|
|
|
64
|
-
# POSIX
|
|
65
62
|
try:
|
|
66
63
|
os.kill(pid, 0)
|
|
67
64
|
return True
|
|
68
65
|
except ProcessLookupError:
|
|
69
66
|
return False
|
|
70
67
|
except PermissionError:
|
|
71
|
-
return True
|
|
68
|
+
return True
|
|
72
69
|
except Exception:
|
|
73
70
|
return False
|
|
74
71
|
|
|
@@ -79,14 +76,13 @@ class TaskRecord:
|
|
|
79
76
|
description: str
|
|
80
77
|
pid: int
|
|
81
78
|
started: float
|
|
82
|
-
status: str = "in_progress"
|
|
79
|
+
status: str = "in_progress"
|
|
83
80
|
session_id: str = ""
|
|
84
81
|
channel: str = ""
|
|
85
82
|
finished: float = 0.0
|
|
86
|
-
boot_id: str = ""
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
start_count: int = 1 # 同一 task_id 被 start 的次数(重复 start 保留最后一次)
|
|
83
|
+
boot_id: str = ""
|
|
84
|
+
retryable: bool = False
|
|
85
|
+
start_count: int = 1
|
|
90
86
|
|
|
91
87
|
def to_dict(self) -> dict:
|
|
92
88
|
return asdict(self)
|
|
@@ -95,10 +91,10 @@ class TaskRecord:
|
|
|
95
91
|
@dataclass
|
|
96
92
|
class RecoveryPlan:
|
|
97
93
|
"""【P2-4】恢复计划:把中断任务分类,调用方(API 层/前端)据此决定动作。"""
|
|
98
|
-
retryable: list = field(default_factory=list)
|
|
99
|
-
notify_user: list = field(default_factory=list)
|
|
100
|
-
needs_manual: list = field(default_factory=list)
|
|
101
|
-
details: list = field(default_factory=list)
|
|
94
|
+
retryable: list = field(default_factory=list)
|
|
95
|
+
notify_user: list = field(default_factory=list)
|
|
96
|
+
needs_manual: list = field(default_factory=list)
|
|
97
|
+
details: list = field(default_factory=list)
|
|
102
98
|
|
|
103
99
|
def to_dict(self) -> dict:
|
|
104
100
|
return asdict(self)
|
|
@@ -107,20 +103,17 @@ class RecoveryPlan:
|
|
|
107
103
|
class RestartRecovery:
|
|
108
104
|
"""任务生命周期日志 + 启动恢复扫描。"""
|
|
109
105
|
|
|
110
|
-
def __init__(self, log_path: str, current_pid: int = None, boot_id: str = None,
|
|
106
|
+
def __init__(self, log_path: str, current_pid: int | None = None, boot_id: str | None = None,
|
|
111
107
|
spine=None, fsync: bool = True):
|
|
112
108
|
self.log_path = log_path
|
|
113
109
|
self.current_pid = current_pid or os.getpid()
|
|
114
|
-
# boot_id:每次启动唯一。任务记录带上它,扫描时"非本次启动的 in_progress"
|
|
115
|
-
# 即属于上一次运行 —— 比单纯靠 pid 可靠(pid 会被 OS 复用)。
|
|
116
110
|
import uuid
|
|
117
111
|
self.boot_id = boot_id or uuid.uuid4().hex[:16]
|
|
118
|
-
self._active: dict[str, TaskRecord] = {}
|
|
119
|
-
self._spine = spine
|
|
120
|
-
self._fsync = fsync
|
|
112
|
+
self._active: dict[str, TaskRecord] = {}
|
|
113
|
+
self._spine = spine
|
|
114
|
+
self._fsync = fsync
|
|
121
115
|
os.makedirs(os.path.dirname(log_path) or ".", exist_ok=True)
|
|
122
116
|
|
|
123
|
-
# ── 任务生命周期 ──────────────────────────────────────
|
|
124
117
|
def task_started(self, task_id: str, description: str,
|
|
125
118
|
session_id: str = "", channel: str = "",
|
|
126
119
|
retryable: bool = False) -> TaskRecord:
|
|
@@ -142,7 +135,6 @@ class RestartRecovery:
|
|
|
142
135
|
"status": status, "pid": self.current_pid,
|
|
143
136
|
"finished": time.time()})
|
|
144
137
|
|
|
145
|
-
# ── 步骤级 checkpoint(P1 断点可续)─────────────────────
|
|
146
138
|
def record_step(self, task_id: str, step: dict) -> None:
|
|
147
139
|
"""记一条「已完成的步骤」。管线每完成一个 stage、工具轮每完成一个工具调用各落一条,
|
|
148
140
|
崩溃/断连后 `steps_of()` 能回答「跑到哪一步」,也让续跑知道哪些步骤不必重做。
|
|
@@ -171,7 +163,7 @@ class RestartRecovery:
|
|
|
171
163
|
try:
|
|
172
164
|
ev = json.loads(line)
|
|
173
165
|
except Exception:
|
|
174
|
-
continue
|
|
166
|
+
continue
|
|
175
167
|
if ev.get("event") == "step" and ev.get("task_id") == task_id:
|
|
176
168
|
out.append({k: v for k, v in ev.items()
|
|
177
169
|
if k not in ("event", "task_id")})
|
|
@@ -179,7 +171,6 @@ class RestartRecovery:
|
|
|
179
171
|
return out
|
|
180
172
|
return out[-limit:] if limit else out
|
|
181
173
|
|
|
182
|
-
# ── 启动恢复扫描 ──────────────────────────────────────
|
|
183
174
|
def scan_interrupted(self) -> list[TaskRecord]:
|
|
184
175
|
"""启动时调用:重建日志状态,找出被中断的任务。
|
|
185
176
|
|
|
@@ -194,9 +185,7 @@ class RestartRecovery:
|
|
|
194
185
|
if rec.status != "in_progress":
|
|
195
186
|
continue
|
|
196
187
|
if rec.boot_id == self.boot_id:
|
|
197
|
-
continue
|
|
198
|
-
# 非本次启动的 in_progress = 上次运行遗留 = 被中断
|
|
199
|
-
# (pid 是否存活作为辅助信息记录,但不作为唯一判据)
|
|
188
|
+
continue
|
|
200
189
|
rec.status = "interrupted"
|
|
201
190
|
interrupted.append(rec)
|
|
202
191
|
self._append({"event": "interrupted", "task_id": task_id,
|
|
@@ -220,7 +209,7 @@ class RestartRecovery:
|
|
|
220
209
|
try:
|
|
221
210
|
ev = json.loads(line)
|
|
222
211
|
except Exception:
|
|
223
|
-
continue
|
|
212
|
+
continue
|
|
224
213
|
self._apply_event(records, ev)
|
|
225
214
|
except Exception:
|
|
226
215
|
pass
|
|
@@ -233,7 +222,6 @@ class RestartRecovery:
|
|
|
233
222
|
return
|
|
234
223
|
kind = ev.get("event")
|
|
235
224
|
if kind == "start":
|
|
236
|
-
# 【P2-4】同一 task_id 多次 start:保留最后一次,但累计 start_count
|
|
237
225
|
existing = records.get(tid)
|
|
238
226
|
start_count = (existing.start_count + 1) if existing else 1
|
|
239
227
|
records[tid] = TaskRecord(
|
|
@@ -249,7 +237,7 @@ class RestartRecovery:
|
|
|
249
237
|
if kind == "finish":
|
|
250
238
|
records[tid].finished = ev.get("finished", 0)
|
|
251
239
|
|
|
252
|
-
def plan_recovery(self, interrupted: list = None) -> RecoveryPlan:
|
|
240
|
+
def plan_recovery(self, interrupted: list | None = None) -> RecoveryPlan:
|
|
253
241
|
"""【P2-4】给中断任务分类,返回恢复计划(不改 scan_interrupted 的返回,兼容)。
|
|
254
242
|
|
|
255
243
|
分类规则:
|
|
@@ -304,9 +292,9 @@ class RestartRecovery:
|
|
|
304
292
|
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
|
|
305
293
|
f.flush()
|
|
306
294
|
if self._fsync:
|
|
307
|
-
os.fsync(f.fileno())
|
|
295
|
+
os.fsync(f.fileno())
|
|
308
296
|
except Exception:
|
|
309
|
-
pass
|
|
297
|
+
pass
|
|
310
298
|
|
|
311
299
|
def compact_log(self, keep_recent_days: float = 7):
|
|
312
300
|
"""压实日志:丢弃已完成且超过 N 天的记录,防文件无限膨胀。
|
|
@@ -317,7 +305,6 @@ class RestartRecovery:
|
|
|
317
305
|
cutoff = time.time() - keep_recent_days * 86400
|
|
318
306
|
keep = []
|
|
319
307
|
for rec in records.values():
|
|
320
|
-
# 保留:仍在进行的,或最近有活动(开始或完成)的
|
|
321
308
|
last_activity = max(rec.started, rec.finished or 0)
|
|
322
309
|
if rec.status == "in_progress" or last_activity >= cutoff:
|
|
323
310
|
keep.append({"event": "start", **rec.to_dict()})
|
|
@@ -331,7 +318,7 @@ class RestartRecovery:
|
|
|
331
318
|
with open(tmp, "w", encoding="utf-8") as f:
|
|
332
319
|
for e in keep:
|
|
333
320
|
f.write(json.dumps(e, ensure_ascii=False) + "\n")
|
|
334
|
-
for e in steps:
|
|
321
|
+
for e in steps:
|
|
335
322
|
f.write(json.dumps(e, ensure_ascii=False) + "\n")
|
|
336
323
|
os.replace(tmp, self.log_path)
|
|
337
324
|
except Exception:
|
|
@@ -9,45 +9,46 @@
|
|
|
9
9
|
"""
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
from dataclasses import dataclass
|
|
12
|
+
from typing import Optional
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
@dataclass
|
|
15
16
|
class TaskIntent:
|
|
16
17
|
"""任务意图:粗粒度类别 + 三个亲和度提示。"""
|
|
17
|
-
category: str
|
|
18
|
+
category: str
|
|
18
19
|
confidence: float
|
|
19
|
-
memory_affinity: float
|
|
20
|
-
reasoning_depth: float
|
|
20
|
+
memory_affinity: float
|
|
21
|
+
reasoning_depth: float
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
@dataclass
|
|
24
25
|
class TaskFeatures:
|
|
25
|
-
complexity: float
|
|
26
|
-
memory_dependency: float
|
|
27
|
-
tool_cost: float
|
|
28
|
-
reasoning_depth: float
|
|
29
|
-
urgency: float
|
|
30
|
-
expected_runtime: float
|
|
26
|
+
complexity: float
|
|
27
|
+
memory_dependency: float
|
|
28
|
+
tool_cost: float
|
|
29
|
+
reasoning_depth: float
|
|
30
|
+
urgency: float
|
|
31
|
+
expected_runtime: float
|
|
31
32
|
|
|
32
33
|
|
|
33
34
|
@dataclass
|
|
34
35
|
class SystemContext:
|
|
35
36
|
"""系统负载上下文。主管据此调整优先级、决定是否抢占。"""
|
|
36
|
-
active_agents: int = 0
|
|
37
|
-
queue_depth: int = 0
|
|
38
|
-
system_load: float = 0.1
|
|
37
|
+
active_agents: int = 0
|
|
38
|
+
queue_depth: int = 0
|
|
39
|
+
system_load: float = 0.1
|
|
39
40
|
|
|
40
41
|
|
|
41
42
|
@dataclass
|
|
42
43
|
class ScheduleDecision:
|
|
43
|
-
priority: float
|
|
44
|
-
route_hint: str
|
|
45
|
-
execution_mode: str
|
|
46
|
-
preemptible: bool
|
|
47
|
-
expected_budget: float
|
|
48
|
-
intent: TaskIntent = None
|
|
49
|
-
features: TaskFeatures = None
|
|
50
|
-
preemption_reason: str = None
|
|
44
|
+
priority: float
|
|
45
|
+
route_hint: str
|
|
46
|
+
execution_mode: str
|
|
47
|
+
preemptible: bool
|
|
48
|
+
expected_budget: float
|
|
49
|
+
intent: Optional[TaskIntent] = None
|
|
50
|
+
features: Optional[TaskFeatures] = None
|
|
51
|
+
preemption_reason: Optional[str] = None
|
|
51
52
|
reason: str = ""
|
|
52
53
|
|
|
53
54
|
def to_dict(self) -> dict:
|
|
@@ -63,7 +64,6 @@ class ScheduleDecision:
|
|
|
63
64
|
class CognitiveScheduler:
|
|
64
65
|
"""意图感知的认知调度器。"""
|
|
65
66
|
|
|
66
|
-
# 意图识别关键词(粗分类)
|
|
67
67
|
_REASONING = ["规划", "计划", "分析", "设计", "推理", "为什么", "怎么办", "策略", "方案"]
|
|
68
68
|
_MEMORY = ["记得", "记忆", "之前", "上次", "回忆", "查一下我", "我说过"]
|
|
69
69
|
_WORKFLOW = ["然后", "接着", "之后", "依次", "批量", "流程", "再然后", "下一步"]
|
|
@@ -86,7 +86,6 @@ class CognitiveScheduler:
|
|
|
86
86
|
complexity, tool_cost, urgency, runtime = 0.7, 0.6, 0.65, 0.8
|
|
87
87
|
elif intent.category == "memory":
|
|
88
88
|
complexity, tool_cost, urgency, runtime = 0.5, 0.2, 0.55, 0.6
|
|
89
|
-
# 紧急词加成
|
|
90
89
|
if any(w in task for w in ["紧急", "立刻", "马上", "尽快", "急"]):
|
|
91
90
|
urgency = min(1.0, urgency + 0.3)
|
|
92
91
|
return TaskFeatures(complexity, intent.memory_affinity, tool_cost,
|
|
@@ -117,16 +116,14 @@ class CognitiveScheduler:
|
|
|
117
116
|
return "tool"
|
|
118
117
|
return "agent" if priority > 0.35 else "tool"
|
|
119
118
|
|
|
120
|
-
def preemption_reason(self, priority: float, ctx: SystemContext) -> str:
|
|
121
|
-
# 阈值匹配实际优先级范围(本调度器优先级多在 0.4 以内,
|
|
122
|
-
# 原 0.55/0.7 几乎不可达,会让抢占形同虚设)
|
|
119
|
+
def preemption_reason(self, priority: float, ctx: SystemContext) -> str | None:
|
|
123
120
|
if priority > 0.35 and ctx.active_agents >= 3:
|
|
124
121
|
return "高优先级 + Agent 压力大"
|
|
125
122
|
if priority > 0.4 and ctx.queue_depth >= 2:
|
|
126
123
|
return "高优先级 + 队列积压"
|
|
127
124
|
return None
|
|
128
125
|
|
|
129
|
-
def schedule(self, task: str, ctx: SystemContext = None) -> ScheduleDecision:
|
|
126
|
+
def schedule(self, task: str, ctx: SystemContext | None = None) -> ScheduleDecision:
|
|
130
127
|
"""主入口:给一个任务做完整调度决策。"""
|
|
131
128
|
ctx = ctx or SystemContext()
|
|
132
129
|
intent = self.parse_intent(task)
|
|
@@ -22,8 +22,6 @@ import base64
|
|
|
22
22
|
|
|
23
23
|
SERVICE = "foxclaw.llm"
|
|
24
24
|
|
|
25
|
-
# 后端探测结果缓存(含失败):每个请求都 import + 探测一次没必要。
|
|
26
|
-
# 失败也缓存是有意的 —— 没有可用后端时不该反复重试。
|
|
27
25
|
_kr_cache = None
|
|
28
26
|
_kr_probed = False
|
|
29
27
|
|
|
@@ -37,7 +35,6 @@ def _keyring():
|
|
|
37
35
|
try:
|
|
38
36
|
import keyring
|
|
39
37
|
from keyring.backends.fail import Keyring as _FailBackend
|
|
40
|
-
# 默认后端是 Fail 时说明系统上没有可用凭据库(典型:无桌面会话的 Linux)
|
|
41
38
|
if isinstance(keyring.get_keyring(), _FailBackend):
|
|
42
39
|
_kr_cache = None
|
|
43
40
|
else:
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
import json
|
|
17
17
|
import re
|
|
18
|
+
import builtins
|
|
18
19
|
import threading
|
|
19
20
|
import time
|
|
20
21
|
import uuid
|
|
@@ -42,9 +43,7 @@ class SessionStore:
|
|
|
42
43
|
self.base.mkdir(parents=True, exist_ok=True)
|
|
43
44
|
self._lock = threading.RLock()
|
|
44
45
|
|
|
45
|
-
# ── 内部 ──────────────────────────────────────────────
|
|
46
46
|
def _path(self, sid: str) -> Path:
|
|
47
|
-
# 只允许我们自己生成的 id 形态,杜绝 ../ 之类路径穿越
|
|
48
47
|
if not re.fullmatch(r"[A-Za-z0-9_-]{1,64}", str(sid or "")):
|
|
49
48
|
raise ValueError("非法会话 id")
|
|
50
49
|
return self.base / f"{sid}.json"
|
|
@@ -64,9 +63,8 @@ class SessionStore:
|
|
|
64
63
|
tmp = p.with_suffix(".tmp")
|
|
65
64
|
with open(tmp, "w", encoding="utf-8") as f:
|
|
66
65
|
json.dump(data, f, ensure_ascii=False, indent=1)
|
|
67
|
-
tmp.replace(p)
|
|
66
|
+
tmp.replace(p)
|
|
68
67
|
|
|
69
|
-
# ── 公开 API ──────────────────────────────────────────
|
|
70
68
|
def create(self, title: str = "", agent_id: str = "supervisor") -> dict:
|
|
71
69
|
sid = uuid.uuid4().hex[:12]
|
|
72
70
|
data = {"id": sid, "title": (title or "新会话")[:60], "agent_id": agent_id,
|
|
@@ -105,7 +103,8 @@ class SessionStore:
|
|
|
105
103
|
with self._lock:
|
|
106
104
|
return self._read(sid)
|
|
107
105
|
|
|
108
|
-
def append(self, sid: str, role: str, content: str,
|
|
106
|
+
def append(self, sid: str, role: str, content: str,
|
|
107
|
+
attachments: builtins.list | None = None) -> bool:
|
|
109
108
|
"""追加一条消息;会话不存在则忽略。首条用户消息会顺带定标题。
|
|
110
109
|
attachments(可选)= 聊天附件元数据 [{name,size,mime,url}];只落盘轻字段(不存 text,
|
|
111
110
|
避免会话文件随附件正文膨胀),供「会话历史」回看时渲染附件卡片。"""
|
|
@@ -128,7 +127,6 @@ class SessionStore:
|
|
|
128
127
|
if atts:
|
|
129
128
|
msg["attachments"] = atts
|
|
130
129
|
msgs.append(msg)
|
|
131
|
-
# 标题仍是默认值时,用首条用户消息提炼
|
|
132
130
|
if role == "user" and (not d.get("title") or d["title"] == "新会话"):
|
|
133
131
|
d["title"] = derive_title(content)
|
|
134
132
|
d["updated_at"] = _now()
|
|
@@ -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 **之前**创建,
|