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
|
@@ -22,8 +22,8 @@ from dataclasses import dataclass
|
|
|
22
22
|
@dataclass
|
|
23
23
|
class LoopVerdict:
|
|
24
24
|
stuck: bool = False
|
|
25
|
-
level: str = "ok"
|
|
26
|
-
detector: str = ""
|
|
25
|
+
level: str = "ok"
|
|
26
|
+
detector: str = ""
|
|
27
27
|
count: int = 0
|
|
28
28
|
message: str = ""
|
|
29
29
|
|
|
@@ -35,9 +35,9 @@ class LoopVerdict:
|
|
|
35
35
|
|
|
36
36
|
@dataclass
|
|
37
37
|
class _ToolCall:
|
|
38
|
-
signature: str
|
|
38
|
+
signature: str
|
|
39
39
|
tool_name: str
|
|
40
|
-
result_hash: str = ""
|
|
40
|
+
result_hash: str = ""
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
class ToolLoopDetector:
|
|
@@ -45,14 +45,11 @@ class ToolLoopDetector:
|
|
|
45
45
|
|
|
46
46
|
def __init__(self, history_size: int = 30, warning: int = 6,
|
|
47
47
|
critical: int = 12, circuit_break: int = 20):
|
|
48
|
-
# 阈值单调递增保证(warning < critical < circuit_break)
|
|
49
48
|
self.warning = max(2, warning)
|
|
50
49
|
self.critical = max(self.warning + 1, critical)
|
|
51
50
|
self.circuit_break = max(self.critical + 1, circuit_break)
|
|
52
51
|
self.history: deque = deque(maxlen=max(history_size, self.circuit_break))
|
|
53
|
-
self._last_verdict = None
|
|
54
|
-
# 【深审·线程安全】SkillAdapter 持单个共享实例,invoke(含管线内 dispatch)可被并发任务
|
|
55
|
-
# 同时调 record → history.append + _detect(list(history)) 非原子,需锁(线程安全铁律)。
|
|
52
|
+
self._last_verdict: LoopVerdict | None = None
|
|
56
53
|
self._lock = threading.RLock()
|
|
57
54
|
|
|
58
55
|
@staticmethod
|
|
@@ -72,18 +69,19 @@ class ToolLoopDetector:
|
|
|
72
69
|
s = str(result)[:500]
|
|
73
70
|
return hashlib.sha256(s.encode()).hexdigest()[:16]
|
|
74
71
|
|
|
75
|
-
def record(self, tool_name: str, args: dict = None, result=None) -> LoopVerdict:
|
|
72
|
+
def record(self, tool_name: str, args: dict | None = None, result=None) -> LoopVerdict:
|
|
76
73
|
"""记录一次工具调用并即时检测。返回 LoopVerdict。"""
|
|
77
74
|
sig = self._signature(tool_name, args or {})
|
|
78
75
|
rhash = self._result_sig(result) if result is not None else ""
|
|
79
|
-
with self._lock:
|
|
76
|
+
with self._lock:
|
|
80
77
|
self.history.append(_ToolCall(sig, tool_name, rhash))
|
|
81
|
-
|
|
82
|
-
|
|
78
|
+
verdict = self._detect()
|
|
79
|
+
self._last_verdict = verdict
|
|
80
|
+
return verdict
|
|
83
81
|
|
|
84
82
|
def _level_for(self, count: int) -> str:
|
|
85
83
|
if count >= self.circuit_break:
|
|
86
|
-
return "critical"
|
|
84
|
+
return "critical"
|
|
87
85
|
if count >= self.critical:
|
|
88
86
|
return "critical"
|
|
89
87
|
if count >= self.warning:
|
|
@@ -95,7 +93,6 @@ class ToolLoopDetector:
|
|
|
95
93
|
if len(h) < self.warning:
|
|
96
94
|
return LoopVerdict()
|
|
97
95
|
|
|
98
|
-
# 1. repeat / no_progress:末尾连续相同签名
|
|
99
96
|
last_sig = h[-1].signature
|
|
100
97
|
streak = 0
|
|
101
98
|
for c in reversed(h):
|
|
@@ -105,8 +102,6 @@ class ToolLoopDetector:
|
|
|
105
102
|
break
|
|
106
103
|
if streak >= self.warning:
|
|
107
104
|
lvl = self._level_for(streak)
|
|
108
|
-
# no_progress 优先:同签名重复 且 (有结果的那些)结果都相同
|
|
109
|
-
# (只比对非空 result_hash,避免混合有/无 result 时漏判轮询卡死)
|
|
110
105
|
streak_calls = h[-streak:]
|
|
111
106
|
with_result = [c.result_hash for c in streak_calls if c.result_hash]
|
|
112
107
|
if len(with_result) >= 2 and len(set(with_result)) == 1:
|
|
@@ -114,13 +109,11 @@ class ToolLoopDetector:
|
|
|
114
109
|
stuck=True, level=lvl, detector="no_progress", count=streak,
|
|
115
110
|
message=(f"工具「{h[-1].tool_name}」轮询 {streak} 次结果无变化"
|
|
116
111
|
+ ("(已达熔断阈值,建议中止)" if streak >= self.circuit_break else "")))
|
|
117
|
-
# 否则按 repeat
|
|
118
112
|
return LoopVerdict(
|
|
119
113
|
stuck=True, level=lvl, detector="repeat", count=streak,
|
|
120
114
|
message=(f"工具「{h[-1].tool_name}」连续重复调用 {streak} 次"
|
|
121
115
|
+ ("(已达熔断阈值,建议中止)" if streak >= self.circuit_break else "")))
|
|
122
116
|
|
|
123
|
-
# 3. ping_pong:末尾两签名交替(A→B→A→B)
|
|
124
117
|
if len(h) >= 4:
|
|
125
118
|
a, b = h[-1].signature, h[-2].signature
|
|
126
119
|
if a != b:
|
|
@@ -24,7 +24,7 @@ AMAP_BASE = "https://restapi.amap.com/v3"
|
|
|
24
24
|
class AMapClient:
|
|
25
25
|
"""高德 Web Service 客户端。无 Key 则 enabled=False,调用返回 None。"""
|
|
26
26
|
|
|
27
|
-
def __init__(self, api_key: str = None):
|
|
27
|
+
def __init__(self, api_key: str | None = None):
|
|
28
28
|
self.api_key = api_key or os.environ.get("AMAP_MAPS_API_KEY")
|
|
29
29
|
self.enabled = bool(self.api_key)
|
|
30
30
|
|
|
@@ -40,12 +40,11 @@ class AMapClient:
|
|
|
40
40
|
except Exception:
|
|
41
41
|
return None
|
|
42
42
|
|
|
43
|
-
# ── 地理编码 ──────────────────────────────────────────
|
|
44
43
|
def geocode(self, address: str, city: str = "") -> Optional[tuple]:
|
|
45
44
|
"""地址 → (lng, lat)。"""
|
|
46
45
|
data = self._get("geocode/geo", {"address": address, "city": city})
|
|
47
46
|
if data and data.get("geocodes"):
|
|
48
|
-
loc = data["geocodes"][0]["location"]
|
|
47
|
+
loc = data["geocodes"][0]["location"]
|
|
49
48
|
lng, lat = loc.split(",")
|
|
50
49
|
return float(lng), float(lat)
|
|
51
50
|
return None
|
|
@@ -57,7 +56,6 @@ class AMapClient:
|
|
|
57
56
|
return data["regeocode"].get("formatted_address")
|
|
58
57
|
return None
|
|
59
58
|
|
|
60
|
-
# ── 路线规划 ──────────────────────────────────────────
|
|
61
59
|
def route(self, origin: str, destination: str, mode: str = "driving",
|
|
62
60
|
city: str = "") -> Optional[dict]:
|
|
63
61
|
"""真实路线。origin/destination 为 'lng,lat'。
|
|
@@ -90,7 +88,6 @@ class AMapClient:
|
|
|
90
88
|
except Exception:
|
|
91
89
|
return None
|
|
92
90
|
|
|
93
|
-
# ── POI / 周边 / 天气 ─────────────────────────────────
|
|
94
91
|
def search_poi(self, keywords: str, city: str = "", offset: int = 10) -> Optional[list]:
|
|
95
92
|
data = self._get("place/text",
|
|
96
93
|
{"keywords": keywords, "city": city, "offset": offset})
|
|
@@ -19,12 +19,12 @@ TravelMode = Literal["walking", "driving", "transit", "cycling"]
|
|
|
19
19
|
@dataclass
|
|
20
20
|
class Waypoint:
|
|
21
21
|
"""一个行程点 = 时空中的一个约束"""
|
|
22
|
-
name: str
|
|
23
|
-
address: str = ""
|
|
24
|
-
lng: Optional[float] = None
|
|
25
|
-
lat: Optional[float] = None
|
|
26
|
-
start_time: Optional[str] = None
|
|
27
|
-
duration_min: int = 0
|
|
22
|
+
name: str
|
|
23
|
+
address: str = ""
|
|
24
|
+
lng: Optional[float] = None
|
|
25
|
+
lat: Optional[float] = None
|
|
26
|
+
start_time: Optional[str] = None
|
|
27
|
+
duration_min: int = 0
|
|
28
28
|
city: str = ""
|
|
29
29
|
note: str = ""
|
|
30
30
|
id: str = field(default_factory=lambda: f"wp_{uuid.uuid4().hex[:10]}")
|
|
@@ -58,7 +58,7 @@ class RouteLeg:
|
|
|
58
58
|
mode: TravelMode = "driving"
|
|
59
59
|
distance_km: float = 0.0
|
|
60
60
|
duration_min: float = 0.0
|
|
61
|
-
source: Literal["estimated", "amap"] = "estimated"
|
|
61
|
+
source: Literal["estimated", "amap"] = "estimated"
|
|
62
62
|
note: str = ""
|
|
63
63
|
|
|
64
64
|
def to_dict(self) -> dict:
|
|
@@ -14,8 +14,6 @@ from __future__ import annotations
|
|
|
14
14
|
from ..cognitive.reasoner import DomainReasoner, ReasonedVerdict, Evidence
|
|
15
15
|
from .reasoning import ItineraryReasoning
|
|
16
16
|
|
|
17
|
-
# "紧凑"阈值:某段空档 / 所需通勤 < 该比例的倍数即算逼近(未赶不及但很赶),
|
|
18
|
-
# 交给可选 LLM 复核是否提醒用户。仅在启用 LLM 时生效。
|
|
19
17
|
TIGHT_RATIO = 1.5
|
|
20
18
|
|
|
21
19
|
|
|
@@ -43,12 +41,11 @@ class ItineraryConflictReasoner(DomainReasoner):
|
|
|
43
41
|
def gather_evidence(self, subject, obs: dict) -> list:
|
|
44
42
|
waypoints, mode = subject
|
|
45
43
|
mode = obs["mode"]
|
|
46
|
-
conflicts = ItineraryReasoning.detect_conflicts(waypoints, mode)
|
|
44
|
+
conflicts = ItineraryReasoning.detect_conflicts(waypoints, mode)
|
|
47
45
|
ev = [Evidence(factor=c["type"], detail=c["reason"], weight=1.0, raw=c)
|
|
48
46
|
for c in conflicts]
|
|
49
47
|
if not conflicts:
|
|
50
48
|
ev.append(Evidence("no_conflict", "未检测到时间冲突或通勤不可达", 0.5))
|
|
51
|
-
# informational(raw=None):顺路簇/密度,丰满证据链但不参与裁决
|
|
52
49
|
try:
|
|
53
50
|
clusters = ItineraryReasoning.cluster_by_proximity(waypoints)
|
|
54
51
|
if clusters:
|
|
@@ -65,11 +62,9 @@ class ItineraryConflictReasoner(DomainReasoner):
|
|
|
65
62
|
def arbitrate(self, subject, obs, hyps, ev) -> ReasonedVerdict:
|
|
66
63
|
triggered = [e for e in ev if e.raw is not None]
|
|
67
64
|
if triggered:
|
|
68
|
-
# 【规则权威】检出硬冲突 → confirm,绝不被 LLM 推翻/隐藏
|
|
69
65
|
return ReasonedVerdict(
|
|
70
66
|
"confirm", ";".join(e.detail for e in triggered),
|
|
71
67
|
ev, hyps, "rules")
|
|
72
|
-
# 无硬冲突:可选让 LLM 就"紧凑排程"复核是否升级(只 approve→confirm)
|
|
73
68
|
if DomainReasoner.llm_available(self.llm, self.llm_enabled) and self._is_tight(subject, obs):
|
|
74
69
|
if DomainReasoner.llm_should_escalate(self.llm, *self._llm_prompt(subject, obs, ev)):
|
|
75
70
|
return ReasonedVerdict("confirm", "排程紧凑,LLM 建议人工确认", ev, hyps, "llm")
|
|
@@ -7,28 +7,29 @@ Foxclaw 行程子系统 — 推理引擎(行程 Agent 的大脑,纯算法)
|
|
|
7
7
|
import math
|
|
8
8
|
from datetime import timedelta
|
|
9
9
|
from itertools import permutations
|
|
10
|
+
from typing import Any, cast
|
|
10
11
|
|
|
11
|
-
from .models import
|
|
12
|
+
from .models import RouteLeg, TravelMode, Waypoint
|
|
12
13
|
|
|
13
14
|
|
|
14
|
-
# 各出行方式的估算平均速度(km/h),接高德后用真实数据替换
|
|
15
15
|
MODE_SPEED = {"walking": 5, "cycling": 15, "transit": 25, "driving": 35}
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
class ItineraryReasoning:
|
|
19
19
|
"""行程推理:把地点+时间变成可决策的行程智能。"""
|
|
20
20
|
|
|
21
|
-
# ── 距离与可达性(直线估算)──────────────────────────
|
|
22
21
|
@staticmethod
|
|
23
22
|
def haversine_km(a: Waypoint, b: Waypoint) -> float:
|
|
24
23
|
"""两点直线距离(km)。Haversine 公式。"""
|
|
25
|
-
|
|
24
|
+
lat_a, lng_a = a.lat, a.lng
|
|
25
|
+
lat_b, lng_b = b.lat, b.lng
|
|
26
|
+
if lat_a is None or lng_a is None or lat_b is None or lng_b is None:
|
|
26
27
|
return 0.0
|
|
27
28
|
R = 6371.0
|
|
28
|
-
dlat = math.radians(
|
|
29
|
-
dlng = math.radians(
|
|
29
|
+
dlat = math.radians(lat_b - lat_a)
|
|
30
|
+
dlng = math.radians(lng_b - lng_a)
|
|
30
31
|
x = (math.sin(dlat/2)**2 +
|
|
31
|
-
math.cos(math.radians(
|
|
32
|
+
math.cos(math.radians(lat_a)) * math.cos(math.radians(lat_b)) *
|
|
32
33
|
math.sin(dlng/2)**2)
|
|
33
34
|
return R * 2 * math.atan2(math.sqrt(x), math.sqrt(1-x))
|
|
34
35
|
|
|
@@ -36,16 +37,16 @@ class ItineraryReasoning:
|
|
|
36
37
|
def estimate_leg(a: Waypoint, b: Waypoint, mode: str = "driving") -> RouteLeg:
|
|
37
38
|
"""估算两点路线段(直线距离 × 1.3 绕路系数 / 速度)。"""
|
|
38
39
|
straight = ItineraryReasoning.haversine_km(a, b)
|
|
39
|
-
road_km = straight * 1.3
|
|
40
|
+
road_km = straight * 1.3
|
|
40
41
|
speed = MODE_SPEED.get(mode, 35)
|
|
41
42
|
duration = (road_km / speed) * 60 if speed else 0
|
|
42
43
|
return RouteLeg(
|
|
43
44
|
from_id=a.id, to_id=b.id, from_name=a.name, to_name=b.name,
|
|
44
|
-
mode=
|
|
45
|
+
mode=cast(TravelMode, mode if mode in MODE_SPEED else "driving"),
|
|
46
|
+
distance_km=round(road_km, 2),
|
|
45
47
|
duration_min=round(duration, 1), source="estimated",
|
|
46
48
|
)
|
|
47
49
|
|
|
48
|
-
# ── 路线优化(TSP)────────────────────────────────────
|
|
49
50
|
@staticmethod
|
|
50
51
|
def optimize_order(waypoints: list, mode: str = "driving",
|
|
51
52
|
fixed_start: bool = True) -> dict:
|
|
@@ -61,10 +62,10 @@ class ItineraryReasoning:
|
|
|
61
62
|
for i in range(len(seq)-1))
|
|
62
63
|
|
|
63
64
|
if len(pts) <= 8:
|
|
64
|
-
# 精确解
|
|
65
65
|
start = pts[0] if fixed_start else None
|
|
66
66
|
rest = pts[1:] if fixed_start else pts
|
|
67
|
-
best
|
|
67
|
+
best: list = []
|
|
68
|
+
best_d = float("inf")
|
|
68
69
|
for perm in permutations(rest):
|
|
69
70
|
seq = ([start] + list(perm)) if start else list(perm)
|
|
70
71
|
d = total_dist(seq)
|
|
@@ -73,7 +74,6 @@ class ItineraryReasoning:
|
|
|
73
74
|
method = "exact"
|
|
74
75
|
ordered = best
|
|
75
76
|
else:
|
|
76
|
-
# 最近邻启发式
|
|
77
77
|
remaining = pts[1:]
|
|
78
78
|
ordered = [pts[0]]
|
|
79
79
|
while remaining:
|
|
@@ -83,7 +83,6 @@ class ItineraryReasoning:
|
|
|
83
83
|
remaining.remove(nxt)
|
|
84
84
|
method = "nearest_neighbor"
|
|
85
85
|
|
|
86
|
-
# 计算总距离/时间
|
|
87
86
|
legs = [ItineraryReasoning.estimate_leg(ordered[i], ordered[i+1], mode)
|
|
88
87
|
for i in range(len(ordered)-1)]
|
|
89
88
|
return {
|
|
@@ -95,17 +94,15 @@ class ItineraryReasoning:
|
|
|
95
94
|
"method": method,
|
|
96
95
|
}
|
|
97
96
|
|
|
98
|
-
# ── 时间冲突 + 可达性 ─────────────────────────────────
|
|
99
97
|
@staticmethod
|
|
100
98
|
def detect_conflicts(waypoints: list, mode: str = "driving") -> list:
|
|
101
99
|
"""按时间排序的行程,检测:① 时间重叠 ② 通勤赶不及。"""
|
|
102
100
|
timed = [w for w in waypoints if w.dt is not None]
|
|
103
101
|
timed.sort(key=lambda w: w.dt)
|
|
104
|
-
issues = []
|
|
102
|
+
issues: list[dict[str, Any]] = []
|
|
105
103
|
for i in range(len(timed) - 1):
|
|
106
104
|
cur, nxt = timed[i], timed[i+1]
|
|
107
105
|
cur_end = cur.dt + timedelta(minutes=cur.duration_min)
|
|
108
|
-
# 时间重叠
|
|
109
106
|
if cur_end > nxt.dt:
|
|
110
107
|
issues.append({
|
|
111
108
|
"type": "time_conflict",
|
|
@@ -115,7 +112,6 @@ class ItineraryReasoning:
|
|
|
115
112
|
f"晚于「{nxt.name}」开始({nxt.dt.strftime('%H:%M')})",
|
|
116
113
|
})
|
|
117
114
|
continue
|
|
118
|
-
# 可达性:中间通勤时间够不够
|
|
119
115
|
if cur.has_coord and nxt.has_coord:
|
|
120
116
|
leg = ItineraryReasoning.estimate_leg(cur, nxt, mode)
|
|
121
117
|
gap_min = (nxt.dt - cur_end).total_seconds() / 60
|
|
@@ -131,7 +127,6 @@ class ItineraryReasoning:
|
|
|
131
127
|
})
|
|
132
128
|
return issues
|
|
133
129
|
|
|
134
|
-
# ── 地理聚类(顺路归组)───────────────────────────────
|
|
135
130
|
@staticmethod
|
|
136
131
|
def cluster_by_proximity(waypoints: list, radius_km: float = 3.0) -> list:
|
|
137
132
|
"""把相近的行程点聚成簇,建议"顺路一起办"。"""
|
|
@@ -149,9 +144,8 @@ class ItineraryReasoning:
|
|
|
149
144
|
cluster.append(other)
|
|
150
145
|
used.add(other.id)
|
|
151
146
|
clusters.append([c.name for c in cluster])
|
|
152
|
-
return [c for c in clusters if len(c) > 1]
|
|
147
|
+
return [c for c in clusters if len(c) > 1]
|
|
153
148
|
|
|
154
|
-
# ── 行程密度 ──────────────────────────────────────────
|
|
155
149
|
@staticmethod
|
|
156
150
|
def density(waypoints: list, mode: str = "driving") -> dict:
|
|
157
151
|
"""行程密度:总点数、总通勤、跨城。"""
|
|
@@ -6,16 +6,17 @@ Foxclaw 行程子系统 — Itinerary Runtime
|
|
|
6
6
|
|
|
7
7
|
import csv
|
|
8
8
|
from pathlib import Path
|
|
9
|
+
from typing import cast
|
|
9
10
|
|
|
10
|
-
from .models import
|
|
11
|
-
from .reasoning import ItineraryReasoning
|
|
11
|
+
from .models import ItineraryVerdict, TravelMode, Waypoint
|
|
12
|
+
from .reasoning import MODE_SPEED, ItineraryReasoning
|
|
12
13
|
from .amap_client import AMapClient
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
class ItineraryRuntime:
|
|
16
17
|
"""行政 Agent 的行程内核。"""
|
|
17
18
|
|
|
18
|
-
def __init__(self, amap_key: str = None, hmr=None):
|
|
19
|
+
def __init__(self, amap_key: str | None = None, hmr=None):
|
|
19
20
|
self.waypoints: list = []
|
|
20
21
|
self.reasoning = ItineraryReasoning()
|
|
21
22
|
self.amap = AMapClient(amap_key)
|
|
@@ -25,13 +26,11 @@ class ItineraryRuntime:
|
|
|
25
26
|
def amap_enabled(self) -> bool:
|
|
26
27
|
return self.amap.enabled
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
lat: float = None, start_time: str = None,
|
|
29
|
+
def add_waypoint(self, name: str, address: str = "", lng: float | None = None,
|
|
30
|
+
lat: float | None = None, start_time: str | None = None,
|
|
31
31
|
duration_min: int = 0, city: str = "") -> dict:
|
|
32
32
|
wp = Waypoint(name=name, address=address, lng=lng, lat=lat,
|
|
33
33
|
start_time=start_time, duration_min=duration_min, city=city)
|
|
34
|
-
# 没坐标但有地址 → 尝试高德地理编码
|
|
35
34
|
if not wp.has_coord and address and self.amap_enabled:
|
|
36
35
|
geo = self.amap.geocode(address, city)
|
|
37
36
|
if geo:
|
|
@@ -62,7 +61,6 @@ class ItineraryRuntime:
|
|
|
62
61
|
count += 1
|
|
63
62
|
return count
|
|
64
63
|
|
|
65
|
-
# ── 真实路线(高德优先,否则估算)─────────────────────
|
|
66
64
|
def real_leg(self, a: Waypoint, b: Waypoint, mode: str = "driving"):
|
|
67
65
|
"""两点路线:高德可用且有坐标时用真实数据,否则直线估算。"""
|
|
68
66
|
if self.amap_enabled and a.has_coord and b.has_coord:
|
|
@@ -70,12 +68,12 @@ class ItineraryRuntime:
|
|
|
70
68
|
if r:
|
|
71
69
|
from .models import RouteLeg
|
|
72
70
|
return RouteLeg(from_id=a.id, to_id=b.id, from_name=a.name,
|
|
73
|
-
to_name=b.name,
|
|
71
|
+
to_name=b.name,
|
|
72
|
+
mode=cast(TravelMode, mode if mode in MODE_SPEED else "driving"),
|
|
74
73
|
distance_km=r["distance_km"],
|
|
75
74
|
duration_min=r["duration_min"], source="amap")
|
|
76
75
|
return self.reasoning.estimate_leg(a, b, mode)
|
|
77
76
|
|
|
78
|
-
# ── 综合分析(行政 Agent 核心输出)────────────────────
|
|
79
77
|
def plan(self, mode: str = "driving", optimize: bool = True) -> dict:
|
|
80
78
|
"""完整行程规划:优化顺序 + 冲突检测 + 聚类 + 守门人裁决。"""
|
|
81
79
|
opt = (self.reasoning.optimize_order(self.waypoints, mode)
|
|
@@ -95,7 +93,6 @@ class ItineraryRuntime:
|
|
|
95
93
|
"map_data": self._to_amap_mapdata(opt),
|
|
96
94
|
}
|
|
97
95
|
|
|
98
|
-
# ── 行程守门人 ────────────────────────────────────────
|
|
99
96
|
def _gatekeeper(self, conflicts, clusters, dens) -> list:
|
|
100
97
|
verdicts = []
|
|
101
98
|
for c in conflicts:
|
|
@@ -124,7 +121,6 @@ class ItineraryRuntime:
|
|
|
124
121
|
payload={"cluster": cl}))
|
|
125
122
|
return verdicts
|
|
126
123
|
|
|
127
|
-
# ── 导出高德可视化格式(MapTaskData)──────────────────
|
|
128
124
|
def _to_amap_mapdata(self, opt) -> list:
|
|
129
125
|
"""转成高德 MapTaskData(POI + route),可喂给高德生成可视化地图。"""
|
|
130
126
|
data = []
|
|
@@ -25,8 +25,8 @@ class CapabilityRegistry:
|
|
|
25
25
|
def __init__(self):
|
|
26
26
|
self._caps: dict[str, Any] = {}
|
|
27
27
|
self._lazy: dict[str, Callable[[], Any]] = {}
|
|
28
|
-
self._lock = threading.RLock()
|
|
29
|
-
self._build_locks: dict[str, threading.Lock] = {}
|
|
28
|
+
self._lock = threading.RLock()
|
|
29
|
+
self._build_locks: dict[str, threading.Lock] = {}
|
|
30
30
|
|
|
31
31
|
def register(self, name: str, handle: Any) -> None:
|
|
32
32
|
"""注册一个已构造好的能力实例(覆盖同名)。"""
|
|
@@ -38,7 +38,7 @@ class CapabilityRegistry:
|
|
|
38
38
|
with self._lock:
|
|
39
39
|
self._lazy[name] = factory
|
|
40
40
|
|
|
41
|
-
def get(self, name: str, default: Any = None) -> Any:
|
|
41
|
+
def get(self, name: str, default: Any | None = None) -> Any:
|
|
42
42
|
"""解析能力。已注册直接返回;惰性则首次构造并缓存;都没有返回 default。
|
|
43
43
|
|
|
44
44
|
【F1 修复】慢惰性构造(如 search_runtime 首建要装 HKC+载模型)**不持全局锁**,
|
|
@@ -54,12 +54,11 @@ class CapabilityRegistry:
|
|
|
54
54
|
blk = self._build_locks.get(name)
|
|
55
55
|
if blk is None:
|
|
56
56
|
blk = self._build_locks[name] = threading.Lock()
|
|
57
|
-
# 出全局锁,用 per-name 锁串行化"同一能力"的构造
|
|
58
57
|
with blk:
|
|
59
58
|
with self._lock:
|
|
60
|
-
if name in self._caps:
|
|
59
|
+
if name in self._caps:
|
|
61
60
|
return self._caps[name]
|
|
62
|
-
inst = factory()
|
|
61
|
+
inst = factory()
|
|
63
62
|
with self._lock:
|
|
64
63
|
self._caps[name] = inst
|
|
65
64
|
return inst
|
|
@@ -22,12 +22,10 @@ from ..core.governance_audit import new_decision_id
|
|
|
22
22
|
@dataclass
|
|
23
23
|
class Verdict:
|
|
24
24
|
allowed: bool
|
|
25
|
-
level: str
|
|
25
|
+
level: str
|
|
26
26
|
reason: str = ""
|
|
27
|
-
source: str = ""
|
|
28
|
-
raw: Any = None
|
|
29
|
-
# 【P3-4】每次裁决的唯一 ID(P1-2 治理审计的关联键)。放最后+默认值:既有位置参数
|
|
30
|
-
# 构造(如 Verdict(True,"allow",...,"permission",None))不受影响。
|
|
27
|
+
source: str = ""
|
|
28
|
+
raw: Any = None
|
|
31
29
|
decision_id: str = field(default="")
|
|
32
30
|
|
|
33
31
|
|
|
@@ -35,9 +33,7 @@ class PolicyGate:
|
|
|
35
33
|
def __init__(self, spine=None, action_evaluator: Optional[Callable] = None,
|
|
36
34
|
auditor=None):
|
|
37
35
|
self._spine = spine
|
|
38
|
-
self._eval = action_evaluator
|
|
39
|
-
# 【P1-2】统一治理审计(可空:standalone 无审计)。鸭子类型注入,避免 kernel→core 之外
|
|
40
|
-
# 的装配环;core.governance_audit.GovernanceAuditor 是唯一预期实现。
|
|
36
|
+
self._eval = action_evaluator
|
|
41
37
|
self._auditor = auditor
|
|
42
38
|
|
|
43
39
|
@property
|
|
@@ -45,7 +41,6 @@ class PolicyGate:
|
|
|
45
41
|
"""SkillAdapter 经此取审计器,把执行终态关联到裁决 decision_id(P3-4 跨层关联)。"""
|
|
46
42
|
return self._auditor
|
|
47
43
|
|
|
48
|
-
# ── 推理准入(Harness)──────────────────────────────────────
|
|
49
44
|
def check_reasoning(self, harness, node) -> Verdict:
|
|
50
45
|
"""经统一门做推理漂移准入。底层仍是那一次 harness.check(同一 HarnessResult),
|
|
51
46
|
Harness 自身已 emit harness_block/warn 事件,门面**不重复 emit**。行为一致。
|
|
@@ -64,7 +59,6 @@ class PolicyGate:
|
|
|
64
59
|
"dominant_dim": getattr(r, "dominant_dim", "")})
|
|
65
60
|
return v
|
|
66
61
|
|
|
67
|
-
# ── 动作准入(skill-guard)──────────────────────────────────
|
|
68
62
|
def check_action(self, skill, args: dict, rules=None,
|
|
69
63
|
confirmed: bool = False, mode: str = "strict",
|
|
70
64
|
taint_level: int = 0, taint_threshold: int = 0) -> Verdict:
|
|
@@ -16,16 +16,16 @@ import threading
|
|
|
16
16
|
import time
|
|
17
17
|
import uuid
|
|
18
18
|
from dataclasses import dataclass, field
|
|
19
|
+
from typing import Optional
|
|
19
20
|
|
|
20
21
|
|
|
21
|
-
# 合法状态与允许的流转(状态机约束:防止非法跳转)
|
|
22
22
|
_VALID_TRANSITIONS = {
|
|
23
23
|
"created": {"queued", "failed"},
|
|
24
24
|
"queued": {"scheduled", "failed"},
|
|
25
25
|
"scheduled": {"running", "failed"},
|
|
26
26
|
"running": {"completed", "failed"},
|
|
27
|
-
"completed": set(),
|
|
28
|
-
"failed": set(),
|
|
27
|
+
"completed": set(),
|
|
28
|
+
"failed": set(),
|
|
29
29
|
}
|
|
30
30
|
_TERMINAL = {"completed", "failed"}
|
|
31
31
|
|
|
@@ -33,15 +33,15 @@ _TERMINAL = {"completed", "failed"}
|
|
|
33
33
|
@dataclass
|
|
34
34
|
class ProcessRecord:
|
|
35
35
|
task: str
|
|
36
|
-
agent_id: str = None
|
|
36
|
+
agent_id: Optional[str] = None
|
|
37
37
|
state: str = "created"
|
|
38
38
|
process_id: str = field(default_factory=lambda: f"proc_{uuid.uuid4().hex[:8]}")
|
|
39
39
|
priority: float = 0.0
|
|
40
|
-
route_hint: str = None
|
|
40
|
+
route_hint: Optional[str] = None
|
|
41
41
|
created_at: float = field(default_factory=time.time)
|
|
42
42
|
updated_at: float = field(default_factory=time.time)
|
|
43
43
|
state_history: list = field(default_factory=list)
|
|
44
|
-
fail_reason: str = None
|
|
44
|
+
fail_reason: Optional[str] = None
|
|
45
45
|
|
|
46
46
|
def to_dict(self) -> dict:
|
|
47
47
|
return {
|
|
@@ -58,21 +58,13 @@ class ProcessManager:
|
|
|
58
58
|
|
|
59
59
|
def __init__(self, bus=None, history_limit: int = 1000):
|
|
60
60
|
self._processes: dict[str, ProcessRecord] = {}
|
|
61
|
-
self.bus = bus
|
|
61
|
+
self.bus = bus
|
|
62
62
|
self._history_limit = history_limit
|
|
63
|
-
# 【深审优化】活跃(非终态)进程数的 O(1) 增量计数:create 时 +1、进入终态时 -1
|
|
64
|
-
# (集中在 _transition)。供 CognitiveState 每次 lifecycle 事件 O(1) 取用,
|
|
65
|
-
# 免去原先每事件 stats() 遍历全部进程(O(n),n≤1000)。
|
|
66
63
|
self._active = 0
|
|
67
|
-
# 并行子 Agent 会让多个线程并发 create/mark_*/_evict 同一个 _processes dict,
|
|
68
|
-
# 且状态机流转是"读 state → 校验 → 写 state"的非原子序列。RLock 保证:
|
|
69
|
-
# · dict 增删与遍历(list/stats/_evict)不撞"dict changed size";
|
|
70
|
-
# · 同一进程的并发二次流转不会都通过终态校验后重复 transition。
|
|
71
|
-
# 可重入:_emit→bus.publish 的处理器可能回调 pm 的方法(同线程重入安全)。
|
|
72
64
|
self._lock = threading.RLock()
|
|
73
65
|
|
|
74
|
-
def create(self, task: str, agent_id: str = None,
|
|
75
|
-
schedule: dict = None) -> ProcessRecord:
|
|
66
|
+
def create(self, task: str, agent_id: str | None = None,
|
|
67
|
+
schedule: dict | None = None) -> ProcessRecord:
|
|
76
68
|
"""创建进程(任务进入系统)。携带调度决策。"""
|
|
77
69
|
rec = ProcessRecord(
|
|
78
70
|
task=task, agent_id=agent_id,
|
|
@@ -81,36 +73,33 @@ class ProcessManager:
|
|
|
81
73
|
)
|
|
82
74
|
with self._lock:
|
|
83
75
|
rec.state_history.append("created")
|
|
84
|
-
# 创建后立即入队 → 已调度(调度决策此时已有)
|
|
85
76
|
self._transition(rec, "queued")
|
|
86
77
|
if schedule:
|
|
87
78
|
self._transition(rec, "scheduled")
|
|
88
79
|
self._processes[rec.process_id] = rec
|
|
89
|
-
self._active += 1
|
|
80
|
+
self._active += 1
|
|
90
81
|
self._evict_if_needed()
|
|
91
82
|
self._emit(rec)
|
|
92
83
|
return rec
|
|
93
84
|
|
|
94
|
-
def mark_running(self, process_id: str) -> ProcessRecord:
|
|
85
|
+
def mark_running(self, process_id: str) -> Optional[ProcessRecord]:
|
|
95
86
|
with self._lock:
|
|
96
87
|
rec = self._processes.get(process_id)
|
|
97
88
|
if rec is None:
|
|
98
89
|
return None
|
|
99
|
-
# 若仍在 queued(create 未带 schedule),先补 scheduled 再 running
|
|
100
90
|
if rec.state == "queued":
|
|
101
91
|
self._transition(rec, "scheduled")
|
|
102
92
|
return self._safe_transition(process_id, "running")
|
|
103
93
|
|
|
104
|
-
def mark_completed(self, process_id: str) -> ProcessRecord:
|
|
94
|
+
def mark_completed(self, process_id: str) -> Optional[ProcessRecord]:
|
|
105
95
|
with self._lock:
|
|
106
96
|
return self._safe_transition(process_id, "completed")
|
|
107
97
|
|
|
108
|
-
def mark_failed(self, process_id: str, reason: str = "") -> ProcessRecord:
|
|
98
|
+
def mark_failed(self, process_id: str, reason: str = "") -> Optional[ProcessRecord]:
|
|
109
99
|
with self._lock:
|
|
110
100
|
rec = self._processes.get(process_id)
|
|
111
101
|
if rec is None:
|
|
112
102
|
return None
|
|
113
|
-
# 已是终态 → 幂等忽略(重试/并发下的二次标记不算错误,不崩溃)
|
|
114
103
|
if rec.state in _TERMINAL:
|
|
115
104
|
return rec
|
|
116
105
|
self._transition(rec, "failed")
|
|
@@ -118,11 +107,11 @@ class ProcessManager:
|
|
|
118
107
|
self._emit(rec)
|
|
119
108
|
return rec
|
|
120
109
|
|
|
121
|
-
def get(self, process_id: str) -> ProcessRecord:
|
|
110
|
+
def get(self, process_id: str) -> Optional[ProcessRecord]:
|
|
122
111
|
with self._lock:
|
|
123
112
|
return self._processes.get(process_id)
|
|
124
113
|
|
|
125
|
-
def list(self, state: str = None, limit: int = 50) -> list:
|
|
114
|
+
def list(self, state: str | None = None, limit: int = 50) -> list:
|
|
126
115
|
with self._lock:
|
|
127
116
|
items = list(self._processes.values())
|
|
128
117
|
if state:
|
|
@@ -138,21 +127,18 @@ class ProcessManager:
|
|
|
138
127
|
|
|
139
128
|
def stats(self) -> dict:
|
|
140
129
|
with self._lock:
|
|
141
|
-
counts = {}
|
|
130
|
+
counts: dict[str, int] = {}
|
|
142
131
|
for r in self._processes.values():
|
|
143
132
|
counts[r.state] = counts.get(r.state, 0) + 1
|
|
144
133
|
return {"total": len(self._processes), "by_state": counts,
|
|
145
134
|
"active": self._active}
|
|
146
135
|
|
|
147
|
-
|
|
148
|
-
def _safe_transition(self, process_id: str, next_state: str) -> ProcessRecord:
|
|
136
|
+
def _safe_transition(self, process_id: str, next_state: str) -> Optional[ProcessRecord]:
|
|
149
137
|
rec = self._processes.get(process_id)
|
|
150
138
|
if rec is None:
|
|
151
139
|
return None
|
|
152
|
-
# 已是终态 → 幂等忽略(不崩溃)
|
|
153
140
|
if rec.state in _TERMINAL:
|
|
154
141
|
return rec
|
|
155
|
-
# 已在目标状态 → 幂等忽略(重复标记不报错)
|
|
156
142
|
if rec.state == next_state:
|
|
157
143
|
return rec
|
|
158
144
|
self._transition(rec, next_state)
|
|
@@ -167,15 +153,12 @@ class ProcessManager:
|
|
|
167
153
|
新增调用点直接调本方法,活跃计数会被重复扣减(可扣成负数,stats 失真)。
|
|
168
154
|
这里是双保险。注意行为变更:终态 → 任何状态,从抛 ValueError 变为静默忽略。
|
|
169
155
|
"""
|
|
170
|
-
# 已是终态 → 幂等忽略(不再 -1,不再改状态)
|
|
171
156
|
if rec.state in _TERMINAL:
|
|
172
157
|
return
|
|
173
158
|
allowed = _VALID_TRANSITIONS.get(rec.state, set())
|
|
174
159
|
if next_state not in allowed:
|
|
175
160
|
raise ValueError(
|
|
176
161
|
f"非法状态流转: {rec.state} → {next_state}(进程 {rec.process_id})")
|
|
177
|
-
# 进入终态 → 活跃计数 -1(每进程恰好 create 时 +1、首次入终态时 -1;
|
|
178
|
-
# 上面的终态守卫保证不会重复扣减)。
|
|
179
162
|
if next_state in _TERMINAL:
|
|
180
163
|
self._active -= 1
|
|
181
164
|
rec.state = next_state
|
|
@@ -183,8 +166,6 @@ class ProcessManager:
|
|
|
183
166
|
rec.state_history.append(next_state)
|
|
184
167
|
|
|
185
168
|
def _emit(self, rec: ProcessRecord):
|
|
186
|
-
# 【v2 ① Step3·Lifecycle 统一】emit 唯一生命周期词汇 task.lifecycle.<state>
|
|
187
|
-
# (取代旧 process.state_changed;Scheduler 委托本 pm,不再自发另一套词汇)。
|
|
188
169
|
if self.bus is not None:
|
|
189
170
|
try:
|
|
190
171
|
self.bus.publish(f"task.lifecycle.{rec.state}", {
|