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,366 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LLM 驱动规划器(P0-1)测试 —— 全离线(用 fake router,不触发真实网络)。
|
|
3
|
+
|
|
4
|
+
覆盖:
|
|
5
|
+
1. 开关:FOXCLAW_LLM_PLAN / FOXCLAW_LLM_REPLAN 默认关 / 显式开
|
|
6
|
+
2. build_plan_tool:submit_plan 的 schema 含 agent 枚举 + flow 枚举
|
|
7
|
+
3. llm_plan 的 fail-safe:不支持 tools / 模型不产出 / args 解析失败 / 空输入 / 抛异常 → None
|
|
8
|
+
4. llm_plan 正常解析:flow + steps + 依赖 + 非法 agent 兜底 + 单步强制 single
|
|
9
|
+
5. SupervisorAgent._llm_plan:未开启/不支持/单步 → None;多步 → (Plan, step_routes)
|
|
10
|
+
6. llm_replan:正常产出修正计划 / 空 steps=放弃 / fail-safe → None
|
|
11
|
+
7. _dispatch_failed:blocked/failed 失败判定
|
|
12
|
+
8. _replan_and_redo:失败 → 反思 → 重执行 → 成功(含放弃短路)
|
|
13
|
+
9. sv_replan stage:无失败/未开启 短路保留原结果
|
|
14
|
+
"""
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
os.environ.setdefault("HF_HUB_OFFLINE", "1")
|
|
20
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
21
|
+
|
|
22
|
+
from foxclaw.planning.llm_planner import (
|
|
23
|
+
llm_plan,
|
|
24
|
+
llm_plan_enabled,
|
|
25
|
+
llm_replan,
|
|
26
|
+
replan_enabled,
|
|
27
|
+
build_plan_tool,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class _FakeRouter:
|
|
32
|
+
"""fake ModelRouter:只暴露 llm_plan 依赖的接口。"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, is_stub=False, supports=True, result=None):
|
|
35
|
+
self.is_stub = is_stub
|
|
36
|
+
self._supports = supports
|
|
37
|
+
self._result = result
|
|
38
|
+
self.last_call = None
|
|
39
|
+
|
|
40
|
+
def supports_tools(self):
|
|
41
|
+
return self._supports and not self.is_stub
|
|
42
|
+
|
|
43
|
+
def call_tools(self, system, messages, tools, tool_choice="auto",
|
|
44
|
+
max_tokens=1024, agent_id=""):
|
|
45
|
+
self.last_call = {"system": system, "messages": messages,
|
|
46
|
+
"tools": tools, "tool_choice": tool_choice}
|
|
47
|
+
return self._result
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _catalog():
|
|
51
|
+
return {"finance": "财务", "office": "办公", "admin": "行政", "assistant": "助手"}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _plan_args(flow="sequential", steps=()):
|
|
55
|
+
return {"id": "c1", "name": "submit_plan",
|
|
56
|
+
"args": {"flow": flow, "steps": list(steps)}}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_enabled_switch():
|
|
60
|
+
print("\n【1】开关默认关 / 显式开")
|
|
61
|
+
os.environ.pop("FOXCLAW_LLM_PLAN", None)
|
|
62
|
+
assert llm_plan_enabled() is False, "默认必须关(可逆/不改变现有行为)"
|
|
63
|
+
os.environ["FOXCLAW_LLM_PLAN"] = "1"
|
|
64
|
+
assert llm_plan_enabled() is True
|
|
65
|
+
os.environ["FOXCLAW_LLM_PLAN"] = "0"
|
|
66
|
+
assert llm_plan_enabled() is False
|
|
67
|
+
os.environ.pop("FOXCLAW_LLM_PLAN", None)
|
|
68
|
+
print(" ✓ 默认关 / '1'开 / '0'关")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_build_plan_tool_schema():
|
|
72
|
+
print("\n【2】submit_plan schema:agent 枚举约束")
|
|
73
|
+
tool = build_plan_tool(_catalog())
|
|
74
|
+
fn = tool["function"]
|
|
75
|
+
assert tool["type"] == "function"
|
|
76
|
+
assert fn["name"] == "submit_plan"
|
|
77
|
+
agent_enum = fn["parameters"]["properties"]["steps"]["items"]["properties"]["agent"]["enum"]
|
|
78
|
+
assert set(agent_enum) == {"finance", "office", "admin", "assistant"}, agent_enum
|
|
79
|
+
flow_enum = fn["parameters"]["properties"]["flow"]["enum"]
|
|
80
|
+
assert set(flow_enum) == {"single", "sequential", "parallel"}
|
|
81
|
+
print(" ✓ schema 含 agent 枚举 + flow 枚举")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_llm_plan_failsafe():
|
|
85
|
+
print("\n【3】fail-safe:不支持/不产出/解析失败/空输入/抛异常 → None")
|
|
86
|
+
# 不支持 tools
|
|
87
|
+
r = _FakeRouter(result={"supported": False})
|
|
88
|
+
assert llm_plan(r, "任务", _catalog()) is None
|
|
89
|
+
# 模型没调用工具(空 tool_calls)
|
|
90
|
+
r2 = _FakeRouter(result={"supported": True, "tool_calls": []})
|
|
91
|
+
assert llm_plan(r2, "任务", _catalog()) is None
|
|
92
|
+
# args 解析失败
|
|
93
|
+
r3 = _FakeRouter(result={"supported": True, "tool_calls": [
|
|
94
|
+
{"id": "c1", "name": "submit_plan", "args": {}, "args_parse_failed": True}]})
|
|
95
|
+
assert llm_plan(r3, "任务", _catalog()) is None
|
|
96
|
+
# 空 catalog / 空 task
|
|
97
|
+
assert llm_plan(r2, "任务", {}) is None
|
|
98
|
+
assert llm_plan(r2, "", _catalog()) is None
|
|
99
|
+
# call_tools 抛异常 → None(不冒泡)
|
|
100
|
+
|
|
101
|
+
class _Boom:
|
|
102
|
+
def call_tools(self, *a, **k):
|
|
103
|
+
raise RuntimeError("boom")
|
|
104
|
+
|
|
105
|
+
assert llm_plan(_Boom(), "任务", _catalog()) is None
|
|
106
|
+
print(" ✓ 6 条 fail-safe 路径全部回退 None")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_llm_plan_parse():
|
|
110
|
+
print("\n【4】正常解析:flow + steps + 依赖 + 非法 agent 兜底")
|
|
111
|
+
r = _FakeRouter(result={"supported": True, "tool_calls": [
|
|
112
|
+
_plan_args("sequential", [
|
|
113
|
+
{"name": "查支出", "agent": "finance", "depends_on": []},
|
|
114
|
+
{"name": "定预算", "agent": "finance", "depends_on": [0]},
|
|
115
|
+
{"name": "发邮件", "agent": "office", "depends_on": [1]},
|
|
116
|
+
])]})
|
|
117
|
+
out = llm_plan(r, "查支出再定预算再发邮件", _catalog())
|
|
118
|
+
assert out["flow"] == "sequential"
|
|
119
|
+
assert [s["name"] for s in out["steps"]] == ["查支出", "定预算", "发邮件"]
|
|
120
|
+
assert [s["agent"] for s in out["steps"]] == ["finance", "finance", "office"]
|
|
121
|
+
assert out["steps"][1]["depends_on"] == [0]
|
|
122
|
+
|
|
123
|
+
# 单步 → flow 强制 single
|
|
124
|
+
r2 = _FakeRouter(result={"supported": True, "tool_calls": [
|
|
125
|
+
_plan_args("parallel", [{"name": "查天气", "agent": "office"}])]})
|
|
126
|
+
out2 = llm_plan(r2, "查天气", _catalog())
|
|
127
|
+
assert out2["flow"] == "single" and len(out2["steps"]) == 1
|
|
128
|
+
|
|
129
|
+
# 非法 agent(含已下放的 assistant/admin)兜底 office
|
|
130
|
+
r3 = _FakeRouter(result={"supported": True, "tool_calls": [
|
|
131
|
+
_plan_args("single", [{"name": "x", "agent": "不存在的Agent"}])]})
|
|
132
|
+
out3 = llm_plan(r3, "x", _catalog())
|
|
133
|
+
assert out3["steps"][0]["agent"] == "office"
|
|
134
|
+
print(" ✓ 解析 / 单步single / 非法agent兜底 正确")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_supervisor_llm_plan():
|
|
138
|
+
print("\n【5】SupervisorAgent._llm_plan:开关 + 路由链路")
|
|
139
|
+
from foxclaw.agents.agents import SupervisorAgent
|
|
140
|
+
D = type("D", (), {"AGENT_NAME": "dummy"})
|
|
141
|
+
sub = {"finance": D(), "office": D()}
|
|
142
|
+
|
|
143
|
+
# 未开启 → None
|
|
144
|
+
os.environ.pop("FOXCLAW_LLM_PLAN", None)
|
|
145
|
+
a1 = SupervisorAgent.__new__(SupervisorAgent)
|
|
146
|
+
a1.router = _FakeRouter(result={"supported": True})
|
|
147
|
+
a1._subagents = sub
|
|
148
|
+
assert a1._llm_plan("任务") is None
|
|
149
|
+
|
|
150
|
+
# 开启 + 多步 → (Plan, step_routes)
|
|
151
|
+
os.environ["FOXCLAW_LLM_PLAN"] = "1"
|
|
152
|
+
a2 = SupervisorAgent.__new__(SupervisorAgent)
|
|
153
|
+
a2.router = _FakeRouter(result={"supported": True, "tool_calls": [
|
|
154
|
+
_plan_args("sequential", [
|
|
155
|
+
{"name": "查支出", "agent": "finance"},
|
|
156
|
+
{"name": "发邮件", "agent": "office"},
|
|
157
|
+
])]})
|
|
158
|
+
a2._subagents = sub
|
|
159
|
+
plan, routes = a2._llm_plan("查支出再发邮件")
|
|
160
|
+
assert plan is not None and plan.flow_type == "sequential"
|
|
161
|
+
assert len(plan.tasks) == 2
|
|
162
|
+
assert routes == [("查支出", "finance"), ("发邮件", "office")]
|
|
163
|
+
|
|
164
|
+
# 单步 → None(交回关键词路由,不改变单步行为)
|
|
165
|
+
a3 = SupervisorAgent.__new__(SupervisorAgent)
|
|
166
|
+
a3.router = _FakeRouter(result={"supported": True, "tool_calls": [
|
|
167
|
+
_plan_args("single", [{"name": "查天气", "agent": "office"}])]})
|
|
168
|
+
a3._subagents = sub
|
|
169
|
+
assert a3._llm_plan("查天气") is None
|
|
170
|
+
|
|
171
|
+
# 不支持 tools → None
|
|
172
|
+
a4 = SupervisorAgent.__new__(SupervisorAgent)
|
|
173
|
+
a4.router = _FakeRouter(is_stub=True)
|
|
174
|
+
a4._subagents = sub
|
|
175
|
+
assert a4._llm_plan("任务") is None
|
|
176
|
+
|
|
177
|
+
os.environ.pop("FOXCLAW_LLM_PLAN", None)
|
|
178
|
+
print(" ✓ 开关 / 多步链路 / 单步回退 / 不支持回退 正确")
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def test_replan_enabled_switch():
|
|
182
|
+
print("\n【6】反思重规划开关")
|
|
183
|
+
os.environ.pop("FOXCLAW_LLM_REPLAN", None)
|
|
184
|
+
assert replan_enabled() is False, "默认必须关(可逆)"
|
|
185
|
+
os.environ["FOXCLAW_LLM_REPLAN"] = "1"
|
|
186
|
+
assert replan_enabled() is True
|
|
187
|
+
os.environ.pop("FOXCLAW_LLM_REPLAN", None)
|
|
188
|
+
print(" ✓ 默认关 / '1'开")
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def test_llm_replan_parse():
|
|
192
|
+
print("\n【7】llm_replan:修正计划 + 空 steps 放弃 + fail-safe")
|
|
193
|
+
prior = [{"subtask": "查支出", "agent": "finance", "output": "超时", "blocked": True},
|
|
194
|
+
{"subtask": "发邮件", "agent": "office", "output": "未执行", "blocked": False}]
|
|
195
|
+
# 正常:换 agent 重排失败步骤
|
|
196
|
+
r = _FakeRouter(result={"supported": True, "tool_calls": [
|
|
197
|
+
_plan_args("sequential", [
|
|
198
|
+
{"name": "查支出(重试)", "agent": "finance"},
|
|
199
|
+
{"name": "发邮件", "agent": "office"},
|
|
200
|
+
])]})
|
|
201
|
+
out = llm_replan(r, "查支出再发邮件", prior, _catalog())
|
|
202
|
+
assert out is not None and [s["agent"] for s in out["steps"]] == ["finance", "office"]
|
|
203
|
+
# 空 steps = 模型判定放弃 → 返回 {"steps": []} 而非 None
|
|
204
|
+
r2 = _FakeRouter(result={"supported": True, "tool_calls": [
|
|
205
|
+
_plan_args("sequential", [])]})
|
|
206
|
+
out2 = llm_replan(r2, "任务", prior, _catalog())
|
|
207
|
+
assert out2 is not None and out2["steps"] == []
|
|
208
|
+
# fail-safe:不支持 tools → None
|
|
209
|
+
r3 = _FakeRouter(result={"supported": False})
|
|
210
|
+
assert llm_replan(r3, "任务", prior, _catalog()) is None
|
|
211
|
+
print(" ✓ 修正计划 / 空steps放弃 / 不支持回退 正确")
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def test_dispatch_failed():
|
|
215
|
+
print("\n【8】_dispatch_failed:失败判定")
|
|
216
|
+
from foxclaw.agents.agents import _dispatch_failed
|
|
217
|
+
# 多步 blocked → True
|
|
218
|
+
assert _dispatch_failed({"result": {"dispatched_to": "multi", "blocked": True}}) is True
|
|
219
|
+
# parallel steps 含 failed → True
|
|
220
|
+
assert _dispatch_failed({"result": {"dispatched_to": "parallel",
|
|
221
|
+
"parallel_steps": [{"failed": True}]}}) is True
|
|
222
|
+
# 单 Agent / 成功 / 非 dict → False
|
|
223
|
+
assert _dispatch_failed({"result": {"dispatched_to": "finance"}}) is False
|
|
224
|
+
assert _dispatch_failed({"result": {"dispatched_to": "multi", "blocked": False,
|
|
225
|
+
"multi_steps": [{"blocked": False}]}}) is False
|
|
226
|
+
assert _dispatch_failed(None) is False
|
|
227
|
+
print(" ✓ blocked/failed/单Agent/成功/None 判定正确")
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def test_replan_and_redo():
|
|
231
|
+
print("\n【9】_replan_and_redo:失败 → 反思 → 重执行 → 成功")
|
|
232
|
+
import asyncio
|
|
233
|
+
from foxclaw.agents.agents import SupervisorAgent
|
|
234
|
+
os.environ["FOXCLAW_LLM_REPLAN"] = "1"
|
|
235
|
+
D = type("D", (), {"AGENT_NAME": "dummy"})
|
|
236
|
+
sub = {"finance": D(), "office": D()}
|
|
237
|
+
|
|
238
|
+
# 反思时返回修正计划(重试失败步骤)
|
|
239
|
+
router = _FakeRouter(result={"supported": True, "tool_calls": [
|
|
240
|
+
_plan_args("sequential", [
|
|
241
|
+
{"name": "查支出(重试)", "agent": "finance"},
|
|
242
|
+
{"name": "发邮件", "agent": "office"},
|
|
243
|
+
])]})
|
|
244
|
+
a = SupervisorAgent.__new__(SupervisorAgent)
|
|
245
|
+
a.router = router
|
|
246
|
+
a._subagents = sub
|
|
247
|
+
a._think = lambda **kw: None # 屏蔽 thought_chain
|
|
248
|
+
|
|
249
|
+
calls = {"n": 0}
|
|
250
|
+
|
|
251
|
+
async def fake_dispatch_multi(task, step_routes, *a2, **kw):
|
|
252
|
+
calls["n"] += 1
|
|
253
|
+
if calls["n"] == 1:
|
|
254
|
+
return {"result": {"dispatched_to": "multi", "blocked": True,
|
|
255
|
+
"multi_steps": [{"step": 1, "subtask": "查支出",
|
|
256
|
+
"agent": "finance", "output": "超时",
|
|
257
|
+
"blocked": True}]}}
|
|
258
|
+
return {"result": {"dispatched_to": "multi", "blocked": False,
|
|
259
|
+
"multi_steps": [{"step": 1, "subtask": "查支出(重试)",
|
|
260
|
+
"agent": "finance", "output": "支出=1000",
|
|
261
|
+
"blocked": False},
|
|
262
|
+
{"step": 2, "subtask": "发邮件",
|
|
263
|
+
"agent": "assistant", "output": "已发送",
|
|
264
|
+
"blocked": False}]}}
|
|
265
|
+
a._dispatch_multi = fake_dispatch_multi
|
|
266
|
+
|
|
267
|
+
failed_dr = {"result": {"dispatched_to": "multi", "blocked": True,
|
|
268
|
+
"multi_steps": [{"step": 1, "subtask": "查支出",
|
|
269
|
+
"agent": "finance", "output": "超时",
|
|
270
|
+
"blocked": True}]}}
|
|
271
|
+
out = asyncio.run(a._replan_and_redo("查支出再发邮件", None, "sess", sub, {}, [], failed_dr))
|
|
272
|
+
assert calls["n"] == 2, f"应重执行 1 次,实际 {calls['n']}"
|
|
273
|
+
assert out["result"]["blocked"] is False
|
|
274
|
+
os.environ.pop("FOXCLAW_LLM_REPLAN", None)
|
|
275
|
+
print(" ✓ 失败触发反思 → 重执行 1 次 → 成功")
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def test_replan_gives_up():
|
|
279
|
+
print("\n【10】_replan_and_redo:模型放弃 → 不重执行,保留原结果")
|
|
280
|
+
import asyncio
|
|
281
|
+
from foxclaw.agents.agents import SupervisorAgent
|
|
282
|
+
os.environ["FOXCLAW_LLM_REPLAN"] = "1"
|
|
283
|
+
D = type("D", (), {"AGENT_NAME": "dummy"})
|
|
284
|
+
sub = {"finance": D(), "assistant": D()}
|
|
285
|
+
|
|
286
|
+
# 反思返回空 steps(放弃)
|
|
287
|
+
router = _FakeRouter(result={"supported": True, "tool_calls": [
|
|
288
|
+
_plan_args("sequential", [])]})
|
|
289
|
+
a = SupervisorAgent.__new__(SupervisorAgent)
|
|
290
|
+
a.router = router
|
|
291
|
+
a._subagents = sub
|
|
292
|
+
a._think = lambda **kw: None
|
|
293
|
+
called = {"n": 0}
|
|
294
|
+
|
|
295
|
+
async def fake_dispatch_multi(task, step_routes, *a2, **kw):
|
|
296
|
+
called["n"] += 1
|
|
297
|
+
return {"result": {"dispatched_to": "multi", "blocked": False}}
|
|
298
|
+
|
|
299
|
+
a._dispatch_multi = fake_dispatch_multi
|
|
300
|
+
failed_dr = {"result": {"dispatched_to": "multi", "blocked": True,
|
|
301
|
+
"multi_steps": [{"step": 1, "subtask": "查支出",
|
|
302
|
+
"agent": "finance", "output": "超时",
|
|
303
|
+
"blocked": True}]}}
|
|
304
|
+
out = asyncio.run(a._replan_and_redo("查支出", None, "s", sub, {}, [], failed_dr))
|
|
305
|
+
assert called["n"] == 0, "放弃时不应重执行"
|
|
306
|
+
assert out is failed_dr, "放弃时保留原结果"
|
|
307
|
+
os.environ.pop("FOXCLAW_LLM_REPLAN", None)
|
|
308
|
+
print(" ✓ 放弃短路,保留原结果")
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def test_sv_replan_stage():
|
|
312
|
+
print("\n【11】sv_replan stage:短路 + 触发")
|
|
313
|
+
import asyncio
|
|
314
|
+
from types import SimpleNamespace
|
|
315
|
+
from foxclaw.pipeline.supervisor_stages import sv_replan
|
|
316
|
+
|
|
317
|
+
# 无失败 → 短路
|
|
318
|
+
a1 = SimpleNamespace(_replan_enabled=lambda: True)
|
|
319
|
+
ctx1 = SimpleNamespace(extra={}, agent=a1,
|
|
320
|
+
dispatch_result={"result": {"dispatched_to": "multi", "blocked": False}})
|
|
321
|
+
asyncio.run(sv_replan(ctx1))
|
|
322
|
+
assert ctx1.dispatch_result["result"]["blocked"] is False
|
|
323
|
+
|
|
324
|
+
# 有失败但未开启 → 短路保留原结果
|
|
325
|
+
a2 = SimpleNamespace(_replan_enabled=lambda: False)
|
|
326
|
+
ctx2 = SimpleNamespace(extra={}, agent=a2,
|
|
327
|
+
dispatch_result={"result": {"dispatched_to": "multi", "blocked": True}})
|
|
328
|
+
asyncio.run(sv_replan(ctx2))
|
|
329
|
+
assert ctx2.dispatch_result["result"]["blocked"] is True
|
|
330
|
+
|
|
331
|
+
# 触发路径:有失败 + 开启 → 调用 _replan_and_redo 更新结果
|
|
332
|
+
async def fake_replan(task, sched, session_id, subagents, context, memories, dr, max_rounds=2):
|
|
333
|
+
return {"result": {"dispatched_to": "multi", "blocked": False}}
|
|
334
|
+
|
|
335
|
+
a3 = SimpleNamespace(_replan_enabled=lambda: True, _replan_and_redo=fake_replan)
|
|
336
|
+
ctx3 = SimpleNamespace(extra={}, agent=a3,
|
|
337
|
+
dispatch_result={"result": {"dispatched_to": "multi", "blocked": True}},
|
|
338
|
+
task="t", sched=None, session_id="s", context={}, memories=[])
|
|
339
|
+
asyncio.run(sv_replan(ctx3))
|
|
340
|
+
assert ctx3.dispatch_result["result"]["blocked"] is False
|
|
341
|
+
|
|
342
|
+
print(" ✓ 无失败/未开启短路,触发路径更新结果")
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def main():
|
|
346
|
+
print("=" * 60)
|
|
347
|
+
print("LLM 驱动规划器(P0-1)")
|
|
348
|
+
print("=" * 60)
|
|
349
|
+
test_enabled_switch()
|
|
350
|
+
test_build_plan_tool_schema()
|
|
351
|
+
test_llm_plan_failsafe()
|
|
352
|
+
test_llm_plan_parse()
|
|
353
|
+
test_supervisor_llm_plan()
|
|
354
|
+
test_replan_enabled_switch()
|
|
355
|
+
test_llm_replan_parse()
|
|
356
|
+
test_dispatch_failed()
|
|
357
|
+
test_replan_and_redo()
|
|
358
|
+
test_replan_gives_up()
|
|
359
|
+
test_sv_replan_stage()
|
|
360
|
+
print("\n" + "=" * 60)
|
|
361
|
+
print("✓ P0-1 验证完成:LLM 规划 + 反思重规划,opt-in + fail-safe,规则拆解 fallback 保留")
|
|
362
|
+
print("=" * 60)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
if __name__ == "__main__":
|
|
366
|
+
main()
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""工具中心第3期 · SKILL.md 指令技能 端到端验证。"""
|
|
2
|
+
import sys, os, tempfile
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
5
|
+
|
|
6
|
+
def test_markdown_skills():
|
|
7
|
+
os.environ["FOXCLAW_SHADOW_ROUNDS"] = "6"
|
|
8
|
+
_DATA = tempfile.mkdtemp(prefix="foxclaw_md_")
|
|
9
|
+
os.environ["FOXCLAW_DATA_DIR"] = _DATA
|
|
10
|
+
|
|
11
|
+
from fastapi.testclient import TestClient
|
|
12
|
+
from foxclaw.api import app
|
|
13
|
+
|
|
14
|
+
SKILL = "---\nname: 周报助手\ndescription: 教怎么写周报\ntriggers: 周报, 汇报\n---\n先列本周成果,再列风险,最后列下周计划。"
|
|
15
|
+
|
|
16
|
+
with TestClient(app) as c:
|
|
17
|
+
print("=" * 60); print("工具中心 · SKILL.md 指令技能 验证"); print("=" * 60)
|
|
18
|
+
|
|
19
|
+
# 1. 缺 ack → 409
|
|
20
|
+
r = c.post("/api/ahs/md-skills", json={"content": SKILL})
|
|
21
|
+
assert r.status_code == 409 and r.json()["detail"]["needs_ack"] is True
|
|
22
|
+
print("[1] 未确认 → 409 needs_ack OK")
|
|
23
|
+
|
|
24
|
+
# 2. ack → 200,列表出现
|
|
25
|
+
r = c.post("/api/ahs/md-skills", json={"content": SKILL, "acknowledged": True})
|
|
26
|
+
assert r.status_code == 200, r.text
|
|
27
|
+
sid = r.json()["skill"]["id"]
|
|
28
|
+
metas = c.get("/api/ahs/md-skills").json()["skills"]
|
|
29
|
+
assert any(s["id"] == sid for s in metas)
|
|
30
|
+
assert all("body" not in s for s in metas) # 列表不含 body
|
|
31
|
+
print("[2] 上传(ack)+列表(不含body) OK")
|
|
32
|
+
|
|
33
|
+
# 3. GET/{id} 含 body
|
|
34
|
+
r = c.get(f"/api/ahs/md-skills/{sid}")
|
|
35
|
+
assert r.status_code == 200 and "成果" in r.json()["body"]
|
|
36
|
+
print("[3] 查看含 body OK")
|
|
37
|
+
|
|
38
|
+
# 4. 缺 name → 400
|
|
39
|
+
r = c.post("/api/ahs/md-skills", json={"content": "---\ndescription: x\n---\nbody", "acknowledged": True})
|
|
40
|
+
assert r.status_code == 400
|
|
41
|
+
print("[4] 缺 name → 400 OK")
|
|
42
|
+
|
|
43
|
+
# 5. 启停
|
|
44
|
+
assert c.patch(f"/api/ahs/md-skills/{sid}", json={"enabled": False}).json()["skill"]["enabled"] is False
|
|
45
|
+
assert c.patch(f"/api/ahs/md-skills/{sid}", json={"enabled": True}).status_code == 200
|
|
46
|
+
print("[5] 启停 OK")
|
|
47
|
+
|
|
48
|
+
# 6. 注入:直建 OS,agent 挂了 md_skills,_skill_augment 命中注入 + 持久化
|
|
49
|
+
from foxclaw import Foxclaw
|
|
50
|
+
os2 = Foxclaw(data_dir=_DATA)
|
|
51
|
+
assert os2.md_skills.get(sid) is not None, "重启后指令技能丢失(持久化)"
|
|
52
|
+
ag = os2.supervisor
|
|
53
|
+
out = ag._skill_augment("帮我写周报", [{"role": "user", "content": "帮我写周报"}])
|
|
54
|
+
assert "先列本周成果" in out[-1]["content"] and "帮我写周报" in out[-1]["content"]
|
|
55
|
+
assert sum(1 for i in range(len(out) - 1) if out[i]["role"] == out[i + 1]["role"] == "user") == 0 # 无连续 user
|
|
56
|
+
out2 = ag._skill_augment("订机票", [{"role": "user", "content": "订机票"}])
|
|
57
|
+
assert out2[0]["content"] == "订机票" # 不含触发词 → 原样
|
|
58
|
+
print("[6] 注入命中/未命中 + 持久化 round-trip OK")
|
|
59
|
+
|
|
60
|
+
# 7. 删除
|
|
61
|
+
with TestClient(app) as c:
|
|
62
|
+
assert c.delete(f"/api/ahs/md-skills/{sid}").status_code == 200
|
|
63
|
+
assert not any(s["id"] == sid for s in c.get("/api/ahs/md-skills").json()["skills"])
|
|
64
|
+
print("[7] 删除 OK")
|
|
65
|
+
|
|
66
|
+
print("\nSKILL.md 指令技能验证全部通过")
|
|
67
|
+
# 注:OpenClaw 兼容段(原 [8]-[12])已拆到 test_markdown_skills_openclaw.py。
|
|
68
|
+
# 原因:单进程内 3×TestClient lifespan + 4×Foxclaw 实例 + 两个不同 ST 模型共存时,
|
|
69
|
+
# 第三次 lifespan 预热阶段 torch/FAISS 必现 0xC0000005 进程崩溃(基线对照实锤,
|
|
70
|
+
# 与业务改动无关)。进程隔离后两段各自全绿。
|
|
71
|
+
|
|
72
|
+
if __name__ == "__main__":
|
|
73
|
+
test_markdown_skills()
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""工具中心第3期 · SKILL.md 指令技能 · OpenClaw 兼容段(原 test_markdown_skills [8]-[12])。
|
|
2
|
+
|
|
3
|
+
独立成文件的原因:原文件单进程内 3×TestClient lifespan + 4×Foxclaw 实例 +
|
|
4
|
+
两个不同 ST 模型共存,第三次 lifespan 预热阶段 torch/FAISS 必现 0xC0000005
|
|
5
|
+
进程崩溃(git stash 基线对照实锤,与业务改动无关——原生库析构/重入问题)。
|
|
6
|
+
进程隔离后两段各自全绿。本段自包含:不依赖前半段的 sid,自带 _DATA。
|
|
7
|
+
"""
|
|
8
|
+
import sys, os, tempfile
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
11
|
+
|
|
12
|
+
def test_markdown_skills_openclaw():
|
|
13
|
+
os.environ["FOXCLAW_SHADOW_ROUNDS"] = "6"
|
|
14
|
+
_DATA = tempfile.mkdtemp(prefix="foxclaw_oc_")
|
|
15
|
+
os.environ["FOXCLAW_DATA_DIR"] = _DATA
|
|
16
|
+
|
|
17
|
+
from fastapi.testclient import TestClient
|
|
18
|
+
from foxclaw.api import app
|
|
19
|
+
|
|
20
|
+
# 8. OpenClaw 兼容:无 triggers 的原味技能(单 .md)→ 装成功 + 描述相关触发
|
|
21
|
+
NO_TRIG = "---\nname: skill-vetter\nversion: 1.0.0\ndescription: Security-first skill vetting. Use before installing any skill from ClawdHub or GitHub.\n---\nCheck red flags before install."
|
|
22
|
+
with TestClient(app) as c:
|
|
23
|
+
r = c.post("/api/ahs/md-skills", json={"content": NO_TRIG, "id": "skill_vetter", "acknowledged": True})
|
|
24
|
+
assert r.status_code == 200, r.text
|
|
25
|
+
assert r.json()["skill"]["version"] == "1.0.0"
|
|
26
|
+
print("[8] 无 triggers 的 OpenClaw 技能(单.md)安装 OK")
|
|
27
|
+
|
|
28
|
+
import io as _io, zipfile as _zip, json as _json, base64 as _b64
|
|
29
|
+
_buf = _io.BytesIO()
|
|
30
|
+
with _zip.ZipFile(_buf, "w") as z:
|
|
31
|
+
z.writestr("SKILL.md", NO_TRIG)
|
|
32
|
+
z.writestr("_meta.json", _json.dumps({"slug": "skill-vetter-zip", "version": "1.0.0"}))
|
|
33
|
+
_pkg = _b64.b64encode(_buf.getvalue()).decode()
|
|
34
|
+
with TestClient(app) as c:
|
|
35
|
+
# 9. zip 包上传(base64)→ id 用 slug、version 带出
|
|
36
|
+
r = c.post("/api/ahs/md-skills", json={"package_b64": _pkg, "acknowledged": True})
|
|
37
|
+
assert r.status_code == 200, r.text
|
|
38
|
+
assert r.json()["skill"]["id"] == "skill-vetter-zip" and r.json()["skill"]["version"] == "1.0.0"
|
|
39
|
+
print("[9] OpenClaw zip 包上传(slug→id,version) OK")
|
|
40
|
+
# 10. zip 无 SKILL.md → 400
|
|
41
|
+
_b2 = _io.BytesIO()
|
|
42
|
+
with _zip.ZipFile(_b2, "w") as z: z.writestr("readme.txt", "hi")
|
|
43
|
+
r = c.post("/api/ahs/md-skills", json={"package_b64": _b64.b64encode(_b2.getvalue()).decode(), "acknowledged": True})
|
|
44
|
+
assert r.status_code == 400
|
|
45
|
+
print("[10] zip 无 SKILL.md → 400 OK")
|
|
46
|
+
|
|
47
|
+
# 11. 描述相关注入(无 triggers)+ 持久化 round-trip
|
|
48
|
+
from foxclaw import Foxclaw
|
|
49
|
+
os3 = Foxclaw(data_dir=_DATA)
|
|
50
|
+
assert os3.md_skills.get("skill-vetter-zip") is not None # 持久化含 zip 装的
|
|
51
|
+
ag3 = os3.supervisor
|
|
52
|
+
hit = ag3._skill_augment("please vet this skill before install", [{"role": "user", "content": "please vet this skill before install"}])
|
|
53
|
+
assert "Check red flags" in hit[-1]["content"] # 描述相关命中注入
|
|
54
|
+
miss = ag3._skill_augment("订一张机票", [{"role": "user", "content": "订一张机票"}])
|
|
55
|
+
assert miss[0]["content"] == "订一张机票" # 无关不注入
|
|
56
|
+
print("[11] 无 triggers 描述相关注入 + 持久化 OK")
|
|
57
|
+
|
|
58
|
+
# 12. 编辑触发词(免解压重打包):PATCH triggers → 中文任务也能触发
|
|
59
|
+
with TestClient(app) as c:
|
|
60
|
+
r = c.patch("/api/ahs/md-skills/skill-vetter-zip", json={"triggers": ["审核", "搜索"]})
|
|
61
|
+
assert r.status_code == 200 and r.json()["skill"]["triggers"] == ["审核", "搜索"], r.text
|
|
62
|
+
os4 = Foxclaw(data_dir=_DATA)
|
|
63
|
+
ag4 = os4.supervisor
|
|
64
|
+
hit = ag4._skill_augment("帮我审核这个技能", [{"role": "user", "content": "帮我审核这个技能"}])
|
|
65
|
+
assert "Check red flags" in hit[-1]["content"] # 加了中文 triggers → 中文任务命中
|
|
66
|
+
print("[12] 编辑触发词 → 中文任务触发 OK")
|
|
67
|
+
|
|
68
|
+
print("\nOpenClaw 兼容验证全部通过")
|
|
69
|
+
|
|
70
|
+
if __name__ == "__main__":
|
|
71
|
+
test_markdown_skills_openclaw()
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""工具中心第2期 · MCP 连接器 端到端验证(monkeypatch async 助手,不联网)。"""
|
|
2
|
+
import sys, os, tempfile
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
5
|
+
|
|
6
|
+
def test_mcp_connectors():
|
|
7
|
+
os.environ["FOXCLAW_SHADOW_ROUNDS"] = "6"
|
|
8
|
+
os.environ["FOXCLAW_SKIP_DOTENV"] = "1" # 不加载开发机 .env(否则 FOXCLAW_API_KEY 会开启鉴权导致 401)
|
|
9
|
+
os.environ["HF_HUB_OFFLINE"] = "1" # 无网环境:跳过 huggingface 联网重试,直接用本地缓存
|
|
10
|
+
os.environ["TRANSFORMERS_OFFLINE"] = "1"
|
|
11
|
+
_DATA = tempfile.mkdtemp(prefix="foxclaw_mcp_")
|
|
12
|
+
os.environ["FOXCLAW_DATA_DIR"] = _DATA
|
|
13
|
+
|
|
14
|
+
from fastapi.testclient import TestClient
|
|
15
|
+
from foxclaw.api import app
|
|
16
|
+
import foxclaw.skills.mcp_client as mc
|
|
17
|
+
from types import SimpleNamespace as NS
|
|
18
|
+
|
|
19
|
+
# 客户端 SSRF 防线(用真实 _amcp_list_tools,monkeypatch 之前测):连内网/元数据 host
|
|
20
|
+
# 应被请求钩子在发包前拦截(防恶意 MCP server 用 307 把你重定向进内网/云元数据)。
|
|
21
|
+
import asyncio as _aio
|
|
22
|
+
_blocked = False
|
|
23
|
+
try:
|
|
24
|
+
_aio.run(mc._amcp_list_tools("http://127.0.0.1:9/mcp", {}, "streamable_http"))
|
|
25
|
+
except BaseException as _e:
|
|
26
|
+
_s = str(_e) + repr(getattr(_e, "exceptions", ""))
|
|
27
|
+
_blocked = ("内网" in _s) or ("127.0.0.1" in _s) or ("拒" in _s)
|
|
28
|
+
assert _blocked, "客户端 SSRF 钩子未拦截内网连接"
|
|
29
|
+
print("[SSRF] 客户端连内网被请求钩子拦截 OK")
|
|
30
|
+
|
|
31
|
+
FAKE_TOOLS = [
|
|
32
|
+
NS(name="get_time", description="读时间", inputSchema={}, annotations=NS(readOnlyHint=True, destructiveHint=None)),
|
|
33
|
+
NS(name="do_thing", description="干活", inputSchema={}, annotations=None),
|
|
34
|
+
]
|
|
35
|
+
async def fake_list(url, headers, transport): return list(FAKE_TOOLS)
|
|
36
|
+
async def fake_call(url, headers, transport, name, args): return {"isError": False, "content": "ok:" + name, "structured": None}
|
|
37
|
+
mc._amcp_list_tools = fake_list
|
|
38
|
+
mc._amcp_call_tool = fake_call
|
|
39
|
+
|
|
40
|
+
SRV = {"id": "demo_srv", "name": "演示", "url": "https://mcp.example.com/mcp", "acknowledged": True}
|
|
41
|
+
|
|
42
|
+
with TestClient(app) as c:
|
|
43
|
+
print("=" * 60); print("工具中心 · MCP 连接器 验证"); print("=" * 60)
|
|
44
|
+
|
|
45
|
+
# 1. 加服务器(https, ack) → 注册 2 工具
|
|
46
|
+
r = c.post("/api/ahs/mcp/servers", json=SRV)
|
|
47
|
+
assert r.status_code == 200, r.text
|
|
48
|
+
assert r.json()["tools_registered"] == 2
|
|
49
|
+
print("[1] 加服务器 + 注册 2 工具 OK")
|
|
50
|
+
|
|
51
|
+
# 2. 技能出现 + fail-closed side_effect
|
|
52
|
+
skills = {s["id"]: s for s in c.get("/api/ahs/skills").json()["skills"]}
|
|
53
|
+
assert skills["mcp__demo_srv__get_time"]["side_effect"] == "read"
|
|
54
|
+
assert skills["mcp__demo_srv__do_thing"]["side_effect"] == "write" # fail-closed
|
|
55
|
+
print("[2] 工具成技能 + fail-closed(read/write) OK")
|
|
56
|
+
|
|
57
|
+
# 3. 服务器列表 tool_count=2
|
|
58
|
+
srvs = c.get("/api/ahs/mcp/servers").json()["servers"]
|
|
59
|
+
assert srvs and srvs[0]["tool_count"] == 2
|
|
60
|
+
print("[3] 服务器列表 tool_count=2 OK")
|
|
61
|
+
|
|
62
|
+
# 4. 审核门 deny 内网
|
|
63
|
+
r = c.post("/api/ahs/mcp/servers", json={"id": "bad", "url": "http://127.0.0.1/mcp"})
|
|
64
|
+
assert r.status_code == 403, r.text
|
|
65
|
+
print("[4] 审核门 deny 内网(403) OK")
|
|
66
|
+
|
|
67
|
+
# 5. 外部低信任未 ack → 409
|
|
68
|
+
r = c.post("/api/ahs/mcp/servers", json={"id": "p2", "url": "https://mcp2.example.com/mcp"})
|
|
69
|
+
assert r.status_code == 409 and r.json()["detail"]["needs_ack"] is True
|
|
70
|
+
print("[5] 外部低信任需确认(409) OK")
|
|
71
|
+
|
|
72
|
+
# 6. 测试调用 mcp 技能:external 外部调用 → 先 needs_confirmation,确认后成功
|
|
73
|
+
r = c.post("/api/ahs/skills/mcp__demo_srv__get_time/test", json={"args": {}})
|
|
74
|
+
assert r.json()["status"] == "needs_confirmation", r.text
|
|
75
|
+
r = c.post("/api/ahs/skills/mcp__demo_srv__get_time/test", json={"args": {}, "confirmed": True})
|
|
76
|
+
j = r.json()
|
|
77
|
+
assert r.status_code == 200 and j["success"] is True, r.text
|
|
78
|
+
# 双层防提示注入:结构标记 _untrusted + 内容内联安全警告(LLM 注入时看得到)
|
|
79
|
+
out = j["output"]
|
|
80
|
+
assert out.get("_untrusted") is True, out
|
|
81
|
+
assert "不可信" in out.get("data", {}).get("content", ""), out
|
|
82
|
+
print("[6] 测试调用 mcp 技能 + 双层 untrusted 标记(防提示注入) OK")
|
|
83
|
+
|
|
84
|
+
# 7. 连接失败 → 502
|
|
85
|
+
async def boom(url, headers, transport): raise RuntimeError("connrefused")
|
|
86
|
+
mc._amcp_list_tools = boom
|
|
87
|
+
r = c.post("/api/ahs/mcp/servers", json={"id": "err_srv", "url": "https://mcp3.example.com/mcp", "acknowledged": True})
|
|
88
|
+
assert r.status_code == 502, r.text
|
|
89
|
+
mc._amcp_list_tools = fake_list
|
|
90
|
+
print("[7] 连接失败 502 OK")
|
|
91
|
+
|
|
92
|
+
# 8. 移除服务器 → 技能消失
|
|
93
|
+
assert c.delete("/api/ahs/mcp/servers/demo_srv").status_code == 200
|
|
94
|
+
assert "mcp__demo_srv__get_time" not in {s["id"] for s in c.get("/api/ahs/skills").json()["skills"]}
|
|
95
|
+
print("[8] 移除服务器 + 技能消失 OK")
|
|
96
|
+
|
|
97
|
+
# 9. 持久化 round-trip
|
|
98
|
+
with TestClient(app) as c:
|
|
99
|
+
c.post("/api/ahs/mcp/servers", json=SRV)
|
|
100
|
+
from foxclaw import Foxclaw
|
|
101
|
+
os2 = Foxclaw(data_dir=_DATA)
|
|
102
|
+
assert any(s["id"] == "demo_srv" for s in os2.mcp_manager.servers()), "server 未持久化"
|
|
103
|
+
assert os2.skill_registry.get("mcp__demo_srv__get_time") is not None, "mcp 技能未持久化"
|
|
104
|
+
print("[9] 重启后 server + mcp 技能仍在(持久化 round-trip) OK")
|
|
105
|
+
|
|
106
|
+
print("\nMCP 连接器验证全部通过")
|
|
107
|
+
|
|
108
|
+
if __name__ == "__main__":
|
|
109
|
+
test_mcp_connectors()
|