foxclaw 0.1.0 → 0.1.2
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 +11 -10
- package/package.json +46 -38
- package/python/.dockerignore +35 -0
- package/python/.env.example +165 -0
- package/python/.gitattributes +46 -0
- package/python/.github/workflows/ci.yml +122 -0
- package/python/.github/workflows/release.yml +183 -0
- package/python/Dockerfile +64 -0
- package/python/LICENSE +21 -0
- package/python/MANIFEST.in +14 -0
- package/python/README.md +192 -0
- package/python/README_CN.md +324 -0
- package/python/bench/bm25_audit.py +199 -0
- package/python/bench/bm25_regression.py +178 -0
- package/python/bench/bm25_stress.py +159 -0
- package/python/bench/cd_audit.py +238 -0
- package/python/bench/chaos.py +142 -0
- package/python/bench/concurrency.py +103 -0
- package/python/bench/e2e_real_models.py +95 -0
- package/python/bench/llm_retry_test.py +248 -0
- package/python/bench/load_test.py +196 -0
- package/python/bench/vector_ivf_upgrade.py +134 -0
- package/python/docs/CHANNELS.md +195 -0
- package/python/docs/GOVERNANCE.md +164 -0
- package/python/docs/RELEASE.md +109 -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 +152 -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 +400 -0
- package/python/examples/governance_middleware_demo.py +77 -0
- package/python/examples/langgraph_governance_demo.py +108 -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 +526 -0
- package/python/foxclaw/agents/__init__.py +12 -0
- package/python/foxclaw/agents/agents.py +35 -0
- package/python/foxclaw/agents/base.py +865 -0
- package/python/foxclaw/agents/impl/__init__.py +1 -0
- package/python/foxclaw/agents/impl/_helpers.py +78 -0
- package/python/foxclaw/agents/impl/finance.py +248 -0
- package/python/foxclaw/agents/impl/office.py +367 -0
- package/python/foxclaw/agents/impl/supervisor.py +659 -0
- package/python/foxclaw/agents/mixins/_context.py +151 -0
- package/python/foxclaw/agents/mixins/augment.py +149 -0
- package/python/foxclaw/agents/mixins/llm.py +66 -0
- package/python/foxclaw/agents/mixins/memory.py +35 -0
- package/python/foxclaw/agents/mixins/preference.py +81 -0
- package/python/foxclaw/agents/mixins/prompt.py +77 -0
- package/python/foxclaw/agents/mixins/skillbox.py +58 -0
- package/python/foxclaw/agents/mixins/tool_exec.py +443 -0
- package/python/foxclaw/api/__init__.py +283 -0
- package/python/foxclaw/api/app.py +123 -0
- package/python/foxclaw/api/middleware.py +317 -0
- package/python/foxclaw/api/routes/ahs.py +417 -0
- package/python/foxclaw/api/routes/attachments.py +117 -0
- package/python/foxclaw/api/routes/browser.py +39 -0
- package/python/foxclaw/api/routes/domain.py +314 -0
- package/python/foxclaw/api/routes/knowledge.py +163 -0
- package/python/foxclaw/api/routes/llm_admin.py +461 -0
- package/python/foxclaw/api/routes/memory.py +89 -0
- package/python/foxclaw/api/routes/orchestration.py +41 -0
- package/python/foxclaw/api/routes/recovery.py +43 -0
- package/python/foxclaw/api/routes/review.py +275 -0
- package/python/foxclaw/api/routes/search.py +37 -0
- package/python/foxclaw/api/routes/system.py +327 -0
- package/python/foxclaw/api/routes/task.py +281 -0
- package/python/foxclaw/api/routes/ws.py +68 -0
- package/python/foxclaw/api/spine.py +55 -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 +54 -0
- package/python/foxclaw/browser/action/action_types.py +45 -0
- package/python/foxclaw/browser/action/action_verifier.py +68 -0
- package/python/foxclaw/browser/action/url_policy.py +147 -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 +166 -0
- package/python/foxclaw/browser/perception/__init__.py +1 -0
- package/python/foxclaw/browser/perception/dom_extractor.py +43 -0
- package/python/foxclaw/browser/perception/element_indexer.py +57 -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 +148 -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 +67 -0
- package/python/foxclaw/channels/__init__.py +140 -0
- package/python/foxclaw/channels/discord.py +184 -0
- package/python/foxclaw/channels/dispatcher.py +204 -0
- package/python/foxclaw/channels/feishu.py +225 -0
- package/python/foxclaw/channels/openclaw_bridge.py +156 -0
- package/python/foxclaw/channels/signing.py +187 -0
- package/python/foxclaw/channels/telegram.py +121 -0
- package/python/foxclaw/channels/weixin.py +225 -0
- package/python/foxclaw/cli.py +154 -0
- package/python/foxclaw/cognitive/__init__.py +4 -0
- package/python/foxclaw/cognitive/reasoner.py +82 -0
- package/python/foxclaw/cognitive/state.py +147 -0
- package/python/foxclaw/core/__init__.py +9 -0
- package/python/foxclaw/core/audit_chain.py +133 -0
- package/python/foxclaw/core/audit_sink.py +100 -0
- package/python/foxclaw/core/compaction.py +283 -0
- package/python/foxclaw/core/config.py +65 -0
- package/python/foxclaw/core/confirm_nonce.py +157 -0
- package/python/foxclaw/core/dispatch.py +27 -0
- package/python/foxclaw/core/envfile.py +48 -0
- package/python/foxclaw/core/failure_policy.py +151 -0
- package/python/foxclaw/core/gateway.py +85 -0
- package/python/foxclaw/core/governance_audit.py +175 -0
- package/python/foxclaw/core/idempotency.py +289 -0
- package/python/foxclaw/core/injection_guard.py +221 -0
- package/python/foxclaw/core/memory_adapter.py +516 -0
- package/python/foxclaw/core/metrics.py +89 -0
- package/python/foxclaw/core/model_router.py +1097 -0
- package/python/foxclaw/core/paths.py +103 -0
- package/python/foxclaw/core/recovery.py +362 -0
- package/python/foxclaw/core/scheduler.py +144 -0
- package/python/foxclaw/core/secrets.py +134 -0
- package/python/foxclaw/core/sessions.py +157 -0
- package/python/foxclaw/core/settings.py +430 -0
- package/python/foxclaw/core/shared_write.py +198 -0
- package/python/foxclaw/core/steering_queue.py +153 -0
- package/python/foxclaw/core/system_bus.py +84 -0
- package/python/foxclaw/core/thought_chain.py +318 -0
- package/python/foxclaw/core/token_meter.py +235 -0
- package/python/foxclaw/core/tools.py +172 -0
- package/python/foxclaw/core/tracing.py +390 -0
- package/python/foxclaw/core/truth_pipeline.py +114 -0
- package/python/foxclaw/core/vec_recall_adapter.py +503 -0
- package/python/foxclaw/core/verify.py +181 -0
- package/python/foxclaw/core/web_search.py +242 -0
- package/python/foxclaw/finance/__init__.py +15 -0
- package/python/foxclaw/finance/evidence.py +153 -0
- package/python/foxclaw/finance/fx.py +100 -0
- package/python/foxclaw/finance/graph.py +188 -0
- package/python/foxclaw/finance/models.py +150 -0
- package/python/foxclaw/finance/nlparse.py +575 -0
- package/python/foxclaw/finance/policy.py +329 -0
- package/python/foxclaw/finance/reasoner.py +129 -0
- package/python/foxclaw/finance/reasoning.py +340 -0
- package/python/foxclaw/finance/reconcile.py +180 -0
- package/python/foxclaw/finance/runtime.py +527 -0
- package/python/foxclaw/governance/__init__.py +24 -0
- package/python/foxclaw/governance/middleware.py +184 -0
- package/python/foxclaw/harness/__init__.py +17 -0
- package/python/foxclaw/harness/_base.py +72 -0
- package/python/foxclaw/harness/_policy.py +359 -0
- package/python/foxclaw/harness/drift.py +89 -0
- package/python/foxclaw/harness/event_bus.py +31 -0
- package/python/foxclaw/harness/harness.py +446 -0
- package/python/foxclaw/harness/loop_detection.py +152 -0
- package/python/foxclaw/itinerary/__init__.py +10 -0
- package/python/foxclaw/itinerary/amap_client.py +117 -0
- package/python/foxclaw/itinerary/models.py +80 -0
- package/python/foxclaw/itinerary/reasoner.py +101 -0
- package/python/foxclaw/itinerary/reasoning.py +174 -0
- package/python/foxclaw/itinerary/runtime.py +148 -0
- package/python/foxclaw/kernel/__init__.py +13 -0
- package/python/foxclaw/kernel/capabilities.py +73 -0
- package/python/foxclaw/kernel/policy_gate.py +114 -0
- package/python/foxclaw/kernel/process_manager.py +205 -0
- package/python/foxclaw/kernel/scheduler.py +131 -0
- package/python/foxclaw/kernel/sentinel.py +453 -0
- package/python/foxclaw/kernel/sentinel_client.py +246 -0
- package/python/foxclaw/kernel/spine.py +97 -0
- package/python/foxclaw/knowledge/__init__.py +376 -0
- package/python/foxclaw/knowledge/truth_crystallizer.py +92 -0
- package/python/foxclaw/learning/__init__.py +12 -0
- package/python/foxclaw/learning/knowledge_graph.py +149 -0
- package/python/foxclaw/learning/models.py +93 -0
- package/python/foxclaw/learning/reasoner.py +70 -0
- package/python/foxclaw/learning/runtime.py +363 -0
- package/python/foxclaw/learning/spaced_repetition.py +96 -0
- package/python/foxclaw/mcp_server.py +189 -0
- package/python/foxclaw/orchestration/__init__.py +184 -0
- package/python/foxclaw/pipeline/__init__.py +23 -0
- package/python/foxclaw/pipeline/cognitive_pipeline.py +481 -0
- package/python/foxclaw/pipeline/supervisor_stages.py +230 -0
- package/python/foxclaw/planning/__init__.py +17 -0
- package/python/foxclaw/planning/llm_planner.py +232 -0
- package/python/foxclaw/planning/planner.py +99 -0
- package/python/foxclaw/search/__init__.py +24 -0
- package/python/foxclaw/search/agent_port.py +194 -0
- package/python/foxclaw/search/evidence.py +113 -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 +73 -0
- package/python/foxclaw/search/models.py +129 -0
- package/python/foxclaw/search/planner.py +44 -0
- package/python/foxclaw/search/retriever.py +39 -0
- package/python/foxclaw/search/runtime.py +134 -0
- package/python/foxclaw/search/strategy.py +44 -0
- package/python/foxclaw/skills/__init__.py +613 -0
- package/python/foxclaw/skills/hooks_spec.py +73 -0
- package/python/foxclaw/skills/lifecycle.py +278 -0
- package/python/foxclaw/skills/markdown_skill.py +174 -0
- package/python/foxclaw/skills/markdown_store.py +39 -0
- package/python/foxclaw/skills/mcp_client.py +190 -0
- package/python/foxclaw/skills/mcp_store.py +43 -0
- package/python/foxclaw/skills/permission.py +180 -0
- package/python/foxclaw/skills/pipeline.py +386 -0
- package/python/foxclaw/skills/policy.py +80 -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 +32 -0
- package/python/frontend/deck/chat_model_picker.js +537 -0
- package/python/frontend/deck/domain.js +296 -0
- package/python/frontend/deck/main.js +1950 -0
- package/python/frontend/deck/markdown.js +48 -0
- package/python/frontend/deck/settings.js +324 -0
- package/python/frontend/deck/state.js +6 -0
- package/python/frontend/deck/util.js +6 -0
- package/python/frontend/deck/visuals.js +212 -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 +1637 -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-BD7VPbMV.js +33 -0
- package/python/frontend/v3/assets/index-CAvnYWGH.css +1 -0
- package/python/frontend/v3/index.html +15 -0
- package/python/frontend/widget_runtime.js +295 -0
- package/python/frontend/widgets.js +696 -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 +62 -0
- package/python/frontend_src/src/api.js +89 -0
- package/python/frontend_src/src/components/ChatDock.vue +377 -0
- package/python/frontend_src/src/components/ComposerMenu.vue +75 -0
- package/python/frontend_src/src/components/DocViewer.vue +171 -0
- package/python/frontend_src/src/components/Inspector.vue +241 -0
- package/python/frontend_src/src/components/LeftRail.vue +394 -0
- package/python/frontend_src/src/components/MenuPopover.vue +130 -0
- package/python/frontend_src/src/components/MessageStream.vue +401 -0
- package/python/frontend_src/src/components/ModelPicker.vue +355 -0
- package/python/frontend_src/src/components/QuickIngest.vue +118 -0
- package/python/frontend_src/src/components/Stage.vue +226 -0
- package/python/frontend_src/src/components/ToolMenu.vue +144 -0
- package/python/frontend_src/src/components/TopBar.vue +90 -0
- package/python/frontend_src/src/components/panels/ActivityPanel.vue +84 -0
- package/python/frontend_src/src/components/panels/AgentsPanel.vue +86 -0
- package/python/frontend_src/src/components/panels/KnowledgePanel.vue +441 -0
- package/python/frontend_src/src/components/panels/MemoryPanel.vue +118 -0
- package/python/frontend_src/src/components/panels/PanelBox.vue +51 -0
- package/python/frontend_src/src/components/panels/ReviewPanel.vue +308 -0
- package/python/frontend_src/src/components/panels/SettingsPanel.vue +254 -0
- package/python/frontend_src/src/components/panels/TasksPanel.vue +74 -0
- package/python/frontend_src/src/components/panels/ToolsPanel.vue +640 -0
- package/python/frontend_src/src/components/panels/UsagePanel.vue +105 -0
- package/python/frontend_src/src/lib/fmt.js +48 -0
- package/python/frontend_src/src/lib/md.js +204 -0
- package/python/frontend_src/src/main.js +7 -0
- package/python/frontend_src/src/stores/app.js +106 -0
- package/python/frontend_src/src/stores/chat.js +484 -0
- package/python/frontend_src/src/stores/model.js +104 -0
- package/python/frontend_src/src/stores/panels.js +330 -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 +46 -0
- package/python/pyproject.toml +265 -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 +76 -0
- package/python/tests/run_all.py +263 -0
- package/python/tests/test_agent_lifecycle.py +360 -0
- package/python/tests/test_ahs_p2.py +414 -0
- package/python/tests/test_anthropic_tools.py +206 -0
- package/python/tests/test_api.py +87 -0
- package/python/tests/test_api_auth.py +83 -0
- package/python/tests/test_api_host_guard.py +69 -0
- package/python/tests/test_attachments.py +167 -0
- package/python/tests/test_audit_chain.py +121 -0
- package/python/tests/test_audit_sink.py +136 -0
- package/python/tests/test_browser.py +153 -0
- package/python/tests/test_browser_request_guard.py +184 -0
- package/python/tests/test_capabilities.py +158 -0
- package/python/tests/test_channel_signing.py +192 -0
- package/python/tests/test_channel_trust.py +109 -0
- package/python/tests/test_channels.py +184 -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 +65 -0
- package/python/tests/test_confirm_nonce.py +197 -0
- package/python/tests/test_context_compaction_wiring.py +130 -0
- package/python/tests/test_dispatch.py +95 -0
- package/python/tests/test_domain_evals.py +403 -0
- package/python/tests/test_domain_import.py +78 -0
- package/python/tests/test_domain_reasoners.py +148 -0
- package/python/tests/test_e2e.py +178 -0
- package/python/tests/test_emb_cache.py +107 -0
- package/python/tests/test_embedding_singleton.py +89 -0
- package/python/tests/test_envfile.py +83 -0
- package/python/tests/test_evals_smoke.py +114 -0
- package/python/tests/test_event_spine.py +182 -0
- package/python/tests/test_evidence_semantic.py +101 -0
- package/python/tests/test_evidence_sse.py +97 -0
- package/python/tests/test_file_ingest.py +146 -0
- package/python/tests/test_finance.py +153 -0
- package/python/tests/test_finance_ledger.py +252 -0
- package/python/tests/test_finance_persist.py +178 -0
- package/python/tests/test_gateway.py +144 -0
- package/python/tests/test_governance.py +468 -0
- package/python/tests/test_governance_audit.py +359 -0
- package/python/tests/test_governance_middleware.py +143 -0
- package/python/tests/test_hkc.py +286 -0
- package/python/tests/test_hkc_index_build_manager.py +339 -0
- package/python/tests/test_hkc_manifest_reuse.py +317 -0
- package/python/tests/test_hkc_search_event_sync.py +533 -0
- package/python/tests/test_import_path_guard.py +161 -0
- package/python/tests/test_injection_guard.py +211 -0
- package/python/tests/test_integration.py +141 -0
- package/python/tests/test_itinerary.py +103 -0
- package/python/tests/test_kee_semantic.py +89 -0
- package/python/tests/test_kernel.py +222 -0
- package/python/tests/test_learning.py +110 -0
- package/python/tests/test_llm_admin.py +160 -0
- package/python/tests/test_llm_planner.py +366 -0
- package/python/tests/test_markdown_skills.py +73 -0
- package/python/tests/test_markdown_skills_openclaw.py +71 -0
- package/python/tests/test_mcp_connectors.py +109 -0
- package/python/tests/test_mcp_guardian_discovery.py +91 -0
- package/python/tests/test_mcp_no_memory.py +43 -0
- package/python/tests/test_mcp_policy_gate.py +77 -0
- package/python/tests/test_mcp_server.py +95 -0
- package/python/tests/test_memory_recall.py +95 -0
- package/python/tests/test_metrics.py +66 -0
- package/python/tests/test_multi_chain.py +102 -0
- package/python/tests/test_multimodal.py +135 -0
- package/python/tests/test_negotiation.py +146 -0
- package/python/tests/test_orchestration.py +119 -0
- package/python/tests/test_orchestration_decoupled.py +109 -0
- package/python/tests/test_p3_p5_hardening.py +338 -0
- package/python/tests/test_parallel_dispatch.py +215 -0
- package/python/tests/test_pipeline.py +533 -0
- package/python/tests/test_plan_visible.py +86 -0
- package/python/tests/test_planner.py +95 -0
- package/python/tests/test_policy_gate.py +109 -0
- package/python/tests/test_reasoner.py +185 -0
- package/python/tests/test_release_check.py +263 -0
- package/python/tests/test_resume_checkpoint.py +156 -0
- package/python/tests/test_routing.py +95 -0
- package/python/tests/test_scheduler.py +141 -0
- package/python/tests/test_search.py +204 -0
- package/python/tests/test_secrets.py +159 -0
- package/python/tests/test_semantic.py +78 -0
- package/python/tests/test_sessions.py +91 -0
- package/python/tests/test_settings.py +281 -0
- package/python/tests/test_shadow_learning.py +330 -0
- package/python/tests/test_shared_write.py +207 -0
- package/python/tests/test_skill_taint_gate.py +154 -0
- package/python/tests/test_skills.py +133 -0
- package/python/tests/test_stats_and_events.py +165 -0
- package/python/tests/test_streaming.py +60 -0
- package/python/tests/test_supervisor_pipeline.py +177 -0
- package/python/tests/test_supervisor_triage.py +108 -0
- package/python/tests/test_taint.py +117 -0
- package/python/tests/test_task_stream.py +85 -0
- package/python/tests/test_thinking_sse.py +67 -0
- package/python/tests/test_thinking_stream.py +76 -0
- package/python/tests/test_tool_center.py +129 -0
- package/python/tests/test_tool_parallel.py +113 -0
- package/python/tests/test_tool_params.py +103 -0
- package/python/tests/test_tool_use.py +242 -0
- package/python/tests/test_toplevel_concurrency.py +117 -0
- package/python/tests/test_tracing.py +290 -0
- package/python/tests/test_truth_crystallizer.py +127 -0
- package/python/tests/test_truth_flow.py +93 -0
- package/python/tests/test_url_policy.py +53 -0
- package/python/tests/test_verify_evidence.py +225 -0
- package/python/tests/test_version_sync.py +71 -0
- package/python/tests/test_web_trigger.py +99 -0
- package/python/tools/check_faiss_type.py +31 -0
- package/python/tools/check_log.ps1 +46 -0
- package/python/tools/ivf_status.py +72 -0
- package/python/tools/mypy_baseline.txt +5 -0
- package/python/tools/mypy_ratchet.py +97 -0
- package/python/tools/pack.py +178 -0
- package/python/tools/release_check.py +349 -0
- package/python/tools/restart_foxclaw.ps1 +74 -0
- package/python/tools/shared_write_audit.py +204 -0
- package/python/tools/size_baseline.txt +28 -0
- package/python/tools/size_check.py +102 -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,390 @@
|
|
|
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
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ── 采样策略 ────────────────────────────────────────────────
|
|
37
|
+
# 内存环存只留最近 N 条、重启即失 → 查不了"昨天那次慢请求"。持久化解决它,但不能无脑全存:
|
|
38
|
+
# 正常请求的 trace 数量大、价值低。故:**慢的 / 出错的必存**(那才是你要查的),
|
|
39
|
+
# 其余按采样率(默认 0 —— 个人 OS 留下的每条都值得看)。
|
|
40
|
+
def _slow_ms() -> float:
|
|
41
|
+
return float(settings.get_safe("FOXCLAW_TRACE_SLOW_MS"))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _sample_rate() -> float:
|
|
45
|
+
return max(0.0, min(1.0, float(settings.get_safe("FOXCLAW_TRACE_SAMPLE"))))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _retain() -> int:
|
|
49
|
+
return max(100, int(settings.get_safe("FOXCLAW_TRACE_RETAIN")))
|
|
50
|
+
|
|
51
|
+
_current: contextvars.ContextVar = contextvars.ContextVar("foxclaw_trace", default=None)
|
|
52
|
+
|
|
53
|
+
MAX_SPANS_PER_TRACE = 200 # 单条 trace 的 span 上限,防病态循环把内存撑爆
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# ── W3C Trace Context(零依赖跨进程传播)────────────────────────
|
|
57
|
+
# 为什么不上 opentelemetry-sdk:Foxclaw 是单进程 + local-first,为它装 SDK/exporter/grpc、
|
|
58
|
+
# 再要求跑个 Jaeger,代价远大于收益。但它确实有一条真实跨进程边界——**MCP 双向**。
|
|
59
|
+
# traceparent 是 W3C 标准、纯字符串、零依赖,恰好覆盖这条边界:
|
|
60
|
+
# 00-{trace_id:32hex}-{span_id:16hex}-{flags:2hex}
|
|
61
|
+
_TP_RE = __import__("re").compile(r"^00-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def parse_traceparent(header: str):
|
|
65
|
+
"""解析入站 traceparent,返回 trace_id(32hex)或 None。非法/缺失一律 None(不信外部输入)。"""
|
|
66
|
+
if not header:
|
|
67
|
+
return None
|
|
68
|
+
m = _TP_RE.match(str(header).strip().lower())
|
|
69
|
+
if not m:
|
|
70
|
+
return None
|
|
71
|
+
tid = m.group(1)
|
|
72
|
+
if tid == "0" * 32: # 全零非法(规范要求)
|
|
73
|
+
return None
|
|
74
|
+
return tid
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def to_traceparent(trace_id: str, span_id: str = None) -> str:
|
|
78
|
+
"""给出站请求生成 traceparent。span_id 缺省随机(本进程作为对端的 parent)。"""
|
|
79
|
+
tid = (trace_id or uuid.uuid4().hex)[:32].rjust(32, "0")
|
|
80
|
+
sid = (span_id or uuid.uuid4().hex[:16])[:16].rjust(16, "0")
|
|
81
|
+
return f"00-{tid}-{sid}-01"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def current_traceparent():
|
|
85
|
+
"""当前上下文的 traceparent(无 trace 则 None)——出站调用带上它即可跨进程接链。"""
|
|
86
|
+
tr = _current.get()
|
|
87
|
+
return to_traceparent(tr.trace_id) if tr is not None else None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class Trace:
|
|
91
|
+
def __init__(self, name: str, trace_id: str = None):
|
|
92
|
+
# 32 hex:直接符合 W3C traceparent 的 trace-id 规格(跨进程可对齐)
|
|
93
|
+
self.trace_id = trace_id or uuid.uuid4().hex
|
|
94
|
+
self.name = name
|
|
95
|
+
self.start_ts = time.time()
|
|
96
|
+
self._t0 = time.perf_counter()
|
|
97
|
+
self.duration_ms = None
|
|
98
|
+
self.spans: list = []
|
|
99
|
+
self._lock = threading.Lock()
|
|
100
|
+
self._dropped = 0
|
|
101
|
+
|
|
102
|
+
def add_span(self, span: dict):
|
|
103
|
+
with self._lock:
|
|
104
|
+
if len(self.spans) >= MAX_SPANS_PER_TRACE:
|
|
105
|
+
self._dropped += 1
|
|
106
|
+
return
|
|
107
|
+
self.spans.append(span)
|
|
108
|
+
|
|
109
|
+
def finish(self):
|
|
110
|
+
self.duration_ms = (time.perf_counter() - self._t0) * 1000.0
|
|
111
|
+
|
|
112
|
+
def has_error(self) -> bool:
|
|
113
|
+
with self._lock:
|
|
114
|
+
return any(not s.get("ok", True) for s in self.spans)
|
|
115
|
+
|
|
116
|
+
def to_dict(self, with_spans: bool = True) -> dict:
|
|
117
|
+
with self._lock:
|
|
118
|
+
spans = list(self.spans)
|
|
119
|
+
d = {
|
|
120
|
+
"trace_id": self.trace_id,
|
|
121
|
+
"name": self.name,
|
|
122
|
+
"start_ts": self.start_ts,
|
|
123
|
+
"duration_ms": round(self.duration_ms, 2) if self.duration_ms is not None else None,
|
|
124
|
+
"span_count": len(spans),
|
|
125
|
+
"dropped_spans": self._dropped,
|
|
126
|
+
}
|
|
127
|
+
if with_spans:
|
|
128
|
+
d["spans"] = spans
|
|
129
|
+
return d
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class TraceStore:
|
|
133
|
+
"""近期用有界环存(快),值得留的落 SQLite(可查历史)。"""
|
|
134
|
+
|
|
135
|
+
def __init__(self, limit: int = 200):
|
|
136
|
+
self._d: deque = deque(maxlen=limit)
|
|
137
|
+
self._lock = threading.Lock()
|
|
138
|
+
self._db_path = None
|
|
139
|
+
|
|
140
|
+
# ── 持久化 ───────────────────────────────────────────
|
|
141
|
+
def attach_db(self, path):
|
|
142
|
+
"""开启持久化并建表。连接**即建即弃、不跨线程复用**(与 ThoughtChain 同款):
|
|
143
|
+
SQLite 连接不可跨线程共享,而 trace 写入发生在请求线程/to_thread 工作线程。"""
|
|
144
|
+
self._db_path = str(path)
|
|
145
|
+
try:
|
|
146
|
+
with self._conn() as c:
|
|
147
|
+
c.execute("""CREATE TABLE IF NOT EXISTS traces(
|
|
148
|
+
trace_id TEXT PRIMARY KEY, name TEXT, start_ts REAL,
|
|
149
|
+
duration_ms REAL, span_count INTEGER, ok INTEGER, spans TEXT)""")
|
|
150
|
+
c.execute("CREATE INDEX IF NOT EXISTS idx_tr_ts ON traces(start_ts)")
|
|
151
|
+
c.execute("CREATE INDEX IF NOT EXISTS idx_tr_dur ON traces(duration_ms)")
|
|
152
|
+
except Exception as e:
|
|
153
|
+
logger.warning("trace 持久化不可用(降级为仅内存): %s", e)
|
|
154
|
+
self._db_path = None
|
|
155
|
+
|
|
156
|
+
def _conn(self):
|
|
157
|
+
conn = sqlite3.connect(self._db_path, timeout=5.0)
|
|
158
|
+
try:
|
|
159
|
+
conn.execute("PRAGMA journal_mode=WAL") # 单写多读,降锁竞争
|
|
160
|
+
except sqlite3.OperationalError:
|
|
161
|
+
pass
|
|
162
|
+
return conn
|
|
163
|
+
|
|
164
|
+
def _should_persist(self, tr: Trace) -> bool:
|
|
165
|
+
if tr.has_error():
|
|
166
|
+
return True # 出错的必留
|
|
167
|
+
if (tr.duration_ms or 0) >= _slow_ms():
|
|
168
|
+
return True # 慢的必留(正是要查的)
|
|
169
|
+
return random.random() < _sample_rate() # 其余按采样率(默认 0)
|
|
170
|
+
|
|
171
|
+
def _persist(self, tr: Trace):
|
|
172
|
+
d = tr.to_dict()
|
|
173
|
+
try:
|
|
174
|
+
with self._conn() as c:
|
|
175
|
+
c.execute("INSERT OR REPLACE INTO traces VALUES(?,?,?,?,?,?,?)",
|
|
176
|
+
(tr.trace_id, tr.name, tr.start_ts, tr.duration_ms,
|
|
177
|
+
d["span_count"], 0 if tr.has_error() else 1,
|
|
178
|
+
json.dumps(d["spans"], ensure_ascii=False)))
|
|
179
|
+
# 保留上限:超出删最旧 → 有界增长(不会把磁盘吃光)
|
|
180
|
+
c.execute("DELETE FROM traces WHERE trace_id IN ("
|
|
181
|
+
"SELECT trace_id FROM traces ORDER BY start_ts DESC "
|
|
182
|
+
"LIMIT -1 OFFSET ?)", (_retain(),))
|
|
183
|
+
except Exception as e:
|
|
184
|
+
logger.warning("trace 落库失败(不影响请求): %s", e)
|
|
185
|
+
|
|
186
|
+
def put(self, tr: Trace):
|
|
187
|
+
with self._lock:
|
|
188
|
+
self._d.append(tr)
|
|
189
|
+
if self._db_path and self._should_persist(tr):
|
|
190
|
+
self._persist(tr) # 锁外落库(慢 I/O 不挡内存环)
|
|
191
|
+
try:
|
|
192
|
+
OTLP.submit(tr) # 可选导出:未配 endpoint 即 no-op;
|
|
193
|
+
except Exception: # 队列满则丢,绝不阻塞请求
|
|
194
|
+
pass
|
|
195
|
+
|
|
196
|
+
def get(self, trace_id: str):
|
|
197
|
+
with self._lock:
|
|
198
|
+
for tr in reversed(self._d):
|
|
199
|
+
if tr.trace_id == trace_id:
|
|
200
|
+
return tr
|
|
201
|
+
return None
|
|
202
|
+
|
|
203
|
+
def get_dict(self, trace_id: str):
|
|
204
|
+
"""先查内存环,未命中再查库(重启后仍能捞出历史慢请求)。"""
|
|
205
|
+
tr = self.get(trace_id)
|
|
206
|
+
if tr is not None:
|
|
207
|
+
return tr.to_dict()
|
|
208
|
+
if not self._db_path:
|
|
209
|
+
return None
|
|
210
|
+
try:
|
|
211
|
+
with self._conn() as c:
|
|
212
|
+
row = c.execute("SELECT trace_id,name,start_ts,duration_ms,span_count,spans "
|
|
213
|
+
"FROM traces WHERE trace_id=?", (trace_id,)).fetchone()
|
|
214
|
+
except Exception:
|
|
215
|
+
return None
|
|
216
|
+
if not row:
|
|
217
|
+
return None
|
|
218
|
+
return {"trace_id": row[0], "name": row[1], "start_ts": row[2],
|
|
219
|
+
"duration_ms": row[3], "span_count": row[4],
|
|
220
|
+
"spans": json.loads(row[5] or "[]"), "persisted": True}
|
|
221
|
+
|
|
222
|
+
def query(self, limit: int = 30, slow_ms: float = None, errors_only: bool = False):
|
|
223
|
+
"""查已落库的历史 trace(内存环之外)。这才是"找出昨天那次慢请求"的入口。"""
|
|
224
|
+
if not self._db_path:
|
|
225
|
+
return []
|
|
226
|
+
sql = "SELECT trace_id,name,start_ts,duration_ms,span_count,ok FROM traces"
|
|
227
|
+
cond, args = [], []
|
|
228
|
+
if slow_ms is not None:
|
|
229
|
+
cond.append("duration_ms >= ?"); args.append(float(slow_ms))
|
|
230
|
+
if errors_only:
|
|
231
|
+
cond.append("ok = 0")
|
|
232
|
+
if cond:
|
|
233
|
+
sql += " WHERE " + " AND ".join(cond)
|
|
234
|
+
sql += " ORDER BY start_ts DESC LIMIT ?"; args.append(int(limit))
|
|
235
|
+
try:
|
|
236
|
+
with self._conn() as c:
|
|
237
|
+
rows = c.execute(sql, args).fetchall()
|
|
238
|
+
except Exception:
|
|
239
|
+
return []
|
|
240
|
+
return [{"trace_id": r[0], "name": r[1], "start_ts": r[2], "duration_ms": r[3],
|
|
241
|
+
"span_count": r[4], "ok": bool(r[5]), "persisted": True} for r in rows]
|
|
242
|
+
|
|
243
|
+
def list(self, limit: int = 30) -> list:
|
|
244
|
+
with self._lock:
|
|
245
|
+
items = list(self._d)[-limit:]
|
|
246
|
+
return [t.to_dict(with_spans=False) for t in reversed(items)]
|
|
247
|
+
|
|
248
|
+
def clear(self):
|
|
249
|
+
with self._lock:
|
|
250
|
+
self._d.clear()
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
# ── OTLP/HTTP-JSON 导出(可选、零依赖)────────────────────────
|
|
254
|
+
# 想接 Jaeger/Tempo/collector 的人设 FOXCLAW_OTLP_ENDPOINT 即可;不设则完全不启用、零成本。
|
|
255
|
+
# 刻意**不引 opentelemetry-sdk**:OTLP 有 HTTP+JSON 传输,stdlib urllib 就能发。
|
|
256
|
+
# 铁律:遥测**绝不能拖慢请求** —— 后台守护线程 + 有界队列,满了就丢(宁丢遥测不拖请求)。
|
|
257
|
+
def _otlp_endpoint() -> str:
|
|
258
|
+
return os.environ.get("FOXCLAW_OTLP_ENDPOINT", "").strip().rstrip("/")
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _otlp_service() -> str:
|
|
262
|
+
return os.environ.get("FOXCLAW_OTLP_SERVICE", "foxclaw").strip()
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _attrs(d: dict) -> list:
|
|
266
|
+
return [{"key": str(k), "value": {"stringValue": str(v)}} for k, v in (d or {}).items()]
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _trace_to_otlp(tr: "Trace") -> dict:
|
|
270
|
+
"""把一条 Trace 转成 OTLP/JSON。根 span 代表整个请求,其余作为其子 span。"""
|
|
271
|
+
t_ns = lambda sec: str(int(sec * 1_000_000_000))
|
|
272
|
+
root_id = uuid.uuid4().hex[:16]
|
|
273
|
+
dur = (tr.duration_ms or 0.0) / 1000.0
|
|
274
|
+
spans = [{
|
|
275
|
+
"traceId": tr.trace_id, "spanId": root_id, "name": tr.name, "kind": 2, # SERVER
|
|
276
|
+
"startTimeUnixNano": t_ns(tr.start_ts),
|
|
277
|
+
"endTimeUnixNano": t_ns(tr.start_ts + dur),
|
|
278
|
+
"status": {"code": 2 if tr.has_error() else 1},
|
|
279
|
+
}]
|
|
280
|
+
for s in tr.to_dict()["spans"]:
|
|
281
|
+
st = s.get("ts", tr.start_ts) # 缺 ts(手工 add_span)→ 兜底
|
|
282
|
+
spans.append({
|
|
283
|
+
"traceId": tr.trace_id, "spanId": uuid.uuid4().hex[:16], "parentSpanId": root_id,
|
|
284
|
+
"name": s.get("name", "span"), "kind": 1, # INTERNAL
|
|
285
|
+
"startTimeUnixNano": t_ns(st),
|
|
286
|
+
"endTimeUnixNano": t_ns(st + (s.get("ms", 0.0) / 1000.0)),
|
|
287
|
+
"attributes": _attrs(s.get("meta")),
|
|
288
|
+
"status": {"code": 1 if s.get("ok", True) else 2},
|
|
289
|
+
})
|
|
290
|
+
return {"resourceSpans": [{
|
|
291
|
+
"resource": {"attributes": _attrs({"service.name": _otlp_service()})},
|
|
292
|
+
"scopeSpans": [{"scope": {"name": "foxclaw.tracing"}, "spans": spans}],
|
|
293
|
+
}]}
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
class _OtlpExporter:
|
|
297
|
+
"""后台导出:有界队列 + 守护线程。队列满即丢(计数可见),永不阻塞调用方。"""
|
|
298
|
+
|
|
299
|
+
def __init__(self, maxsize: int = 512):
|
|
300
|
+
import queue
|
|
301
|
+
self._q = queue.Queue(maxsize=maxsize)
|
|
302
|
+
self._th = None
|
|
303
|
+
self._lock = threading.Lock()
|
|
304
|
+
self.dropped = 0
|
|
305
|
+
self.sent = 0
|
|
306
|
+
self.failed = 0
|
|
307
|
+
|
|
308
|
+
def _ensure_worker(self):
|
|
309
|
+
with self._lock:
|
|
310
|
+
if self._th is None or not self._th.is_alive():
|
|
311
|
+
self._th = threading.Thread(target=self._run, name="otlp-export", daemon=True)
|
|
312
|
+
self._th.start()
|
|
313
|
+
|
|
314
|
+
def submit(self, tr: "Trace"):
|
|
315
|
+
import queue
|
|
316
|
+
if not _otlp_endpoint():
|
|
317
|
+
return
|
|
318
|
+
self._ensure_worker()
|
|
319
|
+
try:
|
|
320
|
+
self._q.put_nowait(tr)
|
|
321
|
+
except queue.Full:
|
|
322
|
+
self.dropped += 1 # 宁可丢遥测,也不让导出拖住请求线程
|
|
323
|
+
|
|
324
|
+
def _run(self):
|
|
325
|
+
import urllib.request
|
|
326
|
+
while True:
|
|
327
|
+
tr = self._q.get()
|
|
328
|
+
ep = _otlp_endpoint()
|
|
329
|
+
if not ep:
|
|
330
|
+
continue
|
|
331
|
+
try:
|
|
332
|
+
body = json.dumps(_trace_to_otlp(tr)).encode("utf-8")
|
|
333
|
+
req = urllib.request.Request(
|
|
334
|
+
ep + "/v1/traces", data=body, method="POST",
|
|
335
|
+
headers={"Content-Type": "application/json"})
|
|
336
|
+
with urllib.request.urlopen(req, timeout=5) as r:
|
|
337
|
+
r.read()
|
|
338
|
+
self.sent += 1
|
|
339
|
+
except Exception as e:
|
|
340
|
+
self.failed += 1
|
|
341
|
+
if self.failed <= 3: # 限量告警,别把日志刷爆
|
|
342
|
+
logger.warning("OTLP 导出失败(不影响请求): %s", e)
|
|
343
|
+
|
|
344
|
+
def stats(self) -> dict:
|
|
345
|
+
return {"enabled": bool(_otlp_endpoint()), "endpoint": _otlp_endpoint(),
|
|
346
|
+
"sent": self.sent, "failed": self.failed, "dropped": self.dropped,
|
|
347
|
+
"queued": self._q.qsize()}
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
OTLP = _OtlpExporter()
|
|
351
|
+
TRACES = TraceStore()
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def start_trace(name: str, trace_id: str = None) -> Trace:
|
|
355
|
+
"""开一条 trace 并绑到当前上下文(由 API 中间件按请求调用)。"""
|
|
356
|
+
tr = Trace(name, trace_id)
|
|
357
|
+
_current.set(tr)
|
|
358
|
+
return tr
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def current_trace():
|
|
362
|
+
return _current.get()
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def clear_trace():
|
|
366
|
+
_current.set(None)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
@contextmanager
|
|
370
|
+
def span(name: str, **meta):
|
|
371
|
+
"""记录一个 span(嵌套用 parent 串联)。不在 trace 里 → 零成本 no-op。
|
|
372
|
+
异常也会被记进 span(ok=False + error),再原样抛出(不吞异常)。"""
|
|
373
|
+
tr = _current.get()
|
|
374
|
+
if tr is None:
|
|
375
|
+
yield None
|
|
376
|
+
return
|
|
377
|
+
t0 = time.perf_counter()
|
|
378
|
+
# ts:span 开始的 epoch 秒。内部只需 ms 就够,但 OTLP 要求每个 span 有 start/end 绝对时刻,
|
|
379
|
+
# 故一并记下(开销可忽略;缺失时导出侧会优雅兜底)。
|
|
380
|
+
rec = {"name": name, "ts": time.time(), "ok": True,
|
|
381
|
+
**({"meta": meta} if meta else {})}
|
|
382
|
+
try:
|
|
383
|
+
yield rec
|
|
384
|
+
except Exception as e:
|
|
385
|
+
rec["ok"] = False
|
|
386
|
+
rec["error"] = f"{type(e).__name__}: {e}"[:200]
|
|
387
|
+
raise
|
|
388
|
+
finally:
|
|
389
|
+
rec["ms"] = round((time.perf_counter() - t0) * 1000.0, 2)
|
|
390
|
+
tr.add_span(rec)
|
|
@@ -0,0 +1,114 @@
|
|
|
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
|
+
|
|
20
|
+
|
|
21
|
+
class TruthPipeline:
|
|
22
|
+
"""事件驱动的真值管线。订阅执行事件,自动流转到审计与记忆。
|
|
23
|
+
|
|
24
|
+
入队方式:任何持有**同一个 EventBus** 的组件,只要 publish execution.completed 即可
|
|
25
|
+
启动整条真值链,例如:
|
|
26
|
+
|
|
27
|
+
bus.publish("execution.completed",
|
|
28
|
+
{"agent_id": ..., "task": ..., "result": ..., "blocked": ...})
|
|
29
|
+
|
|
30
|
+
项目内部的实际入口是 agents/base.py 的 CognitiveAgent._emit_execution()
|
|
31
|
+
(经 EventSpine 转发到同一事件名)。本类**不再自带便捷转发方法** —— 事件名本身就是
|
|
32
|
+
契约,多留一个转发壳只会让「谁能触发真值链」存在两种说法(P3-9 死代码清理)。
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self, bus, hmr=None):
|
|
36
|
+
self.bus = bus
|
|
37
|
+
self.hmr = hmr # 真实 HMR 记忆后端(可空,空则跳过记忆沉淀)
|
|
38
|
+
self.last_trace = None
|
|
39
|
+
self.last_memory_update = None
|
|
40
|
+
|
|
41
|
+
# 订阅事件链。【P4-1】三个都是精确 channel 名 → exact=True:
|
|
42
|
+
# 门面化后 SystemBus 默认是前缀匹配,不锁定的话 "execution.completed.extra"
|
|
43
|
+
# 之类的前缀事件也会触发真值链(语义漂移)。
|
|
44
|
+
bus.subscribe("execution.completed", self._on_execution_completed, exact=True)
|
|
45
|
+
bus.subscribe("trace.recorded", self._on_trace_recorded, exact=True)
|
|
46
|
+
bus.subscribe("truth.appended", self._on_truth_appended, exact=True)
|
|
47
|
+
|
|
48
|
+
def _on_execution_completed(self, msg):
|
|
49
|
+
"""执行完成 → 生成审计 trace,发 trace.recorded。"""
|
|
50
|
+
p = msg.payload
|
|
51
|
+
trace = {
|
|
52
|
+
"agent_id": p.get("agent_id"),
|
|
53
|
+
"task": p.get("task"),
|
|
54
|
+
"result_summary": str(p.get("result", ""))[:200],
|
|
55
|
+
"blocked": p.get("blocked", False),
|
|
56
|
+
"schedule": p.get("schedule"),
|
|
57
|
+
"recorded_at": datetime.now().isoformat(timespec="seconds"),
|
|
58
|
+
}
|
|
59
|
+
self.last_trace = trace
|
|
60
|
+
self.bus.publish("trace.recorded", {**p, "trace": trace})
|
|
61
|
+
|
|
62
|
+
def _on_trace_recorded(self, msg):
|
|
63
|
+
"""审计已记录 → 真值落库,发 truth.appended。
|
|
64
|
+
|
|
65
|
+
【P3-5】blocked=True 的任务不沉淀真值:被拦截 ≠ 真值。只留审计 trace
|
|
66
|
+
(trace.recorded 照常发——审计要记录"发生过什么"),发 truth.skipped 事件
|
|
67
|
+
供前端/审计观测"这次真值沉淀被跳过",不进入真值链。否则订阅 truth.appended
|
|
68
|
+
的下游(如 TruthCrystallizer)会把被拦截的任务当真值结晶进 HKC。
|
|
69
|
+
"""
|
|
70
|
+
p = msg.payload
|
|
71
|
+
if p.get("blocked"):
|
|
72
|
+
self.last_memory_update = {"persisted": False, "reason": "blocked"}
|
|
73
|
+
self.bus.publish("truth.skipped", {**p, "reason": "blocked"})
|
|
74
|
+
return
|
|
75
|
+
# 真值 = "任务被如此处理"的事实 + 产出要点(信息量足够供日后召回)
|
|
76
|
+
result_brief = str(p.get("result", ""))[:200]
|
|
77
|
+
truth = {
|
|
78
|
+
"fact": (f"任务「{p.get('task')}」由 {p.get('agent_id')} 处理。"
|
|
79
|
+
f"要点:{result_brief}"),
|
|
80
|
+
"trace": p.get("trace"),
|
|
81
|
+
}
|
|
82
|
+
self.bus.publish("truth.appended", {**p, "truth": truth})
|
|
83
|
+
|
|
84
|
+
def _on_truth_appended(self, msg):
|
|
85
|
+
"""真值落库 → 沉淀到 HMR 共享记忆,发 memory.updated。
|
|
86
|
+
|
|
87
|
+
【P3-0】写共享层用 agent_id="system":真值沉淀是系统级管线行为,不是某个
|
|
88
|
+
Agent 的自主写入——若按来源 Agent 归因,memory.shared.write 审计事件会把
|
|
89
|
+
系统写入误记成该 Agent 的写入(审计归因失真)。来源 Agent 保留在 tags 的
|
|
90
|
+
origin:<id> 里,便于追溯。权限侧由 SharedWritePolicy 的 facet 全局豁免
|
|
91
|
+
(tags 含 "truth_pipeline")+ 策略表 system:{"*"} 双通道保证不被误拦。
|
|
92
|
+
|
|
93
|
+
【P3-5】双保险:_on_trace_recorded 已挡 blocked,这里再检查一次。
|
|
94
|
+
"""
|
|
95
|
+
p = msg.payload
|
|
96
|
+
update = {"persisted": False}
|
|
97
|
+
if self.hmr is not None and not p.get("blocked"):
|
|
98
|
+
try:
|
|
99
|
+
# 沉淀到 HMR 共享层(所有 Agent 可见),用真实的 ingest 接口
|
|
100
|
+
content = p.get("truth", {}).get("fact", "")
|
|
101
|
+
origin_agent = p.get("agent_id", "unknown")
|
|
102
|
+
self.hmr.ingest(
|
|
103
|
+
content,
|
|
104
|
+
type="execution",
|
|
105
|
+
agent_id="system", # 【P3-0】系统级写入(非来源 Agent)
|
|
106
|
+
title=f"真值_{datetime.now().strftime('%Y%m%d_%H%M%S')}",
|
|
107
|
+
tags=["truth_pipeline", f"origin:{origin_agent}"], # 来源可追溯
|
|
108
|
+
shared=True,
|
|
109
|
+
)
|
|
110
|
+
update = {"persisted": True, "content": content}
|
|
111
|
+
except Exception as e:
|
|
112
|
+
update = {"persisted": False, "error": str(e)}
|
|
113
|
+
self.last_memory_update = update
|
|
114
|
+
self.bus.publish("memory.updated", {**p, "memory_update": update})
|