foxclaw 0.1.0 → 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/README.md +54 -29
- package/bin/foxclaw.cjs +51 -0
- package/index.js +4 -10
- package/package.json +46 -38
- package/python/.dockerignore +35 -0
- package/python/.env.example +173 -0
- package/python/.gitattributes +46 -0
- package/python/.github/workflows/ci.yml +167 -0
- package/python/.github/workflows/release.yml +183 -0
- package/python/CHANGELOG.md +57 -0
- package/python/CHANGELOG_CN.md +57 -0
- package/python/Dockerfile +64 -0
- package/python/LICENSE +21 -0
- package/python/MANIFEST.in +14 -0
- package/python/README.md +201 -0
- package/python/README_CN.md +336 -0
- package/python/bench/bm25_audit.py +186 -0
- package/python/bench/bm25_regression.py +177 -0
- package/python/bench/bm25_stress.py +155 -0
- package/python/bench/cd_audit.py +215 -0
- package/python/bench/chaos.py +140 -0
- package/python/bench/concurrency.py +103 -0
- package/python/bench/e2e_real_models.py +94 -0
- package/python/bench/llm_retry_test.py +241 -0
- package/python/bench/load_test.py +193 -0
- package/python/bench/vector_ivf_upgrade.py +128 -0
- package/python/docs/CHANNELS.md +195 -0
- package/python/docs/GOVERNANCE.md +164 -0
- package/python/docs/RELEASE.md +110 -0
- package/python/docs/RUN.md +75 -0
- package/python/docs/RUN_CN.md +121 -0
- package/python/docs//351/205/215/347/275/256/346/214/207/345/215/227.md +152 -0
- package/python/evals/__init__.py +0 -0
- package/python/evals/governance/README.md +61 -0
- package/python/evals/governance/__init__.py +0 -0
- package/python/evals/governance/cases.py +115 -0
- package/python/evals/governance/compare_embeddings.py +94 -0
- package/python/evals/governance/convergence.svg +11 -0
- package/python/evals/governance/report.json +371 -0
- package/python/evals/governance/run_evals.py +368 -0
- package/python/examples/governance_middleware_demo.py +72 -0
- package/python/examples/langgraph_governance_demo.py +104 -0
- package/python/examples/sample_itinerary.csv +6 -0
- package/python/examples/sample_knowledge.csv +9 -0
- package/python/examples/sample_transactions.csv +14 -0
- package/python/foxclaw/__init__.py +408 -0
- package/python/foxclaw/agents/__init__.py +12 -0
- package/python/foxclaw/agents/agents.py +35 -0
- package/python/foxclaw/agents/base.py +794 -0
- package/python/foxclaw/agents/impl/__init__.py +1 -0
- package/python/foxclaw/agents/impl/_helpers.py +70 -0
- package/python/foxclaw/agents/impl/finance.py +231 -0
- package/python/foxclaw/agents/impl/office.py +350 -0
- package/python/foxclaw/agents/impl/supervisor.py +592 -0
- package/python/foxclaw/agents/mixins/_context.py +128 -0
- package/python/foxclaw/agents/mixins/augment.py +143 -0
- package/python/foxclaw/agents/mixins/llm.py +67 -0
- package/python/foxclaw/agents/mixins/memory.py +41 -0
- package/python/foxclaw/agents/mixins/preference.py +131 -0
- package/python/foxclaw/agents/mixins/prompt.py +71 -0
- package/python/foxclaw/agents/mixins/skillbox.py +58 -0
- package/python/foxclaw/agents/mixins/tool_exec.py +417 -0
- package/python/foxclaw/api/__init__.py +224 -0
- package/python/foxclaw/api/_state.py +43 -0
- package/python/foxclaw/api/bootstrap.py +96 -0
- package/python/foxclaw/api/middleware.py +294 -0
- package/python/foxclaw/api/routes/ahs.py +404 -0
- package/python/foxclaw/api/routes/attachments.py +116 -0
- package/python/foxclaw/api/routes/browser.py +35 -0
- package/python/foxclaw/api/routes/domain.py +306 -0
- package/python/foxclaw/api/routes/knowledge.py +145 -0
- package/python/foxclaw/api/routes/llm_admin.py +449 -0
- package/python/foxclaw/api/routes/memory.py +105 -0
- package/python/foxclaw/api/routes/orchestration.py +39 -0
- package/python/foxclaw/api/routes/recovery.py +44 -0
- package/python/foxclaw/api/routes/review.py +266 -0
- package/python/foxclaw/api/routes/search.py +33 -0
- package/python/foxclaw/api/routes/system.py +311 -0
- package/python/foxclaw/api/routes/task.py +263 -0
- package/python/foxclaw/api/routes/ws.py +64 -0
- package/python/foxclaw/api/spine.py +52 -0
- package/python/foxclaw/browser/__init__.py +13 -0
- package/python/foxclaw/browser/action/__init__.py +1 -0
- package/python/foxclaw/browser/action/action_executor.py +50 -0
- package/python/foxclaw/browser/action/action_types.py +44 -0
- package/python/foxclaw/browser/action/action_verifier.py +68 -0
- package/python/foxclaw/browser/action/url_policy.py +143 -0
- package/python/foxclaw/browser/adapter/__init__.py +1 -0
- package/python/foxclaw/browser/adapter/base_adapter.py +23 -0
- package/python/foxclaw/browser/adapter/web_adapter.py +46 -0
- package/python/foxclaw/browser/driver/__init__.py +1 -0
- package/python/foxclaw/browser/driver/base_driver.py +61 -0
- package/python/foxclaw/browser/driver/playwright_driver.py +170 -0
- package/python/foxclaw/browser/perception/__init__.py +1 -0
- package/python/foxclaw/browser/perception/dom_extractor.py +42 -0
- package/python/foxclaw/browser/perception/element_indexer.py +56 -0
- package/python/foxclaw/browser/perception/semantic_reducer.py +33 -0
- package/python/foxclaw/browser/perception/snapshot.py +29 -0
- package/python/foxclaw/browser/service.py +144 -0
- package/python/foxclaw/browser/session/__init__.py +1 -0
- package/python/foxclaw/browser/session/session_manager.py +57 -0
- package/python/foxclaw/capability/__init__.py +12 -0
- package/python/foxclaw/capability/embedding_service.py +65 -0
- package/python/foxclaw/channels/__init__.py +138 -0
- package/python/foxclaw/channels/discord.py +175 -0
- package/python/foxclaw/channels/dispatcher.py +179 -0
- package/python/foxclaw/channels/feishu.py +215 -0
- package/python/foxclaw/channels/openclaw_bridge.py +151 -0
- package/python/foxclaw/channels/signing.py +187 -0
- package/python/foxclaw/channels/telegram.py +120 -0
- package/python/foxclaw/channels/weixin.py +215 -0
- package/python/foxclaw/cli.py +183 -0
- package/python/foxclaw/cognitive/__init__.py +4 -0
- package/python/foxclaw/cognitive/reasoner.py +81 -0
- package/python/foxclaw/cognitive/state.py +141 -0
- package/python/foxclaw/core/__init__.py +9 -0
- package/python/foxclaw/core/audit_chain.py +131 -0
- package/python/foxclaw/core/audit_sink.py +100 -0
- package/python/foxclaw/core/compaction.py +265 -0
- package/python/foxclaw/core/config.py +65 -0
- package/python/foxclaw/core/confirm_nonce.py +151 -0
- package/python/foxclaw/core/dispatch.py +27 -0
- package/python/foxclaw/core/envfile.py +48 -0
- package/python/foxclaw/core/failure_policy.py +142 -0
- package/python/foxclaw/core/gateway.py +81 -0
- package/python/foxclaw/core/governance_audit.py +171 -0
- package/python/foxclaw/core/idempotency.py +282 -0
- package/python/foxclaw/core/injection_guard.py +202 -0
- package/python/foxclaw/core/memory_adapter.py +489 -0
- package/python/foxclaw/core/metrics.py +88 -0
- package/python/foxclaw/core/model_router.py +985 -0
- package/python/foxclaw/core/paths.py +103 -0
- package/python/foxclaw/core/recovery.py +349 -0
- package/python/foxclaw/core/scheduler.py +141 -0
- package/python/foxclaw/core/secrets.py +131 -0
- package/python/foxclaw/core/sessions.py +155 -0
- package/python/foxclaw/core/settings.py +414 -0
- package/python/foxclaw/core/shared_write.py +186 -0
- package/python/foxclaw/core/steering_queue.py +154 -0
- package/python/foxclaw/core/system_bus.py +84 -0
- package/python/foxclaw/core/thought_chain.py +316 -0
- package/python/foxclaw/core/token_meter.py +231 -0
- package/python/foxclaw/core/tools.py +164 -0
- package/python/foxclaw/core/tracing.py +375 -0
- package/python/foxclaw/core/truth_pipeline.py +110 -0
- package/python/foxclaw/core/update_check.py +149 -0
- package/python/foxclaw/core/vec_recall_adapter.py +477 -0
- package/python/foxclaw/core/verify.py +173 -0
- package/python/foxclaw/core/web_search.py +218 -0
- package/python/foxclaw/finance/__init__.py +15 -0
- package/python/foxclaw/finance/evidence.py +149 -0
- package/python/foxclaw/finance/fx.py +98 -0
- package/python/foxclaw/finance/graph.py +179 -0
- package/python/foxclaw/finance/models.py +146 -0
- package/python/foxclaw/finance/nlparse.py +515 -0
- package/python/foxclaw/finance/policy.py +301 -0
- package/python/foxclaw/finance/reasoner.py +121 -0
- package/python/foxclaw/finance/reasoning.py +319 -0
- package/python/foxclaw/finance/reconcile.py +175 -0
- package/python/foxclaw/finance/runtime.py +500 -0
- package/python/foxclaw/governance/__init__.py +24 -0
- package/python/foxclaw/governance/middleware.py +176 -0
- package/python/foxclaw/harness/__init__.py +17 -0
- package/python/foxclaw/harness/_base.py +66 -0
- package/python/foxclaw/harness/_policy.py +340 -0
- package/python/foxclaw/harness/drift.py +77 -0
- package/python/foxclaw/harness/event_bus.py +28 -0
- package/python/foxclaw/harness/harness.py +358 -0
- package/python/foxclaw/harness/loop_detection.py +145 -0
- package/python/foxclaw/itinerary/__init__.py +10 -0
- package/python/foxclaw/itinerary/amap_client.py +114 -0
- package/python/foxclaw/itinerary/models.py +80 -0
- package/python/foxclaw/itinerary/reasoner.py +96 -0
- package/python/foxclaw/itinerary/reasoning.py +168 -0
- package/python/foxclaw/itinerary/runtime.py +144 -0
- package/python/foxclaw/kernel/__init__.py +13 -0
- package/python/foxclaw/kernel/capabilities.py +72 -0
- package/python/foxclaw/kernel/policy_gate.py +108 -0
- package/python/foxclaw/kernel/process_manager.py +186 -0
- package/python/foxclaw/kernel/scheduler.py +125 -0
- package/python/foxclaw/kernel/sentinel.py +443 -0
- package/python/foxclaw/kernel/sentinel_client.py +243 -0
- package/python/foxclaw/kernel/spine.py +97 -0
- package/python/foxclaw/knowledge/__init__.py +360 -0
- package/python/foxclaw/knowledge/truth_crystallizer.py +89 -0
- package/python/foxclaw/learning/__init__.py +12 -0
- package/python/foxclaw/learning/knowledge_graph.py +144 -0
- package/python/foxclaw/learning/models.py +93 -0
- package/python/foxclaw/learning/reasoner.py +67 -0
- package/python/foxclaw/learning/runtime.py +351 -0
- package/python/foxclaw/learning/spaced_repetition.py +91 -0
- package/python/foxclaw/mcp_server.py +188 -0
- package/python/foxclaw/orchestration/__init__.py +167 -0
- package/python/foxclaw/pipeline/__init__.py +23 -0
- package/python/foxclaw/pipeline/cognitive_pipeline.py +429 -0
- package/python/foxclaw/pipeline/stage_meta.py +21 -0
- package/python/foxclaw/pipeline/supervisor_stages.py +201 -0
- package/python/foxclaw/planning/__init__.py +17 -0
- package/python/foxclaw/planning/llm_planner.py +232 -0
- package/python/foxclaw/planning/planner.py +94 -0
- package/python/foxclaw/search/__init__.py +24 -0
- package/python/foxclaw/search/agent_port.py +190 -0
- package/python/foxclaw/search/evidence.py +109 -0
- package/python/foxclaw/search/external_search.py +50 -0
- package/python/foxclaw/search/hkc_read_port.py +60 -0
- package/python/foxclaw/search/internal_search.py +71 -0
- package/python/foxclaw/search/models.py +127 -0
- package/python/foxclaw/search/planner.py +44 -0
- package/python/foxclaw/search/retriever.py +39 -0
- package/python/foxclaw/search/runtime.py +127 -0
- package/python/foxclaw/search/strategy.py +44 -0
- package/python/foxclaw/skills/__init__.py +559 -0
- package/python/foxclaw/skills/archive.py +111 -0
- package/python/foxclaw/skills/fetch.py +226 -0
- package/python/foxclaw/skills/hooks_spec.py +69 -0
- package/python/foxclaw/skills/install_cmd.py +216 -0
- package/python/foxclaw/skills/lifecycle.py +252 -0
- package/python/foxclaw/skills/markdown_skill.py +175 -0
- package/python/foxclaw/skills/markdown_store.py +39 -0
- package/python/foxclaw/skills/mcp_client.py +184 -0
- package/python/foxclaw/skills/mcp_store.py +40 -0
- package/python/foxclaw/skills/permission.py +145 -0
- package/python/foxclaw/skills/pipeline.py +354 -0
- package/python/foxclaw/skills/policy.py +74 -0
- package/python/foxclaw/skills/review.py +72 -0
- package/python/foxclaw/skills/store.py +57 -0
- package/python/frontend/assets/core_bg.jpg +0 -0
- package/python/frontend/deck/auth.js +25 -0
- package/python/frontend/deck/chat_model_picker.js +483 -0
- package/python/frontend/deck/domain.js +276 -0
- package/python/frontend/deck/main.js +1806 -0
- package/python/frontend/deck/markdown.js +44 -0
- package/python/frontend/deck/settings.js +307 -0
- package/python/frontend/deck/state.js +5 -0
- package/python/frontend/deck/util.js +4 -0
- package/python/frontend/deck/visuals.js +190 -0
- package/python/frontend/deck.css +855 -0
- package/python/frontend/deck.html +686 -0
- package/python/frontend/deck_v2.html +341 -0
- package/python/frontend/planet/index.html +266 -0
- package/python/frontend/planet/planet.css +579 -0
- package/python/frontend/planet/planet.js +1436 -0
- package/python/frontend/planet/vendor/three.core.min.js +6 -0
- package/python/frontend/planet/vendor/three.module.min.js +6 -0
- 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 +15 -0
- package/python/frontend/widget_runtime.js +252 -0
- package/python/frontend/widgets.js +639 -0
- package/python/frontend_src/index.html +14 -0
- package/python/frontend_src/package-lock.json +1376 -0
- package/python/frontend_src/package.json +19 -0
- package/python/frontend_src/src/App.vue +58 -0
- package/python/frontend_src/src/api.js +82 -0
- package/python/frontend_src/src/components/ChatDock.vue +365 -0
- package/python/frontend_src/src/components/ComposerMenu.vue +75 -0
- package/python/frontend_src/src/components/DocViewer.vue +162 -0
- package/python/frontend_src/src/components/Inspector.vue +235 -0
- package/python/frontend_src/src/components/LeftRail.vue +415 -0
- package/python/frontend_src/src/components/MenuPopover.vue +128 -0
- package/python/frontend_src/src/components/MessageStream.vue +389 -0
- package/python/frontend_src/src/components/ModelPicker.vue +350 -0
- package/python/frontend_src/src/components/QuickIngest.vue +112 -0
- package/python/frontend_src/src/components/Stage.vue +272 -0
- package/python/frontend_src/src/components/ToolMenu.vue +141 -0
- package/python/frontend_src/src/components/TopBar.vue +118 -0
- package/python/frontend_src/src/components/panels/ActivityPanel.vue +82 -0
- package/python/frontend_src/src/components/panels/AgentsPanel.vue +83 -0
- package/python/frontend_src/src/components/panels/KnowledgePanel.vue +425 -0
- package/python/frontend_src/src/components/panels/MemoryPanel.vue +117 -0
- package/python/frontend_src/src/components/panels/PanelBox.vue +50 -0
- package/python/frontend_src/src/components/panels/ReviewPanel.vue +292 -0
- package/python/frontend_src/src/components/panels/SettingsPanel.vue +358 -0
- package/python/frontend_src/src/components/panels/TasksPanel.vue +73 -0
- package/python/frontend_src/src/components/panels/ToolsPanel.vue +746 -0
- package/python/frontend_src/src/components/panels/UsagePanel.vue +105 -0
- package/python/frontend_src/src/lib/fmt.js +44 -0
- 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 +190 -0
- package/python/frontend_src/src/main.js +9 -0
- package/python/frontend_src/src/stores/app.js +91 -0
- package/python/frontend_src/src/stores/chat.js +450 -0
- package/python/frontend_src/src/stores/model.js +96 -0
- package/python/frontend_src/src/stores/panels.js +289 -0
- package/python/frontend_src/src/stores/profile.js +31 -0
- package/python/frontend_src/src/stores/system.js +25 -0
- package/python/frontend_src/src/styles/base.css +138 -0
- package/python/frontend_src/src/styles/panel.css +117 -0
- package/python/frontend_src/src/styles/tokens.css +78 -0
- package/python/frontend_src/vite.config.js +39 -0
- package/python/pyproject.toml +268 -0
- package/python/requirements.txt +311 -0
- package/python/skills/README.md +123 -0
- package/python/skills/execution-contract/SKILL.md +30 -0
- package/python/skills/general-assistant/SKILL.md +34 -0
- package/python/skills/planner/SKILL.md +33 -0
- package/python/skills/schedule-itinerary/SKILL.md +35 -0
- package/python/skills/skill-composition/SKILL.md +31 -0
- package/python/skills/skill-guard/SKILL.md +67 -0
- package/python/skills/skill-memory/SKILL.md +50 -0
- package/python/skills/skill-router/SKILL.md +35 -0
- package/python/tests/conftest.py +54 -0
- package/python/tests/run_all.py +200 -0
- package/python/tests/test_agent_lifecycle.py +326 -0
- package/python/tests/test_ahs_p2.py +393 -0
- package/python/tests/test_anthropic_tools.py +192 -0
- package/python/tests/test_api.py +97 -0
- package/python/tests/test_api_auth.py +79 -0
- package/python/tests/test_api_host_guard.py +69 -0
- package/python/tests/test_attachments.py +151 -0
- package/python/tests/test_audit_chain.py +116 -0
- package/python/tests/test_audit_sink.py +130 -0
- package/python/tests/test_browser.py +149 -0
- package/python/tests/test_browser_request_guard.py +180 -0
- package/python/tests/test_capabilities.py +152 -0
- package/python/tests/test_channel_signing.py +185 -0
- package/python/tests/test_channel_trust.py +109 -0
- package/python/tests/test_channels.py +174 -0
- package/python/tests/test_cli.py +46 -0
- package/python/tests/test_compaction_toolpair.py +119 -0
- package/python/tests/test_config_layering.py +61 -0
- package/python/tests/test_confirm_nonce.py +186 -0
- package/python/tests/test_context_compaction_wiring.py +124 -0
- package/python/tests/test_dispatch.py +89 -0
- package/python/tests/test_domain_evals.py +334 -0
- package/python/tests/test_domain_import.py +72 -0
- package/python/tests/test_domain_reasoners.py +134 -0
- package/python/tests/test_e2e.py +161 -0
- package/python/tests/test_emb_cache.py +97 -0
- package/python/tests/test_embedding_singleton.py +87 -0
- package/python/tests/test_envfile.py +83 -0
- package/python/tests/test_evals_smoke.py +90 -0
- package/python/tests/test_event_spine.py +174 -0
- package/python/tests/test_evidence_semantic.py +88 -0
- package/python/tests/test_evidence_sse.py +94 -0
- package/python/tests/test_file_ingest.py +135 -0
- package/python/tests/test_finance.py +144 -0
- package/python/tests/test_finance_ledger.py +245 -0
- package/python/tests/test_finance_persist.py +174 -0
- package/python/tests/test_gateway.py +125 -0
- package/python/tests/test_governance.py +421 -0
- package/python/tests/test_governance_audit.py +324 -0
- package/python/tests/test_governance_middleware.py +137 -0
- package/python/tests/test_hkc.py +268 -0
- package/python/tests/test_hkc_index_build_manager.py +306 -0
- package/python/tests/test_hkc_manifest_reuse.py +294 -0
- package/python/tests/test_hkc_search_event_sync.py +482 -0
- package/python/tests/test_import_path_guard.py +155 -0
- package/python/tests/test_injection_guard.py +193 -0
- package/python/tests/test_integration.py +117 -0
- package/python/tests/test_itinerary.py +100 -0
- package/python/tests/test_kee_semantic.py +84 -0
- package/python/tests/test_kernel.py +212 -0
- package/python/tests/test_learning.py +101 -0
- package/python/tests/test_llm_admin.py +145 -0
- package/python/tests/test_llm_planner.py +344 -0
- package/python/tests/test_markdown_skills.py +62 -0
- package/python/tests/test_markdown_skills_openclaw.py +66 -0
- package/python/tests/test_mcp_connectors.py +97 -0
- package/python/tests/test_mcp_guardian_discovery.py +87 -0
- package/python/tests/test_mcp_no_memory.py +43 -0
- package/python/tests/test_mcp_policy_gate.py +76 -0
- package/python/tests/test_mcp_server.py +92 -0
- package/python/tests/test_memory_recall.py +92 -0
- package/python/tests/test_metrics.py +58 -0
- package/python/tests/test_multi_chain.py +90 -0
- package/python/tests/test_multimodal.py +122 -0
- package/python/tests/test_negotiation.py +136 -0
- package/python/tests/test_orchestration.py +109 -0
- package/python/tests/test_orchestration_decoupled.py +103 -0
- package/python/tests/test_p3_p5_hardening.py +324 -0
- package/python/tests/test_parallel_dispatch.py +207 -0
- package/python/tests/test_pipeline.py +492 -0
- package/python/tests/test_plan_visible.py +86 -0
- package/python/tests/test_planner.py +88 -0
- package/python/tests/test_policy_gate.py +108 -0
- package/python/tests/test_profile.py +67 -0
- package/python/tests/test_readme_claims.py +163 -0
- package/python/tests/test_reasoner.py +170 -0
- package/python/tests/test_release_check.py +252 -0
- package/python/tests/test_resume_checkpoint.py +148 -0
- package/python/tests/test_routing.py +85 -0
- package/python/tests/test_scheduler.py +138 -0
- package/python/tests/test_search.py +194 -0
- package/python/tests/test_secrets.py +153 -0
- package/python/tests/test_semantic.py +69 -0
- package/python/tests/test_sessions.py +90 -0
- package/python/tests/test_settings.py +265 -0
- package/python/tests/test_shadow_learning.py +290 -0
- package/python/tests/test_shared_write.py +200 -0
- package/python/tests/test_skill_taint_gate.py +145 -0
- package/python/tests/test_skills.py +117 -0
- package/python/tests/test_skills_fetch.py +282 -0
- package/python/tests/test_stats_and_events.py +162 -0
- package/python/tests/test_streaming.py +55 -0
- package/python/tests/test_supervisor_pipeline.py +176 -0
- package/python/tests/test_supervisor_triage.py +102 -0
- package/python/tests/test_taint.py +112 -0
- package/python/tests/test_task_stream.py +74 -0
- package/python/tests/test_thinking_sse.py +66 -0
- package/python/tests/test_thinking_stream.py +76 -0
- package/python/tests/test_tool_center.py +113 -0
- package/python/tests/test_tool_parallel.py +113 -0
- package/python/tests/test_tool_params.py +99 -0
- package/python/tests/test_tool_use.py +229 -0
- package/python/tests/test_toplevel_concurrency.py +116 -0
- package/python/tests/test_tracing.py +256 -0
- package/python/tests/test_truth_crystallizer.py +125 -0
- package/python/tests/test_truth_flow.py +88 -0
- package/python/tests/test_update_check.py +95 -0
- package/python/tests/test_url_policy.py +53 -0
- package/python/tests/test_verify_evidence.py +212 -0
- package/python/tests/test_version_sync.py +100 -0
- package/python/tests/test_web_trigger.py +82 -0
- package/python/tools/check_faiss_type.py +30 -0
- package/python/tools/check_log.ps1 +38 -0
- package/python/tools/ivf_status.py +72 -0
- package/python/tools/mypy_baseline.txt +13 -0
- package/python/tools/mypy_ratchet.py +143 -0
- package/python/tools/pack.py +173 -0
- package/python/tools/release_check.py +349 -0
- package/python/tools/restart_foxclaw.ps1 +71 -0
- package/python/tools/shared_write_audit.py +190 -0
- package/python/tools/size_baseline.txt +28 -0
- package/python/tools/size_check.py +102 -0
- package/python/tools/strip_comments.py +228 -0
- package/python/tools/verify_env.bat +2 -0
- package/python/tools/verify_env.py +25 -0
- package/python/vendor/HMR_LICENSE +37 -0
- package/python/vendor/HMR_README.md +65 -0
- package/python/vendor/hkc/LICENSE +37 -0
- package/python/vendor/hkc/hkc_ace/__init__.py +0 -0
- package/python/vendor/hkc/hkc_ace/ace.py +457 -0
- package/python/vendor/hkc/hkc_ace/skill_taxonomy.json +465 -0
- package/python/vendor/hkc/hkc_api/__init__.py +0 -0
- package/python/vendor/hkc/hkc_api/container.py +416 -0
- package/python/vendor/hkc/hkc_api/ingest_jobs.py +103 -0
- package/python/vendor/hkc/hkc_api/main.py +157 -0
- package/python/vendor/hkc/hkc_api/routes/__init__.py +0 -0
- package/python/vendor/hkc/hkc_api/routes/ability.py +151 -0
- package/python/vendor/hkc/hkc_api/routes/config.py +51 -0
- package/python/vendor/hkc/hkc_api/routes/knowledge.py +630 -0
- package/python/vendor/hkc/hkc_api/routes/search.py +88 -0
- package/python/vendor/hkc/hkc_api/schemas.py +153 -0
- package/python/vendor/hkc/hkc_api/synthesis.py +179 -0
- package/python/vendor/hkc/hkc_core/__init__.py +0 -0
- package/python/vendor/hkc/hkc_core/graph/__init__.py +0 -0
- package/python/vendor/hkc/hkc_core/graph/base.py +188 -0
- package/python/vendor/hkc/hkc_core/graph/sqlite_store.py +789 -0
- package/python/vendor/hkc/hkc_core/models/__init__.py +0 -0
- package/python/vendor/hkc/hkc_core/models/enums.py +65 -0
- package/python/vendor/hkc/hkc_core/models/ku.py +307 -0
- package/python/vendor/hkc/hkc_core/utils/__init__.py +0 -0
- package/python/vendor/hkc/hkc_core/utils/id_gen.py +113 -0
- package/python/vendor/hkc/hkc_crystallizer/__init__.py +44 -0
- package/python/vendor/hkc/hkc_crystallizer/adapters.py +94 -0
- package/python/vendor/hkc/hkc_crystallizer/candidate.py +145 -0
- package/python/vendor/hkc/hkc_crystallizer/crystallizer.py +179 -0
- package/python/vendor/hkc/hkc_crystallizer/ingress.py +90 -0
- package/python/vendor/hkc/hkc_kde/__init__.py +0 -0
- package/python/vendor/hkc/hkc_kde/chunker.py +197 -0
- package/python/vendor/hkc/hkc_kde/extractor.py +623 -0
- package/python/vendor/hkc/hkc_kde/kde.py +298 -0
- package/python/vendor/hkc/hkc_kde/loaders/__init__.py +0 -0
- package/python/vendor/hkc/hkc_kde/loaders/ebook_loader.py +153 -0
- package/python/vendor/hkc/hkc_kde/loaders/office_loader.py +194 -0
- package/python/vendor/hkc/hkc_kde/loaders/pdf_loader.py +162 -0
- package/python/vendor/hkc/hkc_kde/loaders/text_loader.py +157 -0
- package/python/vendor/hkc/hkc_kde/loaders/web_loader.py +91 -0
- package/python/vendor/hkc/hkc_kde/models.py +74 -0
- package/python/vendor/hkc/hkc_kde/packager.py +329 -0
- package/python/vendor/hkc/hkc_kde/relation_disc.py +151 -0
- package/python/vendor/hkc/hkc_kee/__init__.py +0 -0
- package/python/vendor/hkc/hkc_kee/dedup.py +227 -0
- package/python/vendor/hkc/hkc_kee/fingerprint.py +106 -0
- package/python/vendor/hkc/hkc_kee/kee.py +541 -0
- package/python/vendor/hkc/hkc_kep/__init__.py +0 -0
- package/python/vendor/hkc/hkc_kep/event_bus.py +205 -0
- package/python/vendor/hkc/hkc_sdk/__init__.py +63 -0
- package/python/vendor/hkc/hkc_sdk/base.py +151 -0
- package/python/vendor/hkc/hkc_sdk/direct_client.py +153 -0
- package/python/vendor/hkc/hkc_sdk/exceptions.py +39 -0
- package/python/vendor/hkc/hkc_sdk/http_client.py +166 -0
- package/python/vendor/hkc/hkc_sdk/models.py +171 -0
- package/python/vendor/hkc/hkc_search/__init__.py +0 -0
- package/python/vendor/hkc/hkc_search/bm25.py +325 -0
- package/python/vendor/hkc/hkc_search/embedding_backends.py +374 -0
- package/python/vendor/hkc/hkc_search/graph_search.py +132 -0
- package/python/vendor/hkc/hkc_search/hybrid.py +331 -0
- package/python/vendor/hkc/hkc_search/index_build_manager.py +378 -0
- package/python/vendor/hkc/hkc_search/vector_search.py +484 -0
- package/python/vendor/hmr/__init__.py +0 -0
- package/python/vendor/hmr/core/__init__.py +0 -0
- package/python/vendor/hmr/core/hmr.py +684 -0
- package/python/vendor/hmr/core/models.py +310 -0
- package/python/vendor/hmr/engines/__init__.py +0 -0
- package/python/vendor/hmr/engines/evolution.py +710 -0
- package/python/vendor/hmr/engines/jit_compiler.py +472 -0
- package/python/vendor/hmr/engines/lifecycle.py +264 -0
- package/python/vendor/hmr/engines/policy.py +625 -0
- package/python/vendor/hmr/engines/recall.py +260 -0
- package/python/vendor/hmr/engines/runtime_state.py +158 -0
- package/python/vendor/hmr/engines/scheduler.py +323 -0
- package/python/vendor/hmr/engines/semantic.py +150 -0
- package/python/vendor/hmr/engines/temporal.py +261 -0
- package/python/vendor/hmr/engines/thought_chain.py +635 -0
- package/python/vendor/hmr/graph/__init__.py +0 -0
- package/python/vendor/hmr/graph/cwg.py +338 -0
- package/python/vendor/hmr/graph/memory_graph.py +530 -0
- package/python/vendor/hmr/storage/__init__.py +0 -0
- package/python/vendor/hmr/storage/memory_fs.py +300 -0
- package/python/vendor/hmr/storage/vector_store.py +520 -0
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
"""
|
|
2
|
+
请求链路追踪(tracing.py)
|
|
3
|
+
|
|
4
|
+
与既有可观测设施的分工(不重复造):
|
|
5
|
+
METRICS 聚合视角:各端点/LLM 的计数、错误率、延迟分位 —— 回答"整体健康吗"
|
|
6
|
+
ThoughtChain 单 Agent 的推理节点 —— 回答"它当时怎么想的"
|
|
7
|
+
EventSpine 事件流(correlation_id 逐事件随机)—— 回答"发生过什么"
|
|
8
|
+
Trace(这里) 单次请求视角:一个 trace_id 贯穿 agent→LLM→skill→memory,带层级 span
|
|
9
|
+
与耗时 —— 回答"**这一次**请求慢在哪、调了谁"
|
|
10
|
+
|
|
11
|
+
实现要点:
|
|
12
|
+
· contextvars 传播:与 _suppress_memory 同款。asyncio.to_thread / asyncio.run 会带上
|
|
13
|
+
上下文,故后台线程里的 span 仍归属发起它的请求。
|
|
14
|
+
· span() 在无 trace 时是**零成本 no-op**(不在请求里跑的后台任务不会报错/不产生垃圾)。
|
|
15
|
+
· 有界环形存储:只留最近 N 条 trace,防内存无限增长(与 metrics「unmatched」同款教训)。
|
|
16
|
+
· 线程安全:Trace.spans 与 TraceStore 各自加锁。
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
import json
|
|
22
|
+
import time
|
|
23
|
+
import uuid
|
|
24
|
+
import random
|
|
25
|
+
import sqlite3
|
|
26
|
+
import logging
|
|
27
|
+
import threading
|
|
28
|
+
import contextvars
|
|
29
|
+
from collections import deque
|
|
30
|
+
from contextlib import contextmanager
|
|
31
|
+
from . import settings
|
|
32
|
+
from typing import Any
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _slow_ms() -> float:
|
|
38
|
+
return float(settings.get_safe("FOXCLAW_TRACE_SLOW_MS"))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _sample_rate() -> float:
|
|
42
|
+
return max(0.0, min(1.0, float(settings.get_safe("FOXCLAW_TRACE_SAMPLE"))))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _retain() -> int:
|
|
46
|
+
return max(100, int(settings.get_safe("FOXCLAW_TRACE_RETAIN")))
|
|
47
|
+
|
|
48
|
+
_current: contextvars.ContextVar = contextvars.ContextVar("foxclaw_trace", default=None)
|
|
49
|
+
|
|
50
|
+
MAX_SPANS_PER_TRACE = 200
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
_TP_RE = __import__("re").compile(r"^00-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def parse_traceparent(header: str):
|
|
57
|
+
"""解析入站 traceparent,返回 trace_id(32hex)或 None。非法/缺失一律 None(不信外部输入)。"""
|
|
58
|
+
if not header:
|
|
59
|
+
return None
|
|
60
|
+
m = _TP_RE.match(str(header).strip().lower())
|
|
61
|
+
if not m:
|
|
62
|
+
return None
|
|
63
|
+
tid = m.group(1)
|
|
64
|
+
if tid == "0" * 32:
|
|
65
|
+
return None
|
|
66
|
+
return tid
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def to_traceparent(trace_id: str, span_id: str | None = None) -> str:
|
|
70
|
+
"""给出站请求生成 traceparent。span_id 缺省随机(本进程作为对端的 parent)。"""
|
|
71
|
+
tid = (trace_id or uuid.uuid4().hex)[:32].rjust(32, "0")
|
|
72
|
+
sid = (span_id or uuid.uuid4().hex[:16])[:16].rjust(16, "0")
|
|
73
|
+
return f"00-{tid}-{sid}-01"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def current_traceparent():
|
|
77
|
+
"""当前上下文的 traceparent(无 trace 则 None)——出站调用带上它即可跨进程接链。"""
|
|
78
|
+
tr = _current.get()
|
|
79
|
+
return to_traceparent(tr.trace_id) if tr is not None else None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class Trace:
|
|
83
|
+
def __init__(self, name: str, trace_id: str | None = None):
|
|
84
|
+
self.trace_id = trace_id or uuid.uuid4().hex
|
|
85
|
+
self.name = name
|
|
86
|
+
self.start_ts = time.time()
|
|
87
|
+
self._t0 = time.perf_counter()
|
|
88
|
+
self.duration_ms: float | None = None
|
|
89
|
+
self.spans: list = []
|
|
90
|
+
self._lock = threading.Lock()
|
|
91
|
+
self._dropped = 0
|
|
92
|
+
|
|
93
|
+
def add_span(self, span: dict):
|
|
94
|
+
with self._lock:
|
|
95
|
+
if len(self.spans) >= MAX_SPANS_PER_TRACE:
|
|
96
|
+
self._dropped += 1
|
|
97
|
+
return
|
|
98
|
+
self.spans.append(span)
|
|
99
|
+
|
|
100
|
+
def finish(self):
|
|
101
|
+
self.duration_ms = (time.perf_counter() - self._t0) * 1000.0
|
|
102
|
+
|
|
103
|
+
def has_error(self) -> bool:
|
|
104
|
+
with self._lock:
|
|
105
|
+
return any(not s.get("ok", True) for s in self.spans)
|
|
106
|
+
|
|
107
|
+
def to_dict(self, with_spans: bool = True) -> dict:
|
|
108
|
+
with self._lock:
|
|
109
|
+
spans = list(self.spans)
|
|
110
|
+
d = {
|
|
111
|
+
"trace_id": self.trace_id,
|
|
112
|
+
"name": self.name,
|
|
113
|
+
"start_ts": self.start_ts,
|
|
114
|
+
"duration_ms": round(self.duration_ms, 2) if self.duration_ms is not None else None,
|
|
115
|
+
"span_count": len(spans),
|
|
116
|
+
"dropped_spans": self._dropped,
|
|
117
|
+
}
|
|
118
|
+
if with_spans:
|
|
119
|
+
d["spans"] = spans
|
|
120
|
+
return d
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class TraceStore:
|
|
124
|
+
"""近期用有界环存(快),值得留的落 SQLite(可查历史)。"""
|
|
125
|
+
|
|
126
|
+
def __init__(self, limit: int = 200):
|
|
127
|
+
self._d: deque = deque(maxlen=limit)
|
|
128
|
+
self._lock = threading.Lock()
|
|
129
|
+
self._db_path: str | None = None
|
|
130
|
+
|
|
131
|
+
def attach_db(self, path):
|
|
132
|
+
"""开启持久化并建表。连接**即建即弃、不跨线程复用**(与 ThoughtChain 同款):
|
|
133
|
+
SQLite 连接不可跨线程共享,而 trace 写入发生在请求线程/to_thread 工作线程。"""
|
|
134
|
+
self._db_path = str(path)
|
|
135
|
+
try:
|
|
136
|
+
with self._conn() as c:
|
|
137
|
+
c.execute("""CREATE TABLE IF NOT EXISTS traces(
|
|
138
|
+
trace_id TEXT PRIMARY KEY, name TEXT, start_ts REAL,
|
|
139
|
+
duration_ms REAL, span_count INTEGER, ok INTEGER, spans TEXT)""")
|
|
140
|
+
c.execute("CREATE INDEX IF NOT EXISTS idx_tr_ts ON traces(start_ts)")
|
|
141
|
+
c.execute("CREATE INDEX IF NOT EXISTS idx_tr_dur ON traces(duration_ms)")
|
|
142
|
+
except Exception as e:
|
|
143
|
+
logger.warning("trace 持久化不可用(降级为仅内存): %s", e)
|
|
144
|
+
self._db_path = None
|
|
145
|
+
|
|
146
|
+
def _conn(self):
|
|
147
|
+
if self._db_path is None:
|
|
148
|
+
raise RuntimeError("trace 持久化未启用:先 attach_db")
|
|
149
|
+
conn = sqlite3.connect(self._db_path, timeout=5.0)
|
|
150
|
+
try:
|
|
151
|
+
conn.execute("PRAGMA journal_mode=WAL")
|
|
152
|
+
except sqlite3.OperationalError:
|
|
153
|
+
pass
|
|
154
|
+
return conn
|
|
155
|
+
|
|
156
|
+
def _should_persist(self, tr: Trace) -> bool:
|
|
157
|
+
if tr.has_error():
|
|
158
|
+
return True
|
|
159
|
+
if (tr.duration_ms or 0) >= _slow_ms():
|
|
160
|
+
return True
|
|
161
|
+
return random.random() < _sample_rate()
|
|
162
|
+
|
|
163
|
+
def _persist(self, tr: Trace):
|
|
164
|
+
d = tr.to_dict()
|
|
165
|
+
try:
|
|
166
|
+
with self._conn() as c:
|
|
167
|
+
c.execute("INSERT OR REPLACE INTO traces VALUES(?,?,?,?,?,?,?)",
|
|
168
|
+
(tr.trace_id, tr.name, tr.start_ts, tr.duration_ms,
|
|
169
|
+
d["span_count"], 0 if tr.has_error() else 1,
|
|
170
|
+
json.dumps(d["spans"], ensure_ascii=False)))
|
|
171
|
+
c.execute("DELETE FROM traces WHERE trace_id IN ("
|
|
172
|
+
"SELECT trace_id FROM traces ORDER BY start_ts DESC "
|
|
173
|
+
"LIMIT -1 OFFSET ?)", (_retain(),))
|
|
174
|
+
except Exception as e:
|
|
175
|
+
logger.warning("trace 落库失败(不影响请求): %s", e)
|
|
176
|
+
|
|
177
|
+
def put(self, tr: Trace):
|
|
178
|
+
with self._lock:
|
|
179
|
+
self._d.append(tr)
|
|
180
|
+
if self._db_path and self._should_persist(tr):
|
|
181
|
+
self._persist(tr)
|
|
182
|
+
try:
|
|
183
|
+
OTLP.submit(tr)
|
|
184
|
+
except Exception:
|
|
185
|
+
pass
|
|
186
|
+
|
|
187
|
+
def get(self, trace_id: str):
|
|
188
|
+
with self._lock:
|
|
189
|
+
for tr in reversed(self._d):
|
|
190
|
+
if tr.trace_id == trace_id:
|
|
191
|
+
return tr
|
|
192
|
+
return None
|
|
193
|
+
|
|
194
|
+
def get_dict(self, trace_id: str):
|
|
195
|
+
"""先查内存环,未命中再查库(重启后仍能捞出历史慢请求)。"""
|
|
196
|
+
tr = self.get(trace_id)
|
|
197
|
+
if tr is not None:
|
|
198
|
+
return tr.to_dict()
|
|
199
|
+
if not self._db_path:
|
|
200
|
+
return None
|
|
201
|
+
try:
|
|
202
|
+
with self._conn() as c:
|
|
203
|
+
row = c.execute("SELECT trace_id,name,start_ts,duration_ms,span_count,spans "
|
|
204
|
+
"FROM traces WHERE trace_id=?", (trace_id,)).fetchone()
|
|
205
|
+
except Exception:
|
|
206
|
+
return None
|
|
207
|
+
if not row:
|
|
208
|
+
return None
|
|
209
|
+
return {"trace_id": row[0], "name": row[1], "start_ts": row[2],
|
|
210
|
+
"duration_ms": row[3], "span_count": row[4],
|
|
211
|
+
"spans": json.loads(row[5] or "[]"), "persisted": True}
|
|
212
|
+
|
|
213
|
+
def query(self, limit: int = 30, slow_ms: float | None = None, errors_only: bool = False):
|
|
214
|
+
"""查已落库的历史 trace(内存环之外)。这才是"找出昨天那次慢请求"的入口。"""
|
|
215
|
+
if not self._db_path:
|
|
216
|
+
return []
|
|
217
|
+
sql = "SELECT trace_id,name,start_ts,duration_ms,span_count,ok FROM traces"
|
|
218
|
+
cond, args = [], []
|
|
219
|
+
if slow_ms is not None:
|
|
220
|
+
cond.append("duration_ms >= ?"); args.append(float(slow_ms))
|
|
221
|
+
if errors_only:
|
|
222
|
+
cond.append("ok = 0")
|
|
223
|
+
if cond:
|
|
224
|
+
sql += " WHERE " + " AND ".join(cond)
|
|
225
|
+
sql += " ORDER BY start_ts DESC LIMIT ?"; args.append(int(limit))
|
|
226
|
+
try:
|
|
227
|
+
with self._conn() as c:
|
|
228
|
+
rows = c.execute(sql, args).fetchall()
|
|
229
|
+
except Exception:
|
|
230
|
+
return []
|
|
231
|
+
return [{"trace_id": r[0], "name": r[1], "start_ts": r[2], "duration_ms": r[3],
|
|
232
|
+
"span_count": r[4], "ok": bool(r[5]), "persisted": True} for r in rows]
|
|
233
|
+
|
|
234
|
+
def list(self, limit: int = 30) -> list:
|
|
235
|
+
with self._lock:
|
|
236
|
+
items = list(self._d)[-limit:]
|
|
237
|
+
return [t.to_dict(with_spans=False) for t in reversed(items)]
|
|
238
|
+
|
|
239
|
+
def clear(self):
|
|
240
|
+
with self._lock:
|
|
241
|
+
self._d.clear()
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _otlp_endpoint() -> str:
|
|
245
|
+
return os.environ.get("FOXCLAW_OTLP_ENDPOINT", "").strip().rstrip("/")
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _otlp_service() -> str:
|
|
249
|
+
return os.environ.get("FOXCLAW_OTLP_SERVICE", "foxclaw").strip()
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _attrs(d: dict) -> list:
|
|
253
|
+
return [{"key": str(k), "value": {"stringValue": str(v)}} for k, v in (d or {}).items()]
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _trace_to_otlp(tr: "Trace") -> dict:
|
|
257
|
+
"""把一条 Trace 转成 OTLP/JSON。根 span 代表整个请求,其余作为其子 span。"""
|
|
258
|
+
t_ns = lambda sec: str(int(sec * 1_000_000_000))
|
|
259
|
+
root_id = uuid.uuid4().hex[:16]
|
|
260
|
+
dur = (tr.duration_ms or 0.0) / 1000.0
|
|
261
|
+
spans = [{
|
|
262
|
+
"traceId": tr.trace_id, "spanId": root_id, "name": tr.name, "kind": 2,
|
|
263
|
+
"startTimeUnixNano": t_ns(tr.start_ts),
|
|
264
|
+
"endTimeUnixNano": t_ns(tr.start_ts + dur),
|
|
265
|
+
"status": {"code": 2 if tr.has_error() else 1},
|
|
266
|
+
}]
|
|
267
|
+
for s in tr.to_dict()["spans"]:
|
|
268
|
+
st = s.get("ts", tr.start_ts)
|
|
269
|
+
spans.append({
|
|
270
|
+
"traceId": tr.trace_id, "spanId": uuid.uuid4().hex[:16], "parentSpanId": root_id,
|
|
271
|
+
"name": s.get("name", "span"), "kind": 1,
|
|
272
|
+
"startTimeUnixNano": t_ns(st),
|
|
273
|
+
"endTimeUnixNano": t_ns(st + (s.get("ms", 0.0) / 1000.0)),
|
|
274
|
+
"attributes": _attrs(s.get("meta")),
|
|
275
|
+
"status": {"code": 1 if s.get("ok", True) else 2},
|
|
276
|
+
})
|
|
277
|
+
return {"resourceSpans": [{
|
|
278
|
+
"resource": {"attributes": _attrs({"service.name": _otlp_service()})},
|
|
279
|
+
"scopeSpans": [{"scope": {"name": "foxclaw.tracing"}, "spans": spans}],
|
|
280
|
+
}]}
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
class _OtlpExporter:
|
|
284
|
+
"""后台导出:有界队列 + 守护线程。队列满即丢(计数可见),永不阻塞调用方。"""
|
|
285
|
+
|
|
286
|
+
def __init__(self, maxsize: int = 512):
|
|
287
|
+
import queue
|
|
288
|
+
self._q: queue.Queue[Any] = queue.Queue(maxsize=maxsize)
|
|
289
|
+
self._th: threading.Thread | None = None
|
|
290
|
+
self._lock = threading.Lock()
|
|
291
|
+
self.dropped = 0
|
|
292
|
+
self.sent = 0
|
|
293
|
+
self.failed = 0
|
|
294
|
+
|
|
295
|
+
def _ensure_worker(self):
|
|
296
|
+
with self._lock:
|
|
297
|
+
if self._th is None or not self._th.is_alive():
|
|
298
|
+
self._th = threading.Thread(target=self._run, name="otlp-export", daemon=True)
|
|
299
|
+
self._th.start()
|
|
300
|
+
|
|
301
|
+
def submit(self, tr: "Trace"):
|
|
302
|
+
import queue
|
|
303
|
+
if not _otlp_endpoint():
|
|
304
|
+
return
|
|
305
|
+
self._ensure_worker()
|
|
306
|
+
try:
|
|
307
|
+
self._q.put_nowait(tr)
|
|
308
|
+
except queue.Full:
|
|
309
|
+
self.dropped += 1
|
|
310
|
+
|
|
311
|
+
def _run(self):
|
|
312
|
+
import urllib.request
|
|
313
|
+
while True:
|
|
314
|
+
tr = self._q.get()
|
|
315
|
+
ep = _otlp_endpoint()
|
|
316
|
+
if not ep:
|
|
317
|
+
continue
|
|
318
|
+
try:
|
|
319
|
+
body = json.dumps(_trace_to_otlp(tr)).encode("utf-8")
|
|
320
|
+
req = urllib.request.Request(
|
|
321
|
+
ep + "/v1/traces", data=body, method="POST",
|
|
322
|
+
headers={"Content-Type": "application/json"})
|
|
323
|
+
with urllib.request.urlopen(req, timeout=5) as r:
|
|
324
|
+
r.read()
|
|
325
|
+
self.sent += 1
|
|
326
|
+
except Exception as e:
|
|
327
|
+
self.failed += 1
|
|
328
|
+
if self.failed <= 3:
|
|
329
|
+
logger.warning("OTLP 导出失败(不影响请求): %s", e)
|
|
330
|
+
|
|
331
|
+
def stats(self) -> dict:
|
|
332
|
+
return {"enabled": bool(_otlp_endpoint()), "endpoint": _otlp_endpoint(),
|
|
333
|
+
"sent": self.sent, "failed": self.failed, "dropped": self.dropped,
|
|
334
|
+
"queued": self._q.qsize()}
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
OTLP = _OtlpExporter()
|
|
338
|
+
TRACES = TraceStore()
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def start_trace(name: str, trace_id: str | None = None) -> Trace:
|
|
342
|
+
"""开一条 trace 并绑到当前上下文(由 API 中间件按请求调用)。"""
|
|
343
|
+
tr = Trace(name, trace_id)
|
|
344
|
+
_current.set(tr)
|
|
345
|
+
return tr
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def current_trace():
|
|
349
|
+
return _current.get()
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def clear_trace():
|
|
353
|
+
_current.set(None)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
@contextmanager
|
|
357
|
+
def span(name: str, **meta):
|
|
358
|
+
"""记录一个 span(嵌套用 parent 串联)。不在 trace 里 → 零成本 no-op。
|
|
359
|
+
异常也会被记进 span(ok=False + error),再原样抛出(不吞异常)。"""
|
|
360
|
+
tr = _current.get()
|
|
361
|
+
if tr is None:
|
|
362
|
+
yield None
|
|
363
|
+
return
|
|
364
|
+
t0 = time.perf_counter()
|
|
365
|
+
rec = {"name": name, "ts": time.time(), "ok": True,
|
|
366
|
+
**({"meta": meta} if meta else {})}
|
|
367
|
+
try:
|
|
368
|
+
yield rec
|
|
369
|
+
except Exception as e:
|
|
370
|
+
rec["ok"] = False
|
|
371
|
+
rec["error"] = f"{type(e).__name__}: {e}"[:200]
|
|
372
|
+
raise
|
|
373
|
+
finally:
|
|
374
|
+
rec["ms"] = round((time.perf_counter() - t0) * 1000.0, 2)
|
|
375
|
+
tr.add_span(rec)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""
|
|
2
|
+
真值管线 (TruthPipeline)
|
|
3
|
+
|
|
4
|
+
借鉴 Codex 版 Foxclaw 的 TruthPipeline:用事件总线把"执行结果"自动流转为
|
|
5
|
+
"审计记录 → 记忆更新"的真值链。但接到本项目真实的 ThoughtChain 与 HMR,
|
|
6
|
+
而非 Codex 版的简化组件。
|
|
7
|
+
|
|
8
|
+
事件链:
|
|
9
|
+
execution.completed (某 Agent 执行完任务)
|
|
10
|
+
→ trace.recorded (写入 ThoughtChain 审计)
|
|
11
|
+
→ truth.appended (真值落库)
|
|
12
|
+
→ memory.updated (沉淀到 HMR 共享记忆)
|
|
13
|
+
|
|
14
|
+
好处:执行方只需 publish 一个 execution.completed 事件,后续审计/记忆流转自动发生,
|
|
15
|
+
执行逻辑与审计/记忆解耦(对齐 AHS 的可观测原则)。
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
from datetime import datetime
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TruthPipeline:
|
|
23
|
+
"""事件驱动的真值管线。订阅执行事件,自动流转到审计与记忆。
|
|
24
|
+
|
|
25
|
+
入队方式:任何持有**同一个 EventBus** 的组件,只要 publish execution.completed 即可
|
|
26
|
+
启动整条真值链,例如:
|
|
27
|
+
|
|
28
|
+
bus.publish("execution.completed",
|
|
29
|
+
{"agent_id": ..., "task": ..., "result": ..., "blocked": ...})
|
|
30
|
+
|
|
31
|
+
项目内部的实际入口是 agents/base.py 的 CognitiveAgent._emit_execution()
|
|
32
|
+
(经 EventSpine 转发到同一事件名)。本类**不再自带便捷转发方法** —— 事件名本身就是
|
|
33
|
+
契约,多留一个转发壳只会让「谁能触发真值链」存在两种说法(P3-9 死代码清理)。
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, bus, hmr=None):
|
|
37
|
+
self.bus = bus
|
|
38
|
+
self.hmr = hmr
|
|
39
|
+
self.last_trace = None
|
|
40
|
+
self.last_memory_update = None
|
|
41
|
+
|
|
42
|
+
bus.subscribe("execution.completed", self._on_execution_completed, exact=True)
|
|
43
|
+
bus.subscribe("trace.recorded", self._on_trace_recorded, exact=True)
|
|
44
|
+
bus.subscribe("truth.appended", self._on_truth_appended, exact=True)
|
|
45
|
+
|
|
46
|
+
def _on_execution_completed(self, msg):
|
|
47
|
+
"""执行完成 → 生成审计 trace,发 trace.recorded。"""
|
|
48
|
+
p = msg.payload
|
|
49
|
+
trace = {
|
|
50
|
+
"agent_id": p.get("agent_id"),
|
|
51
|
+
"task": p.get("task"),
|
|
52
|
+
"result_summary": str(p.get("result", ""))[:200],
|
|
53
|
+
"blocked": p.get("blocked", False),
|
|
54
|
+
"schedule": p.get("schedule"),
|
|
55
|
+
"recorded_at": datetime.now().isoformat(timespec="seconds"),
|
|
56
|
+
}
|
|
57
|
+
self.last_trace = trace
|
|
58
|
+
self.bus.publish("trace.recorded", {**p, "trace": trace})
|
|
59
|
+
|
|
60
|
+
def _on_trace_recorded(self, msg):
|
|
61
|
+
"""审计已记录 → 真值落库,发 truth.appended。
|
|
62
|
+
|
|
63
|
+
【P3-5】blocked=True 的任务不沉淀真值:被拦截 ≠ 真值。只留审计 trace
|
|
64
|
+
(trace.recorded 照常发——审计要记录"发生过什么"),发 truth.skipped 事件
|
|
65
|
+
供前端/审计观测"这次真值沉淀被跳过",不进入真值链。否则订阅 truth.appended
|
|
66
|
+
的下游(如 TruthCrystallizer)会把被拦截的任务当真值结晶进 HKC。
|
|
67
|
+
"""
|
|
68
|
+
p = msg.payload
|
|
69
|
+
if p.get("blocked"):
|
|
70
|
+
self.last_memory_update = {"persisted": False, "reason": "blocked"}
|
|
71
|
+
self.bus.publish("truth.skipped", {**p, "reason": "blocked"})
|
|
72
|
+
return
|
|
73
|
+
result_brief = str(p.get("result", ""))[:200]
|
|
74
|
+
truth = {
|
|
75
|
+
"fact": (f"任务「{p.get('task')}」由 {p.get('agent_id')} 处理。"
|
|
76
|
+
f"要点:{result_brief}"),
|
|
77
|
+
"trace": p.get("trace"),
|
|
78
|
+
}
|
|
79
|
+
self.bus.publish("truth.appended", {**p, "truth": truth})
|
|
80
|
+
|
|
81
|
+
def _on_truth_appended(self, msg):
|
|
82
|
+
"""真值落库 → 沉淀到 HMR 共享记忆,发 memory.updated。
|
|
83
|
+
|
|
84
|
+
【P3-0】写共享层用 agent_id="system":真值沉淀是系统级管线行为,不是某个
|
|
85
|
+
Agent 的自主写入——若按来源 Agent 归因,memory.shared.write 审计事件会把
|
|
86
|
+
系统写入误记成该 Agent 的写入(审计归因失真)。来源 Agent 保留在 tags 的
|
|
87
|
+
origin:<id> 里,便于追溯。权限侧由 SharedWritePolicy 的 facet 全局豁免
|
|
88
|
+
(tags 含 "truth_pipeline")+ 策略表 system:{"*"} 双通道保证不被误拦。
|
|
89
|
+
|
|
90
|
+
【P3-5】双保险:_on_trace_recorded 已挡 blocked,这里再检查一次。
|
|
91
|
+
"""
|
|
92
|
+
p = msg.payload
|
|
93
|
+
update: dict[str, Any] = {"persisted": False}
|
|
94
|
+
if self.hmr is not None and not p.get("blocked"):
|
|
95
|
+
try:
|
|
96
|
+
content = p.get("truth", {}).get("fact", "")
|
|
97
|
+
origin_agent = p.get("agent_id", "unknown")
|
|
98
|
+
self.hmr.ingest(
|
|
99
|
+
content,
|
|
100
|
+
type="execution",
|
|
101
|
+
agent_id="system",
|
|
102
|
+
title=f"真值_{datetime.now().strftime('%Y%m%d_%H%M%S')}",
|
|
103
|
+
tags=["truth_pipeline", f"origin:{origin_agent}"],
|
|
104
|
+
shared=True,
|
|
105
|
+
)
|
|
106
|
+
update = {"persisted": True, "content": content}
|
|
107
|
+
except Exception as e:
|
|
108
|
+
update = {"persisted": False, "error": str(e)}
|
|
109
|
+
self.last_memory_update = update
|
|
110
|
+
self.bus.publish("memory.updated", {**p, "memory_update": update})
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""可选的新版本检查(默认关)。
|
|
2
|
+
|
|
3
|
+
为什么默认关:foxclaw 的定位是本地优先 —— 默认外呼会让「断网也能用」多一个
|
|
4
|
+
例外,也给安全审计多一个出网点。**只有** 设置 ``FOXCLAW_UPDATE_CHECK=1`` 时才会
|
|
5
|
+
访问 PyPI,其余情况 ``status()`` 里 ``enabled=False``、``update_available=False``。
|
|
6
|
+
|
|
7
|
+
失败一律静默:查版本是锦上添花,不能因为它让 ``/api/system/info`` 变慢或报错。
|
|
8
|
+
- 结果按 24h 缓存到 ``<FOXCLAW_DATA_DIR>/update_check.json``;
|
|
9
|
+
- 查询在 daemon 线程里跑,不阻塞请求;
|
|
10
|
+
- 查失败时保留上次成功的结果(只是不刷新时间),避免一次网络抖动让提示消失。
|
|
11
|
+
|
|
12
|
+
只发一个 GET ``https://pypi.org/pypi/foxclaw/json``,不带任何本机信息。
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import threading
|
|
20
|
+
import time
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
_PYPI_URL = "https://pypi.org/pypi/foxclaw/json"
|
|
25
|
+
_TTL = 24 * 3600
|
|
26
|
+
_TIMEOUT = 3.0
|
|
27
|
+
|
|
28
|
+
_lock = threading.Lock()
|
|
29
|
+
_inflight = False
|
|
30
|
+
_last_error: str | None = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _enabled() -> bool:
|
|
34
|
+
return os.environ.get("FOXCLAW_UPDATE_CHECK", "").strip().lower() in {"1", "true", "yes", "on"}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _current() -> str:
|
|
38
|
+
from foxclaw import __version__
|
|
39
|
+
|
|
40
|
+
return __version__
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _data_dir() -> Path:
|
|
44
|
+
return Path(os.environ.get("FOXCLAW_DATA_DIR") or "./foxclaw_data").expanduser()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _cache_path() -> Path:
|
|
48
|
+
return _data_dir() / "update_check.json"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def parse_version(value: str) -> tuple[int, ...]:
|
|
52
|
+
"""``"0.1.10"`` → ``(0, 1, 10)``。只取各段的前导数字,预发布/本地后缀丢掉。
|
|
53
|
+
|
|
54
|
+
不引入 ``packaging``:这里只需要「谁更新」,自己比数字段足够,也少一个依赖。
|
|
55
|
+
"""
|
|
56
|
+
parts: list[int] = []
|
|
57
|
+
for chunk in str(value or "").strip().split("."):
|
|
58
|
+
digits = ""
|
|
59
|
+
for ch in chunk:
|
|
60
|
+
if not ch.isdigit():
|
|
61
|
+
break
|
|
62
|
+
digits += ch
|
|
63
|
+
if not digits:
|
|
64
|
+
break
|
|
65
|
+
parts.append(int(digits))
|
|
66
|
+
return tuple(parts)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def is_newer(latest: str, current: str) -> bool:
|
|
70
|
+
"""latest 是否严格新于 current(逐段比,缺的段补 0)。"""
|
|
71
|
+
left, right = parse_version(latest), parse_version(current)
|
|
72
|
+
width = max(len(left), len(right))
|
|
73
|
+
return left + (0,) * (width - len(left)) > right + (0,) * (width - len(right))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _read_cache() -> dict[str, Any]:
|
|
77
|
+
try:
|
|
78
|
+
data = json.loads(_cache_path().read_text(encoding="utf-8"))
|
|
79
|
+
except Exception:
|
|
80
|
+
return {}
|
|
81
|
+
return data if isinstance(data, dict) else {}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _write_cache(data: dict[str, Any]) -> None:
|
|
85
|
+
try:
|
|
86
|
+
path = _cache_path()
|
|
87
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
88
|
+
path.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
|
|
89
|
+
except Exception:
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _fetch_latest() -> str:
|
|
94
|
+
import urllib.request
|
|
95
|
+
|
|
96
|
+
req = urllib.request.Request(
|
|
97
|
+
_PYPI_URL,
|
|
98
|
+
headers={"Accept": "application/json", "User-Agent": f"foxclaw/{_current()}"},
|
|
99
|
+
)
|
|
100
|
+
with urllib.request.urlopen(req, timeout=_TIMEOUT) as resp: # noqa: S310 - 地址是常量 https
|
|
101
|
+
payload = json.loads(resp.read().decode("utf-8", "replace"))
|
|
102
|
+
version = ((payload or {}).get("info") or {}).get("version") or ""
|
|
103
|
+
if not version:
|
|
104
|
+
raise ValueError("PyPI 响应里没有 info.version")
|
|
105
|
+
return str(version)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _refresh() -> None:
|
|
109
|
+
global _inflight, _last_error
|
|
110
|
+
try:
|
|
111
|
+
latest = _fetch_latest()
|
|
112
|
+
_write_cache({"latest": latest, "checked_at": time.time(), "current": _current()})
|
|
113
|
+
_last_error = None
|
|
114
|
+
except Exception as exc:
|
|
115
|
+
_last_error = f"{type(exc).__name__}: {exc}"
|
|
116
|
+
finally:
|
|
117
|
+
with _lock:
|
|
118
|
+
_inflight = False
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def maybe_refresh() -> None:
|
|
122
|
+
"""启用且缓存过期 → 后台查一次。幂等、非阻塞,可放心在每个 system/info 里调。"""
|
|
123
|
+
global _inflight
|
|
124
|
+
if not _enabled():
|
|
125
|
+
return
|
|
126
|
+
checked = _read_cache().get("checked_at")
|
|
127
|
+
if isinstance(checked, (int, float)) and time.time() - checked < _TTL:
|
|
128
|
+
return
|
|
129
|
+
with _lock:
|
|
130
|
+
if _inflight:
|
|
131
|
+
return
|
|
132
|
+
_inflight = True
|
|
133
|
+
threading.Thread(target=_refresh, name="foxclaw-update-check", daemon=True).start()
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def status() -> dict[str, Any]:
|
|
137
|
+
"""给 ``/api/system/info`` 的只读快照。未启用时 latest 仍是缓存值,但不报可更新。"""
|
|
138
|
+
enabled = _enabled()
|
|
139
|
+
current = _current()
|
|
140
|
+
cache = _read_cache()
|
|
141
|
+
latest = str(cache.get("latest") or "")
|
|
142
|
+
return {
|
|
143
|
+
"enabled": enabled,
|
|
144
|
+
"current": current,
|
|
145
|
+
"latest": latest or None,
|
|
146
|
+
"update_available": bool(enabled and latest and is_newer(latest, current)),
|
|
147
|
+
"checked_at": cache.get("checked_at"),
|
|
148
|
+
"error": _last_error,
|
|
149
|
+
}
|