foxclaw 0.1.0 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -29
- package/bin/foxclaw.cjs +51 -0
- package/index.js +4 -10
- package/package.json +46 -38
- package/python/.dockerignore +35 -0
- package/python/.env.example +173 -0
- package/python/.gitattributes +46 -0
- package/python/.github/workflows/ci.yml +167 -0
- package/python/.github/workflows/release.yml +183 -0
- package/python/CHANGELOG.md +57 -0
- package/python/CHANGELOG_CN.md +57 -0
- package/python/Dockerfile +64 -0
- package/python/LICENSE +21 -0
- package/python/MANIFEST.in +14 -0
- package/python/README.md +201 -0
- package/python/README_CN.md +336 -0
- package/python/bench/bm25_audit.py +186 -0
- package/python/bench/bm25_regression.py +177 -0
- package/python/bench/bm25_stress.py +155 -0
- package/python/bench/cd_audit.py +215 -0
- package/python/bench/chaos.py +140 -0
- package/python/bench/concurrency.py +103 -0
- package/python/bench/e2e_real_models.py +94 -0
- package/python/bench/llm_retry_test.py +241 -0
- package/python/bench/load_test.py +193 -0
- package/python/bench/vector_ivf_upgrade.py +128 -0
- package/python/docs/CHANNELS.md +195 -0
- package/python/docs/GOVERNANCE.md +164 -0
- package/python/docs/RELEASE.md +110 -0
- package/python/docs/RUN.md +75 -0
- package/python/docs/RUN_CN.md +121 -0
- package/python/docs//351/205/215/347/275/256/346/214/207/345/215/227.md +152 -0
- package/python/evals/__init__.py +0 -0
- package/python/evals/governance/README.md +61 -0
- package/python/evals/governance/__init__.py +0 -0
- package/python/evals/governance/cases.py +115 -0
- package/python/evals/governance/compare_embeddings.py +94 -0
- package/python/evals/governance/convergence.svg +11 -0
- package/python/evals/governance/report.json +371 -0
- package/python/evals/governance/run_evals.py +368 -0
- package/python/examples/governance_middleware_demo.py +72 -0
- package/python/examples/langgraph_governance_demo.py +104 -0
- package/python/examples/sample_itinerary.csv +6 -0
- package/python/examples/sample_knowledge.csv +9 -0
- package/python/examples/sample_transactions.csv +14 -0
- package/python/foxclaw/__init__.py +408 -0
- package/python/foxclaw/agents/__init__.py +12 -0
- package/python/foxclaw/agents/agents.py +35 -0
- package/python/foxclaw/agents/base.py +794 -0
- package/python/foxclaw/agents/impl/__init__.py +1 -0
- package/python/foxclaw/agents/impl/_helpers.py +70 -0
- package/python/foxclaw/agents/impl/finance.py +231 -0
- package/python/foxclaw/agents/impl/office.py +350 -0
- package/python/foxclaw/agents/impl/supervisor.py +592 -0
- package/python/foxclaw/agents/mixins/_context.py +128 -0
- package/python/foxclaw/agents/mixins/augment.py +143 -0
- package/python/foxclaw/agents/mixins/llm.py +67 -0
- package/python/foxclaw/agents/mixins/memory.py +41 -0
- package/python/foxclaw/agents/mixins/preference.py +131 -0
- package/python/foxclaw/agents/mixins/prompt.py +71 -0
- package/python/foxclaw/agents/mixins/skillbox.py +58 -0
- package/python/foxclaw/agents/mixins/tool_exec.py +417 -0
- package/python/foxclaw/api/__init__.py +224 -0
- package/python/foxclaw/api/_state.py +43 -0
- package/python/foxclaw/api/bootstrap.py +96 -0
- package/python/foxclaw/api/middleware.py +294 -0
- package/python/foxclaw/api/routes/ahs.py +404 -0
- package/python/foxclaw/api/routes/attachments.py +116 -0
- package/python/foxclaw/api/routes/browser.py +35 -0
- package/python/foxclaw/api/routes/domain.py +306 -0
- package/python/foxclaw/api/routes/knowledge.py +145 -0
- package/python/foxclaw/api/routes/llm_admin.py +449 -0
- package/python/foxclaw/api/routes/memory.py +105 -0
- package/python/foxclaw/api/routes/orchestration.py +39 -0
- package/python/foxclaw/api/routes/recovery.py +44 -0
- package/python/foxclaw/api/routes/review.py +266 -0
- package/python/foxclaw/api/routes/search.py +33 -0
- package/python/foxclaw/api/routes/system.py +311 -0
- package/python/foxclaw/api/routes/task.py +263 -0
- package/python/foxclaw/api/routes/ws.py +64 -0
- package/python/foxclaw/api/spine.py +52 -0
- package/python/foxclaw/browser/__init__.py +13 -0
- package/python/foxclaw/browser/action/__init__.py +1 -0
- package/python/foxclaw/browser/action/action_executor.py +50 -0
- package/python/foxclaw/browser/action/action_types.py +44 -0
- package/python/foxclaw/browser/action/action_verifier.py +68 -0
- package/python/foxclaw/browser/action/url_policy.py +143 -0
- package/python/foxclaw/browser/adapter/__init__.py +1 -0
- package/python/foxclaw/browser/adapter/base_adapter.py +23 -0
- package/python/foxclaw/browser/adapter/web_adapter.py +46 -0
- package/python/foxclaw/browser/driver/__init__.py +1 -0
- package/python/foxclaw/browser/driver/base_driver.py +61 -0
- package/python/foxclaw/browser/driver/playwright_driver.py +170 -0
- package/python/foxclaw/browser/perception/__init__.py +1 -0
- package/python/foxclaw/browser/perception/dom_extractor.py +42 -0
- package/python/foxclaw/browser/perception/element_indexer.py +56 -0
- package/python/foxclaw/browser/perception/semantic_reducer.py +33 -0
- package/python/foxclaw/browser/perception/snapshot.py +29 -0
- package/python/foxclaw/browser/service.py +144 -0
- package/python/foxclaw/browser/session/__init__.py +1 -0
- package/python/foxclaw/browser/session/session_manager.py +57 -0
- package/python/foxclaw/capability/__init__.py +12 -0
- package/python/foxclaw/capability/embedding_service.py +65 -0
- package/python/foxclaw/channels/__init__.py +138 -0
- package/python/foxclaw/channels/discord.py +175 -0
- package/python/foxclaw/channels/dispatcher.py +179 -0
- package/python/foxclaw/channels/feishu.py +215 -0
- package/python/foxclaw/channels/openclaw_bridge.py +151 -0
- package/python/foxclaw/channels/signing.py +187 -0
- package/python/foxclaw/channels/telegram.py +120 -0
- package/python/foxclaw/channels/weixin.py +215 -0
- package/python/foxclaw/cli.py +183 -0
- package/python/foxclaw/cognitive/__init__.py +4 -0
- package/python/foxclaw/cognitive/reasoner.py +81 -0
- package/python/foxclaw/cognitive/state.py +141 -0
- package/python/foxclaw/core/__init__.py +9 -0
- package/python/foxclaw/core/audit_chain.py +131 -0
- package/python/foxclaw/core/audit_sink.py +100 -0
- package/python/foxclaw/core/compaction.py +265 -0
- package/python/foxclaw/core/config.py +65 -0
- package/python/foxclaw/core/confirm_nonce.py +151 -0
- package/python/foxclaw/core/dispatch.py +27 -0
- package/python/foxclaw/core/envfile.py +48 -0
- package/python/foxclaw/core/failure_policy.py +142 -0
- package/python/foxclaw/core/gateway.py +81 -0
- package/python/foxclaw/core/governance_audit.py +171 -0
- package/python/foxclaw/core/idempotency.py +282 -0
- package/python/foxclaw/core/injection_guard.py +202 -0
- package/python/foxclaw/core/memory_adapter.py +489 -0
- package/python/foxclaw/core/metrics.py +88 -0
- package/python/foxclaw/core/model_router.py +985 -0
- package/python/foxclaw/core/paths.py +103 -0
- package/python/foxclaw/core/recovery.py +349 -0
- package/python/foxclaw/core/scheduler.py +141 -0
- package/python/foxclaw/core/secrets.py +131 -0
- package/python/foxclaw/core/sessions.py +155 -0
- package/python/foxclaw/core/settings.py +414 -0
- package/python/foxclaw/core/shared_write.py +186 -0
- package/python/foxclaw/core/steering_queue.py +154 -0
- package/python/foxclaw/core/system_bus.py +84 -0
- package/python/foxclaw/core/thought_chain.py +316 -0
- package/python/foxclaw/core/token_meter.py +231 -0
- package/python/foxclaw/core/tools.py +164 -0
- package/python/foxclaw/core/tracing.py +375 -0
- package/python/foxclaw/core/truth_pipeline.py +110 -0
- package/python/foxclaw/core/update_check.py +149 -0
- package/python/foxclaw/core/vec_recall_adapter.py +477 -0
- package/python/foxclaw/core/verify.py +173 -0
- package/python/foxclaw/core/web_search.py +218 -0
- package/python/foxclaw/finance/__init__.py +15 -0
- package/python/foxclaw/finance/evidence.py +149 -0
- package/python/foxclaw/finance/fx.py +98 -0
- package/python/foxclaw/finance/graph.py +179 -0
- package/python/foxclaw/finance/models.py +146 -0
- package/python/foxclaw/finance/nlparse.py +515 -0
- package/python/foxclaw/finance/policy.py +301 -0
- package/python/foxclaw/finance/reasoner.py +121 -0
- package/python/foxclaw/finance/reasoning.py +319 -0
- package/python/foxclaw/finance/reconcile.py +175 -0
- package/python/foxclaw/finance/runtime.py +500 -0
- package/python/foxclaw/governance/__init__.py +24 -0
- package/python/foxclaw/governance/middleware.py +176 -0
- package/python/foxclaw/harness/__init__.py +17 -0
- package/python/foxclaw/harness/_base.py +66 -0
- package/python/foxclaw/harness/_policy.py +340 -0
- package/python/foxclaw/harness/drift.py +77 -0
- package/python/foxclaw/harness/event_bus.py +28 -0
- package/python/foxclaw/harness/harness.py +358 -0
- package/python/foxclaw/harness/loop_detection.py +145 -0
- package/python/foxclaw/itinerary/__init__.py +10 -0
- package/python/foxclaw/itinerary/amap_client.py +114 -0
- package/python/foxclaw/itinerary/models.py +80 -0
- package/python/foxclaw/itinerary/reasoner.py +96 -0
- package/python/foxclaw/itinerary/reasoning.py +168 -0
- package/python/foxclaw/itinerary/runtime.py +144 -0
- package/python/foxclaw/kernel/__init__.py +13 -0
- package/python/foxclaw/kernel/capabilities.py +72 -0
- package/python/foxclaw/kernel/policy_gate.py +108 -0
- package/python/foxclaw/kernel/process_manager.py +186 -0
- package/python/foxclaw/kernel/scheduler.py +125 -0
- package/python/foxclaw/kernel/sentinel.py +443 -0
- package/python/foxclaw/kernel/sentinel_client.py +243 -0
- package/python/foxclaw/kernel/spine.py +97 -0
- package/python/foxclaw/knowledge/__init__.py +360 -0
- package/python/foxclaw/knowledge/truth_crystallizer.py +89 -0
- package/python/foxclaw/learning/__init__.py +12 -0
- package/python/foxclaw/learning/knowledge_graph.py +144 -0
- package/python/foxclaw/learning/models.py +93 -0
- package/python/foxclaw/learning/reasoner.py +67 -0
- package/python/foxclaw/learning/runtime.py +351 -0
- package/python/foxclaw/learning/spaced_repetition.py +91 -0
- package/python/foxclaw/mcp_server.py +188 -0
- package/python/foxclaw/orchestration/__init__.py +167 -0
- package/python/foxclaw/pipeline/__init__.py +23 -0
- package/python/foxclaw/pipeline/cognitive_pipeline.py +429 -0
- package/python/foxclaw/pipeline/stage_meta.py +21 -0
- package/python/foxclaw/pipeline/supervisor_stages.py +201 -0
- package/python/foxclaw/planning/__init__.py +17 -0
- package/python/foxclaw/planning/llm_planner.py +232 -0
- package/python/foxclaw/planning/planner.py +94 -0
- package/python/foxclaw/search/__init__.py +24 -0
- package/python/foxclaw/search/agent_port.py +190 -0
- package/python/foxclaw/search/evidence.py +109 -0
- package/python/foxclaw/search/external_search.py +50 -0
- package/python/foxclaw/search/hkc_read_port.py +60 -0
- package/python/foxclaw/search/internal_search.py +71 -0
- package/python/foxclaw/search/models.py +127 -0
- package/python/foxclaw/search/planner.py +44 -0
- package/python/foxclaw/search/retriever.py +39 -0
- package/python/foxclaw/search/runtime.py +127 -0
- package/python/foxclaw/search/strategy.py +44 -0
- package/python/foxclaw/skills/__init__.py +559 -0
- package/python/foxclaw/skills/archive.py +111 -0
- package/python/foxclaw/skills/fetch.py +226 -0
- package/python/foxclaw/skills/hooks_spec.py +69 -0
- package/python/foxclaw/skills/install_cmd.py +216 -0
- package/python/foxclaw/skills/lifecycle.py +252 -0
- package/python/foxclaw/skills/markdown_skill.py +175 -0
- package/python/foxclaw/skills/markdown_store.py +39 -0
- package/python/foxclaw/skills/mcp_client.py +184 -0
- package/python/foxclaw/skills/mcp_store.py +40 -0
- package/python/foxclaw/skills/permission.py +145 -0
- package/python/foxclaw/skills/pipeline.py +354 -0
- package/python/foxclaw/skills/policy.py +74 -0
- package/python/foxclaw/skills/review.py +72 -0
- package/python/foxclaw/skills/store.py +57 -0
- package/python/frontend/assets/core_bg.jpg +0 -0
- package/python/frontend/deck/auth.js +25 -0
- package/python/frontend/deck/chat_model_picker.js +483 -0
- package/python/frontend/deck/domain.js +276 -0
- package/python/frontend/deck/main.js +1806 -0
- package/python/frontend/deck/markdown.js +44 -0
- package/python/frontend/deck/settings.js +307 -0
- package/python/frontend/deck/state.js +5 -0
- package/python/frontend/deck/util.js +4 -0
- package/python/frontend/deck/visuals.js +190 -0
- package/python/frontend/deck.css +855 -0
- package/python/frontend/deck.html +686 -0
- package/python/frontend/deck_v2.html +341 -0
- package/python/frontend/planet/index.html +266 -0
- package/python/frontend/planet/planet.css +579 -0
- package/python/frontend/planet/planet.js +1436 -0
- package/python/frontend/planet/vendor/three.core.min.js +6 -0
- package/python/frontend/planet/vendor/three.module.min.js +6 -0
- package/python/frontend/v3/assets/index-CQSBdxk4.js +34 -0
- package/python/frontend/v3/assets/index-D5nIsjms.css +1 -0
- package/python/frontend/v3/index.html +15 -0
- package/python/frontend/widget_runtime.js +252 -0
- package/python/frontend/widgets.js +639 -0
- package/python/frontend_src/index.html +14 -0
- package/python/frontend_src/package-lock.json +1376 -0
- package/python/frontend_src/package.json +19 -0
- package/python/frontend_src/src/App.vue +58 -0
- package/python/frontend_src/src/api.js +82 -0
- package/python/frontend_src/src/components/ChatDock.vue +365 -0
- package/python/frontend_src/src/components/ComposerMenu.vue +75 -0
- package/python/frontend_src/src/components/DocViewer.vue +162 -0
- package/python/frontend_src/src/components/Inspector.vue +235 -0
- package/python/frontend_src/src/components/LeftRail.vue +415 -0
- package/python/frontend_src/src/components/MenuPopover.vue +128 -0
- package/python/frontend_src/src/components/MessageStream.vue +389 -0
- package/python/frontend_src/src/components/ModelPicker.vue +350 -0
- package/python/frontend_src/src/components/QuickIngest.vue +112 -0
- package/python/frontend_src/src/components/Stage.vue +272 -0
- package/python/frontend_src/src/components/ToolMenu.vue +141 -0
- package/python/frontend_src/src/components/TopBar.vue +118 -0
- package/python/frontend_src/src/components/panels/ActivityPanel.vue +82 -0
- package/python/frontend_src/src/components/panels/AgentsPanel.vue +83 -0
- package/python/frontend_src/src/components/panels/KnowledgePanel.vue +425 -0
- package/python/frontend_src/src/components/panels/MemoryPanel.vue +117 -0
- package/python/frontend_src/src/components/panels/PanelBox.vue +50 -0
- package/python/frontend_src/src/components/panels/ReviewPanel.vue +292 -0
- package/python/frontend_src/src/components/panels/SettingsPanel.vue +358 -0
- package/python/frontend_src/src/components/panels/TasksPanel.vue +73 -0
- package/python/frontend_src/src/components/panels/ToolsPanel.vue +746 -0
- package/python/frontend_src/src/components/panels/UsagePanel.vue +105 -0
- package/python/frontend_src/src/lib/fmt.js +44 -0
- package/python/frontend_src/src/lib/i18n.js +53 -0
- package/python/frontend_src/src/lib/locales/en.js +173 -0
- package/python/frontend_src/src/lib/locales/zh.js +174 -0
- package/python/frontend_src/src/lib/md.js +190 -0
- package/python/frontend_src/src/main.js +9 -0
- package/python/frontend_src/src/stores/app.js +91 -0
- package/python/frontend_src/src/stores/chat.js +450 -0
- package/python/frontend_src/src/stores/model.js +96 -0
- package/python/frontend_src/src/stores/panels.js +289 -0
- package/python/frontend_src/src/stores/profile.js +31 -0
- package/python/frontend_src/src/stores/system.js +25 -0
- package/python/frontend_src/src/styles/base.css +138 -0
- package/python/frontend_src/src/styles/panel.css +117 -0
- package/python/frontend_src/src/styles/tokens.css +78 -0
- package/python/frontend_src/vite.config.js +39 -0
- package/python/pyproject.toml +268 -0
- package/python/requirements.txt +311 -0
- package/python/skills/README.md +123 -0
- package/python/skills/execution-contract/SKILL.md +30 -0
- package/python/skills/general-assistant/SKILL.md +34 -0
- package/python/skills/planner/SKILL.md +33 -0
- package/python/skills/schedule-itinerary/SKILL.md +35 -0
- package/python/skills/skill-composition/SKILL.md +31 -0
- package/python/skills/skill-guard/SKILL.md +67 -0
- package/python/skills/skill-memory/SKILL.md +50 -0
- package/python/skills/skill-router/SKILL.md +35 -0
- package/python/tests/conftest.py +54 -0
- package/python/tests/run_all.py +200 -0
- package/python/tests/test_agent_lifecycle.py +326 -0
- package/python/tests/test_ahs_p2.py +393 -0
- package/python/tests/test_anthropic_tools.py +192 -0
- package/python/tests/test_api.py +97 -0
- package/python/tests/test_api_auth.py +79 -0
- package/python/tests/test_api_host_guard.py +69 -0
- package/python/tests/test_attachments.py +151 -0
- package/python/tests/test_audit_chain.py +116 -0
- package/python/tests/test_audit_sink.py +130 -0
- package/python/tests/test_browser.py +149 -0
- package/python/tests/test_browser_request_guard.py +180 -0
- package/python/tests/test_capabilities.py +152 -0
- package/python/tests/test_channel_signing.py +185 -0
- package/python/tests/test_channel_trust.py +109 -0
- package/python/tests/test_channels.py +174 -0
- package/python/tests/test_cli.py +46 -0
- package/python/tests/test_compaction_toolpair.py +119 -0
- package/python/tests/test_config_layering.py +61 -0
- package/python/tests/test_confirm_nonce.py +186 -0
- package/python/tests/test_context_compaction_wiring.py +124 -0
- package/python/tests/test_dispatch.py +89 -0
- package/python/tests/test_domain_evals.py +334 -0
- package/python/tests/test_domain_import.py +72 -0
- package/python/tests/test_domain_reasoners.py +134 -0
- package/python/tests/test_e2e.py +161 -0
- package/python/tests/test_emb_cache.py +97 -0
- package/python/tests/test_embedding_singleton.py +87 -0
- package/python/tests/test_envfile.py +83 -0
- package/python/tests/test_evals_smoke.py +90 -0
- package/python/tests/test_event_spine.py +174 -0
- package/python/tests/test_evidence_semantic.py +88 -0
- package/python/tests/test_evidence_sse.py +94 -0
- package/python/tests/test_file_ingest.py +135 -0
- package/python/tests/test_finance.py +144 -0
- package/python/tests/test_finance_ledger.py +245 -0
- package/python/tests/test_finance_persist.py +174 -0
- package/python/tests/test_gateway.py +125 -0
- package/python/tests/test_governance.py +421 -0
- package/python/tests/test_governance_audit.py +324 -0
- package/python/tests/test_governance_middleware.py +137 -0
- package/python/tests/test_hkc.py +268 -0
- package/python/tests/test_hkc_index_build_manager.py +306 -0
- package/python/tests/test_hkc_manifest_reuse.py +294 -0
- package/python/tests/test_hkc_search_event_sync.py +482 -0
- package/python/tests/test_import_path_guard.py +155 -0
- package/python/tests/test_injection_guard.py +193 -0
- package/python/tests/test_integration.py +117 -0
- package/python/tests/test_itinerary.py +100 -0
- package/python/tests/test_kee_semantic.py +84 -0
- package/python/tests/test_kernel.py +212 -0
- package/python/tests/test_learning.py +101 -0
- package/python/tests/test_llm_admin.py +145 -0
- package/python/tests/test_llm_planner.py +344 -0
- package/python/tests/test_markdown_skills.py +62 -0
- package/python/tests/test_markdown_skills_openclaw.py +66 -0
- package/python/tests/test_mcp_connectors.py +97 -0
- package/python/tests/test_mcp_guardian_discovery.py +87 -0
- package/python/tests/test_mcp_no_memory.py +43 -0
- package/python/tests/test_mcp_policy_gate.py +76 -0
- package/python/tests/test_mcp_server.py +92 -0
- package/python/tests/test_memory_recall.py +92 -0
- package/python/tests/test_metrics.py +58 -0
- package/python/tests/test_multi_chain.py +90 -0
- package/python/tests/test_multimodal.py +122 -0
- package/python/tests/test_negotiation.py +136 -0
- package/python/tests/test_orchestration.py +109 -0
- package/python/tests/test_orchestration_decoupled.py +103 -0
- package/python/tests/test_p3_p5_hardening.py +324 -0
- package/python/tests/test_parallel_dispatch.py +207 -0
- package/python/tests/test_pipeline.py +492 -0
- package/python/tests/test_plan_visible.py +86 -0
- package/python/tests/test_planner.py +88 -0
- package/python/tests/test_policy_gate.py +108 -0
- package/python/tests/test_profile.py +67 -0
- package/python/tests/test_readme_claims.py +163 -0
- package/python/tests/test_reasoner.py +170 -0
- package/python/tests/test_release_check.py +252 -0
- package/python/tests/test_resume_checkpoint.py +148 -0
- package/python/tests/test_routing.py +85 -0
- package/python/tests/test_scheduler.py +138 -0
- package/python/tests/test_search.py +194 -0
- package/python/tests/test_secrets.py +153 -0
- package/python/tests/test_semantic.py +69 -0
- package/python/tests/test_sessions.py +90 -0
- package/python/tests/test_settings.py +265 -0
- package/python/tests/test_shadow_learning.py +290 -0
- package/python/tests/test_shared_write.py +200 -0
- package/python/tests/test_skill_taint_gate.py +145 -0
- package/python/tests/test_skills.py +117 -0
- package/python/tests/test_skills_fetch.py +282 -0
- package/python/tests/test_stats_and_events.py +162 -0
- package/python/tests/test_streaming.py +55 -0
- package/python/tests/test_supervisor_pipeline.py +176 -0
- package/python/tests/test_supervisor_triage.py +102 -0
- package/python/tests/test_taint.py +112 -0
- package/python/tests/test_task_stream.py +74 -0
- package/python/tests/test_thinking_sse.py +66 -0
- package/python/tests/test_thinking_stream.py +76 -0
- package/python/tests/test_tool_center.py +113 -0
- package/python/tests/test_tool_parallel.py +113 -0
- package/python/tests/test_tool_params.py +99 -0
- package/python/tests/test_tool_use.py +229 -0
- package/python/tests/test_toplevel_concurrency.py +116 -0
- package/python/tests/test_tracing.py +256 -0
- package/python/tests/test_truth_crystallizer.py +125 -0
- package/python/tests/test_truth_flow.py +88 -0
- package/python/tests/test_update_check.py +95 -0
- package/python/tests/test_url_policy.py +53 -0
- package/python/tests/test_verify_evidence.py +212 -0
- package/python/tests/test_version_sync.py +100 -0
- package/python/tests/test_web_trigger.py +82 -0
- package/python/tools/check_faiss_type.py +30 -0
- package/python/tools/check_log.ps1 +38 -0
- package/python/tools/ivf_status.py +72 -0
- package/python/tools/mypy_baseline.txt +13 -0
- package/python/tools/mypy_ratchet.py +143 -0
- package/python/tools/pack.py +173 -0
- package/python/tools/release_check.py +349 -0
- package/python/tools/restart_foxclaw.ps1 +71 -0
- package/python/tools/shared_write_audit.py +190 -0
- package/python/tools/size_baseline.txt +28 -0
- package/python/tools/size_check.py +102 -0
- package/python/tools/strip_comments.py +228 -0
- package/python/tools/verify_env.bat +2 -0
- package/python/tools/verify_env.py +25 -0
- package/python/vendor/HMR_LICENSE +37 -0
- package/python/vendor/HMR_README.md +65 -0
- package/python/vendor/hkc/LICENSE +37 -0
- package/python/vendor/hkc/hkc_ace/__init__.py +0 -0
- package/python/vendor/hkc/hkc_ace/ace.py +457 -0
- package/python/vendor/hkc/hkc_ace/skill_taxonomy.json +465 -0
- package/python/vendor/hkc/hkc_api/__init__.py +0 -0
- package/python/vendor/hkc/hkc_api/container.py +416 -0
- package/python/vendor/hkc/hkc_api/ingest_jobs.py +103 -0
- package/python/vendor/hkc/hkc_api/main.py +157 -0
- package/python/vendor/hkc/hkc_api/routes/__init__.py +0 -0
- package/python/vendor/hkc/hkc_api/routes/ability.py +151 -0
- package/python/vendor/hkc/hkc_api/routes/config.py +51 -0
- package/python/vendor/hkc/hkc_api/routes/knowledge.py +630 -0
- package/python/vendor/hkc/hkc_api/routes/search.py +88 -0
- package/python/vendor/hkc/hkc_api/schemas.py +153 -0
- package/python/vendor/hkc/hkc_api/synthesis.py +179 -0
- package/python/vendor/hkc/hkc_core/__init__.py +0 -0
- package/python/vendor/hkc/hkc_core/graph/__init__.py +0 -0
- package/python/vendor/hkc/hkc_core/graph/base.py +188 -0
- package/python/vendor/hkc/hkc_core/graph/sqlite_store.py +789 -0
- package/python/vendor/hkc/hkc_core/models/__init__.py +0 -0
- package/python/vendor/hkc/hkc_core/models/enums.py +65 -0
- package/python/vendor/hkc/hkc_core/models/ku.py +307 -0
- package/python/vendor/hkc/hkc_core/utils/__init__.py +0 -0
- package/python/vendor/hkc/hkc_core/utils/id_gen.py +113 -0
- package/python/vendor/hkc/hkc_crystallizer/__init__.py +44 -0
- package/python/vendor/hkc/hkc_crystallizer/adapters.py +94 -0
- package/python/vendor/hkc/hkc_crystallizer/candidate.py +145 -0
- package/python/vendor/hkc/hkc_crystallizer/crystallizer.py +179 -0
- package/python/vendor/hkc/hkc_crystallizer/ingress.py +90 -0
- package/python/vendor/hkc/hkc_kde/__init__.py +0 -0
- package/python/vendor/hkc/hkc_kde/chunker.py +197 -0
- package/python/vendor/hkc/hkc_kde/extractor.py +623 -0
- package/python/vendor/hkc/hkc_kde/kde.py +298 -0
- package/python/vendor/hkc/hkc_kde/loaders/__init__.py +0 -0
- package/python/vendor/hkc/hkc_kde/loaders/ebook_loader.py +153 -0
- package/python/vendor/hkc/hkc_kde/loaders/office_loader.py +194 -0
- package/python/vendor/hkc/hkc_kde/loaders/pdf_loader.py +162 -0
- package/python/vendor/hkc/hkc_kde/loaders/text_loader.py +157 -0
- package/python/vendor/hkc/hkc_kde/loaders/web_loader.py +91 -0
- package/python/vendor/hkc/hkc_kde/models.py +74 -0
- package/python/vendor/hkc/hkc_kde/packager.py +329 -0
- package/python/vendor/hkc/hkc_kde/relation_disc.py +151 -0
- package/python/vendor/hkc/hkc_kee/__init__.py +0 -0
- package/python/vendor/hkc/hkc_kee/dedup.py +227 -0
- package/python/vendor/hkc/hkc_kee/fingerprint.py +106 -0
- package/python/vendor/hkc/hkc_kee/kee.py +541 -0
- package/python/vendor/hkc/hkc_kep/__init__.py +0 -0
- package/python/vendor/hkc/hkc_kep/event_bus.py +205 -0
- package/python/vendor/hkc/hkc_sdk/__init__.py +63 -0
- package/python/vendor/hkc/hkc_sdk/base.py +151 -0
- package/python/vendor/hkc/hkc_sdk/direct_client.py +153 -0
- package/python/vendor/hkc/hkc_sdk/exceptions.py +39 -0
- package/python/vendor/hkc/hkc_sdk/http_client.py +166 -0
- package/python/vendor/hkc/hkc_sdk/models.py +171 -0
- package/python/vendor/hkc/hkc_search/__init__.py +0 -0
- package/python/vendor/hkc/hkc_search/bm25.py +325 -0
- package/python/vendor/hkc/hkc_search/embedding_backends.py +374 -0
- package/python/vendor/hkc/hkc_search/graph_search.py +132 -0
- package/python/vendor/hkc/hkc_search/hybrid.py +331 -0
- package/python/vendor/hkc/hkc_search/index_build_manager.py +378 -0
- package/python/vendor/hkc/hkc_search/vector_search.py +484 -0
- package/python/vendor/hmr/__init__.py +0 -0
- package/python/vendor/hmr/core/__init__.py +0 -0
- package/python/vendor/hmr/core/hmr.py +684 -0
- package/python/vendor/hmr/core/models.py +310 -0
- package/python/vendor/hmr/engines/__init__.py +0 -0
- package/python/vendor/hmr/engines/evolution.py +710 -0
- package/python/vendor/hmr/engines/jit_compiler.py +472 -0
- package/python/vendor/hmr/engines/lifecycle.py +264 -0
- package/python/vendor/hmr/engines/policy.py +625 -0
- package/python/vendor/hmr/engines/recall.py +260 -0
- package/python/vendor/hmr/engines/runtime_state.py +158 -0
- package/python/vendor/hmr/engines/scheduler.py +323 -0
- package/python/vendor/hmr/engines/semantic.py +150 -0
- package/python/vendor/hmr/engines/temporal.py +261 -0
- package/python/vendor/hmr/engines/thought_chain.py +635 -0
- package/python/vendor/hmr/graph/__init__.py +0 -0
- package/python/vendor/hmr/graph/cwg.py +338 -0
- package/python/vendor/hmr/graph/memory_graph.py +530 -0
- package/python/vendor/hmr/storage/__init__.py +0 -0
- package/python/vendor/hmr/storage/memory_fs.py +300 -0
- package/python/vendor/hmr/storage/vector_store.py +520 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""主管分诊与自答策略回归。
|
|
2
|
+
|
|
3
|
+
覆盖主管在「是否命中领域关键词」×「模型是否可用」下的派发决策:
|
|
4
|
+
· 关键词命中 → 直接路由到领域 Agent(快、确定),不额外调用一次 LLM 分诊;
|
|
5
|
+
· 未命中 + 模型可用 → 主管**直接自答**(dispatched_to=supervisor, self_answered=True);
|
|
6
|
+
· 未命中 + stub → 走派发兜底(assistant),保持离线确定性。
|
|
7
|
+
|
|
8
|
+
⚠ 历史说明(改名前为 test_llm_routing.py):本文件曾断言「关键词未命中 → LLM 分诊精修到
|
|
9
|
+
finance」。那次精修实现在 `agents.py:_llm_classify`,但因每次要多一次约 5s 的串行 LLM
|
|
10
|
+
往返,产品上被「主管直接自答」取代而退化为**死代码**(全仓库零调用点),已随本轮删除。
|
|
11
|
+
原断言随之永远无法满足,长期为红,却被归因为「环境问题」——这是**误判**:它不是环境问题,
|
|
12
|
+
是**测试过期**。现按真实行为重写,并把三种组合都锁住。
|
|
13
|
+
|
|
14
|
+
用"假 router"模拟真机(is_stub=False),避免依赖真实 API key,全离线。
|
|
15
|
+
"""
|
|
16
|
+
import asyncio
|
|
17
|
+
import sys
|
|
18
|
+
import tempfile
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
21
|
+
|
|
22
|
+
import foxclaw.core.web_search as ws
|
|
23
|
+
ws.needs_search = lambda q: False
|
|
24
|
+
|
|
25
|
+
from foxclaw import Foxclaw
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def check(cond, msg):
|
|
29
|
+
if not cond:
|
|
30
|
+
raise AssertionError(f"✗ {msg}")
|
|
31
|
+
print(f" ✓ {msg}")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
CLASSIFY_MARK = "只输出一个词"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def main():
|
|
38
|
+
print("=" * 56)
|
|
39
|
+
print("主管分诊与自答策略回归")
|
|
40
|
+
print("=" * 56)
|
|
41
|
+
os_ = Foxclaw(data_dir=tempfile.mkdtemp())
|
|
42
|
+
|
|
43
|
+
calls = []
|
|
44
|
+
|
|
45
|
+
def fake(system, messages, max_tokens=2048, agent_id=""):
|
|
46
|
+
content = str(messages[-1]["content"]) if messages else ""
|
|
47
|
+
calls.append(content)
|
|
48
|
+
if CLASSIFY_MARK in content:
|
|
49
|
+
return "finance"
|
|
50
|
+
return "已处理"
|
|
51
|
+
|
|
52
|
+
os_.router.provider = "deepseek"
|
|
53
|
+
os_.router._client = object()
|
|
54
|
+
os_.router.complete = fake
|
|
55
|
+
check(os_.router.is_stub is False, "router 处于'可用'状态(非 stub)")
|
|
56
|
+
|
|
57
|
+
print("\n【1】关键词未命中 + 模型可用 → 主管直接自答(不再二次分诊)")
|
|
58
|
+
no_kw = "讲个笑话听听"
|
|
59
|
+
check(os_.supervisor._has_keyword_hit(no_kw) is False, f"「{no_kw[:12]}…」确无关键词命中")
|
|
60
|
+
r = asyncio.run(os_.supervisor.run_task(no_kw))
|
|
61
|
+
d1 = r["result"].get("dispatched_to")
|
|
62
|
+
check(d1 == "supervisor", f"未命中 → 主管自答(dispatched_to={d1},预期 supervisor)")
|
|
63
|
+
check(r["result"].get("self_answered") is True, "标记 self_answered=True(确为自答而非派发)")
|
|
64
|
+
check(bool(str(r["result"].get("sub_result") or "").strip()), "自答产出非空答复")
|
|
65
|
+
check(not any(CLASSIFY_MARK in c for c in calls), "全程未发出分诊 prompt(不再多花一次 LLM 往返)")
|
|
66
|
+
|
|
67
|
+
print("\n【1b】零星花费报备(无财务关键词)→ 认作财务命中,派给财务")
|
|
68
|
+
for line in ("今天买了一件衣服花了105块", "后天花了100洗车"):
|
|
69
|
+
check(os_.supervisor._has_keyword_hit(line), f"「{line[:12]}…」算作领域命中")
|
|
70
|
+
check(os_.supervisor._route(line) == "finance", f"「{line[:12]}…」→ finance")
|
|
71
|
+
check(os_.supervisor._route("每月房租3000占收入10000这样合理吗") == "finance",
|
|
72
|
+
"房租占比咨询 → finance")
|
|
73
|
+
|
|
74
|
+
print("\n【2】关键词命中 → 直接路由,不问 LLM 分诊(省成本)")
|
|
75
|
+
calls.clear()
|
|
76
|
+
r2 = asyncio.run(os_.supervisor.run_task("帮我做个预算分析"))
|
|
77
|
+
d2 = r2["result"].get("dispatched_to")
|
|
78
|
+
check(d2 == "finance", f"命中'预算' → 直接 finance(实为 {d2})")
|
|
79
|
+
check(not any(CLASSIFY_MARK in c for c in calls), "关键词命中时不发出分诊 prompt")
|
|
80
|
+
|
|
81
|
+
print("\n【3】stub + 关键词未命中 → 主管自处理(离线确定性)")
|
|
82
|
+
os_.router.provider = "stub"
|
|
83
|
+
os_.router._client = None
|
|
84
|
+
calls.clear()
|
|
85
|
+
r3 = asyncio.run(os_.supervisor.run_task("解释一下什么是航天器"))
|
|
86
|
+
check(os_.router.is_stub is True, "router 回到 stub")
|
|
87
|
+
d3 = r3["result"].get("dispatched_to")
|
|
88
|
+
check(d3 == "supervisor", f"stub 下未命中 → 主管自身处理(实为 {d3})")
|
|
89
|
+
check(not r3["result"].get("self_answered"), "stub 下走派发路径,而非主管自答")
|
|
90
|
+
check(not any(CLASSIFY_MARK in c for c in calls), "stub 下不发出分诊 prompt")
|
|
91
|
+
|
|
92
|
+
print("\n" + "=" * 56)
|
|
93
|
+
print("主管分诊与自答策略回归通过")
|
|
94
|
+
print("=" * 56)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_supervisor_triage():
|
|
98
|
+
main()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
if __name__ == "__main__":
|
|
102
|
+
main()
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""
|
|
2
|
+
数据污点追踪 + 动态审批门槛测试 —— 全离线。
|
|
3
|
+
|
|
4
|
+
对齐 Meta Muse「eBPF 污点追踪」的软件版:会话一旦取回用户/敏感数据就打污点,
|
|
5
|
+
之后外发/副作用动作的审批门槛动态提高。覆盖:
|
|
6
|
+
1. injection_guard.taint_level:来源 → 敏感度分级。
|
|
7
|
+
2. permission.evaluate 污点门槛:默认关闭(行为不变)/ 达标+外发 → TAINT_CONFIRM /
|
|
8
|
+
达标+只读 → 不触发 / 未达标 → 不触发。
|
|
9
|
+
3. base 污点传播:_dispatch_tool 取回 memory/web_search 后会话污点单调上升;
|
|
10
|
+
会话开始 _taint_reset 清零。
|
|
11
|
+
"""
|
|
12
|
+
import os, sys, types
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
os.environ.setdefault("HF_HUB_OFFLINE", "1")
|
|
15
|
+
os.environ.setdefault("TRANSFORMERS_OFFLINE", "1")
|
|
16
|
+
os.environ["FOXCLAW_EMBEDDING_MODEL"] = "foxclaw-nonexistent-fast"
|
|
17
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
18
|
+
|
|
19
|
+
from foxclaw.core import injection_guard as IG
|
|
20
|
+
from foxclaw.skills import permission as PERM
|
|
21
|
+
from foxclaw.agents.base import (
|
|
22
|
+
CognitiveAgent, _taint_get, _taint_raise, _taint_reset,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _skill(side_effect="read", read_only=True, external=False, destructive=False):
|
|
27
|
+
"""构造一个 permission.evaluate 可用的最小 skill-spec(duck-typed)。"""
|
|
28
|
+
return types.SimpleNamespace(
|
|
29
|
+
id=f"tool:test_{side_effect}",
|
|
30
|
+
params={},
|
|
31
|
+
side_effect=side_effect,
|
|
32
|
+
read_only=read_only,
|
|
33
|
+
external=external,
|
|
34
|
+
destructive=destructive,
|
|
35
|
+
trust_level=lambda: "high",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_taint_level_grading():
|
|
40
|
+
assert IG.taint_level("web_search") == IG.TAINT_EXTERNAL == 1
|
|
41
|
+
assert IG.taint_level("knowledge") == IG.TAINT_PERSONAL == 2
|
|
42
|
+
assert IG.taint_level("file") == IG.TAINT_PERSONAL == 2
|
|
43
|
+
assert IG.taint_level("memory") == IG.TAINT_SENSITIVE == 3
|
|
44
|
+
assert IG.taint_level("unknown_source") == IG.TAINT_EXTERNAL
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_permission_taint_default_off_behavior_unchanged():
|
|
48
|
+
s = _skill(side_effect="send", read_only=False)
|
|
49
|
+
d = PERM.evaluate(s, {})
|
|
50
|
+
assert d.decision == "ask" and d.reason_code == PERM.SIDE_EFFECT_CONFIRM
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_permission_taint_blocks_send_when_sensitive():
|
|
54
|
+
s = _skill(side_effect="send", read_only=False)
|
|
55
|
+
d = PERM.evaluate(s, {}, taint_level=IG.TAINT_SENSITIVE,
|
|
56
|
+
taint_threshold=IG.TAINT_PERSONAL)
|
|
57
|
+
assert d.decision == "ask" and d.reason_code == PERM.TAINT_CONFIRM
|
|
58
|
+
assert d.requires_confirmation
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_permission_taint_blocks_external_when_personal():
|
|
62
|
+
s = _skill(side_effect="read", read_only=False, external=True)
|
|
63
|
+
d = PERM.evaluate(s, {}, taint_level=IG.TAINT_PERSONAL,
|
|
64
|
+
taint_threshold=IG.TAINT_PERSONAL)
|
|
65
|
+
assert d.decision == "ask" and d.reason_code == PERM.TAINT_CONFIRM
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_permission_taint_not_trigger_on_readonly():
|
|
69
|
+
s = _skill(side_effect="read", read_only=True)
|
|
70
|
+
d = PERM.evaluate(s, {}, taint_level=IG.TAINT_SENSITIVE,
|
|
71
|
+
taint_threshold=IG.TAINT_PERSONAL)
|
|
72
|
+
assert d.decision == "allow"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_permission_taint_not_trigger_below_threshold():
|
|
76
|
+
s = _skill(side_effect="send", read_only=False)
|
|
77
|
+
d = PERM.evaluate(s, {}, taint_level=IG.TAINT_EXTERNAL,
|
|
78
|
+
taint_threshold=IG.TAINT_PERSONAL)
|
|
79
|
+
assert d.reason_code == PERM.SIDE_EFFECT_CONFIRM
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_taint_helpers_on_fake_agent():
|
|
83
|
+
a = types.SimpleNamespace(_think=lambda **k: None)
|
|
84
|
+
assert _taint_get(a) == 0
|
|
85
|
+
_taint_raise(a, IG.TAINT_EXTERNAL)
|
|
86
|
+
_taint_raise(a, IG.TAINT_SENSITIVE)
|
|
87
|
+
assert _taint_get(a) == IG.TAINT_SENSITIVE
|
|
88
|
+
_taint_reset(a)
|
|
89
|
+
assert _taint_get(a) == 0
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_dispatch_recall_memory_raises_taint():
|
|
93
|
+
a = types.SimpleNamespace(
|
|
94
|
+
_think=lambda **k: None,
|
|
95
|
+
_recall_memory=lambda q, top_k=5: [{"content": "用户偏好:喜欢简洁"}])
|
|
96
|
+
out = CognitiveAgent._dispatch_tool(a, "recall_memory", {"query": "偏好"})
|
|
97
|
+
assert "喜欢简洁" in out
|
|
98
|
+
assert _taint_get(a) == IG.TAINT_SENSITIVE
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_dispatch_web_search_raises_taint(monkeypatch):
|
|
102
|
+
from foxclaw.core import web_search as WS
|
|
103
|
+
monkeypatch.setattr(WS, "search_answer", lambda q: {"answer": "答案ABC", "references": []})
|
|
104
|
+
a = types.SimpleNamespace(_think=lambda **k: None)
|
|
105
|
+
out = CognitiveAgent._dispatch_tool(a, "web_search", {"query": "问题"})
|
|
106
|
+
assert "答案ABC" in out
|
|
107
|
+
assert _taint_get(a) == IG.TAINT_EXTERNAL
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
if __name__ == "__main__":
|
|
111
|
+
import pytest
|
|
112
|
+
pytest.main([__file__, "-v"])
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""D1:/api/task/stream SSE —— 任务执行流式化(逐 stage 进度 + act 逐 token + 最终 result)。
|
|
2
|
+
与 test_streaming.py 的 /api/chat/stream 端点测试对称:那条测对话直答流式,这条测**走完整认知
|
|
3
|
+
管线**的任务流式。核心逻辑(run_task_stream)在 test_agent_lifecycle 已直测,这里补端点级 SSE 打通。"""
|
|
4
|
+
import os, sys, tempfile, json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_task_stream():
|
|
10
|
+
os.environ["FOXCLAW_SHADOW_ROUNDS"] = "6"
|
|
11
|
+
os.environ.setdefault("FOXCLAW_EMBEDDING_MODEL", "foxclaw-nonexistent-fast")
|
|
12
|
+
os.environ["FOXCLAW_STREAM_RATE"] = "60"
|
|
13
|
+
os.environ["FOXCLAW_DATA_DIR"] = tempfile.mkdtemp(prefix="foxclaw_taskstream_")
|
|
14
|
+
os.environ["FOXCLAW_SKIP_DOTENV"] = "1"
|
|
15
|
+
for _k in ("DEEPSEEK_API_KEY", "ANTHROPIC_API_KEY", "OPENAI_API_KEY",
|
|
16
|
+
"GOOGLE_API_KEY", "DASHSCOPE_API_KEY", "FOXCLAW_API_KEY"):
|
|
17
|
+
os.environ[_k] = ""
|
|
18
|
+
|
|
19
|
+
print("=" * 60); print("D1 · /api/task/stream 任务流式化验证"); print("=" * 60)
|
|
20
|
+
|
|
21
|
+
from fastapi.testclient import TestClient
|
|
22
|
+
import foxclaw.api as _api
|
|
23
|
+
from foxclaw.api import app
|
|
24
|
+
_api._STREAM_LIMITER.capacity = 60
|
|
25
|
+
_api._STREAM_LIMITER.rate = 1.0
|
|
26
|
+
_api._STREAM_LIMITER.tokens = 60.0
|
|
27
|
+
|
|
28
|
+
with TestClient(app) as c:
|
|
29
|
+
with c.stream("POST", "/api/task/stream",
|
|
30
|
+
json={"task": "帮我分析一下本月开销", "agent_id": "office"}) as resp:
|
|
31
|
+
assert resp.status_code == 200, resp.status_code
|
|
32
|
+
stages, deltas, results, errors = [], [], [], []
|
|
33
|
+
for line in resp.iter_lines():
|
|
34
|
+
if not line or not line.startswith("data: "):
|
|
35
|
+
continue
|
|
36
|
+
ev = json.loads(line[6:])
|
|
37
|
+
if "stage" in ev: stages.append(ev["stage"])
|
|
38
|
+
if "delta" in ev: deltas.append(ev["delta"])
|
|
39
|
+
if "result" in ev: results.append(ev["result"])
|
|
40
|
+
if "error" in ev: errors.append(ev["error"])
|
|
41
|
+
assert not errors, f"不应有 error 事件: {errors}"
|
|
42
|
+
assert "act" in stages, f"应有 stage 进度事件(含 act): {stages}"
|
|
43
|
+
assert len(deltas) > 0, "应有 act 逐 token delta 事件(证明流式透传经端点打通)"
|
|
44
|
+
assert len(results) == 1, f"应恰有一个最终 result 事件,实得 {len(results)}"
|
|
45
|
+
assert results[0].get("agent") == "office" and "result" in results[0], results[0]
|
|
46
|
+
print(f"[1] /api/task/stream SSE(stage={len(stages)} delta={len(deltas)} result=1) OK")
|
|
47
|
+
|
|
48
|
+
with c.stream("POST", "/api/task/stream",
|
|
49
|
+
json={"task": "随便问问", "agent_id": "supervisor"}) as resp:
|
|
50
|
+
assert resp.status_code == 200, resp.status_code
|
|
51
|
+
stages, results, errors = [], [], []
|
|
52
|
+
for line in resp.iter_lines():
|
|
53
|
+
if not line or not line.startswith("data: "):
|
|
54
|
+
continue
|
|
55
|
+
ev = json.loads(line[6:])
|
|
56
|
+
if "stage" in ev: stages.append(ev["stage"])
|
|
57
|
+
if "result" in ev: results.append(ev["result"])
|
|
58
|
+
if "error" in ev: errors.append(ev["error"])
|
|
59
|
+
assert not errors, f"不应有 error 事件: {errors}"
|
|
60
|
+
assert len(results) == 1, f"应恰有一个最终 result 事件,实得 {len(results)}"
|
|
61
|
+
r0 = results[0]
|
|
62
|
+
assert r0.get("agent") == "supervisor", \
|
|
63
|
+
f"P3-3:主管走自身 pipeline,不再回落 assistant(实为 {r0.get('agent')})"
|
|
64
|
+
assert "dispatch" in stages and "route" in stages, \
|
|
65
|
+
f"应见主管管线 stage(route/dispatch): {stages}"
|
|
66
|
+
assert r0["result"].get("dispatched_to") == "supervisor", \
|
|
67
|
+
f"stub 无关键词 → 主管自身处理(实为 {r0['result'].get('dispatched_to')})"
|
|
68
|
+
print("[2] agent_id=supervisor → 主管自身流式管线(P3-3,不再回落) OK")
|
|
69
|
+
|
|
70
|
+
print("\nD1 /api/task/stream 验证通过")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
if __name__ == "__main__":
|
|
74
|
+
test_task_stream()
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""/api/chat/stream 的命名 SSE 事件:`event: thinking` 与正文 delta 分离。
|
|
2
|
+
|
|
3
|
+
前端按事件名折叠展示推理;正文 delta 里不得出现推理文本(否则复制/落库会被污染)。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
import tempfile
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_chat_stream_emits_named_thinking_event():
|
|
18
|
+
if "foxclaw.api" in sys.modules:
|
|
19
|
+
import pytest
|
|
20
|
+
pytest.skip("需独立进程运行(模块级单例已固化),请用 tests/run_all.py")
|
|
21
|
+
os.environ.setdefault("FOXCLAW_DATA_DIR", tempfile.mkdtemp(prefix="foxclaw_think_"))
|
|
22
|
+
|
|
23
|
+
from fastapi.testclient import TestClient
|
|
24
|
+
from foxclaw.agents.base import CognitiveAgent
|
|
25
|
+
from foxclaw.api import app
|
|
26
|
+
|
|
27
|
+
def _fake_stream(self, task, stop_event=None, images=None, history=None,
|
|
28
|
+
attachments=None, on_thinking=None, on_evidence=None):
|
|
29
|
+
if on_thinking is not None:
|
|
30
|
+
on_thinking("先分析一下用户意图")
|
|
31
|
+
yield "好的"
|
|
32
|
+
|
|
33
|
+
original = CognitiveAgent.stream_reply
|
|
34
|
+
CognitiveAgent.stream_reply = _fake_stream
|
|
35
|
+
try:
|
|
36
|
+
with TestClient(app) as c:
|
|
37
|
+
resp = c.post("/api/chat/stream", json={"task": "hi", "agent_id": "supervisor"})
|
|
38
|
+
assert resp.status_code == 200, resp.status_code
|
|
39
|
+
body = resp.text
|
|
40
|
+
finally:
|
|
41
|
+
CognitiveAgent.stream_reply = original
|
|
42
|
+
|
|
43
|
+
assert "event: thinking" in body, "推理必须以命名事件送出"
|
|
44
|
+
blocks = [b for b in body.split("\n\n") if b.strip()]
|
|
45
|
+
think_payloads, delta_payloads = [], []
|
|
46
|
+
for b in blocks:
|
|
47
|
+
name = "message"
|
|
48
|
+
data = None
|
|
49
|
+
for line in b.splitlines():
|
|
50
|
+
if line.startswith("event: "):
|
|
51
|
+
name = line[7:].strip()
|
|
52
|
+
elif line.startswith("data: "):
|
|
53
|
+
data = line[6:].strip()
|
|
54
|
+
if data is None:
|
|
55
|
+
continue
|
|
56
|
+
ev = json.loads(data)
|
|
57
|
+
if name == "thinking":
|
|
58
|
+
think_payloads.append(ev)
|
|
59
|
+
elif "delta" in ev:
|
|
60
|
+
delta_payloads.append(ev["delta"])
|
|
61
|
+
|
|
62
|
+
assert think_payloads, "应有 thinking 事件块"
|
|
63
|
+
assert "先分析一下用户意图" == "".join(p.get("thinking", "") for p in think_payloads)
|
|
64
|
+
joined = "".join(delta_payloads)
|
|
65
|
+
assert joined == "好的"
|
|
66
|
+
assert "先分析一下" not in joined, "推理不得混入正文 delta"
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""推理过程可见:`_ThinkStripper` 不再丢弃思考,原生 reasoning 走独立 thinking 通道。
|
|
2
|
+
|
|
3
|
+
背景:`core/model_router.py` 的 `_ThinkStripper.feed` 只返回可见正文,think 块内容被整段
|
|
4
|
+
淘汰;openai-compatible 的 `delta.reasoning_content`(deepseek-reasoner 等)更是完全没读,
|
|
5
|
+
用户只看到结论,看不到推理。现在两者都转成 `on_thinking` 回调 → SSE `event: thinking`。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from types import SimpleNamespace
|
|
11
|
+
|
|
12
|
+
from foxclaw.core.model_router import ModelRouter, _ThinkStripper
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class _FakeCompletions:
|
|
16
|
+
def __init__(self, chunks):
|
|
17
|
+
self.chunks = chunks
|
|
18
|
+
self.kwargs: dict = {}
|
|
19
|
+
|
|
20
|
+
def create(self, **kwargs):
|
|
21
|
+
self.kwargs = kwargs
|
|
22
|
+
return iter(self.chunks)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class _FakeClient:
|
|
26
|
+
def __init__(self, chunks):
|
|
27
|
+
self.chat = SimpleNamespace(completions=_FakeCompletions(chunks))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _chunk(content=None, reasoning=None):
|
|
31
|
+
delta = SimpleNamespace(content=content, reasoning_content=reasoning)
|
|
32
|
+
return SimpleNamespace(choices=[SimpleNamespace(delta=delta)])
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _router(chunks) -> ModelRouter:
|
|
36
|
+
r = ModelRouter(provider="openai", model="gpt-4o-mini")
|
|
37
|
+
r._client = _FakeClient(chunks)
|
|
38
|
+
return r
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_stripper_returns_think_text_separately() -> None:
|
|
42
|
+
s = _ThinkStripper()
|
|
43
|
+
vis1, th1 = s.feed_events("答案前<thi")
|
|
44
|
+
assert (vis1, th1) == ("答案前", ""), "半个标签不能当正文吐出"
|
|
45
|
+
vis2, th2 = s.feed_events("nk>内部推理</think>答案")
|
|
46
|
+
assert vis2 == "答案"
|
|
47
|
+
assert th2 == "内部推理"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_stripper_feed_still_visible_only() -> None:
|
|
51
|
+
s = _ThinkStripper()
|
|
52
|
+
assert s.feed("a<think>b</think>c") == "ac"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_reasoning_content_goes_to_thinking_only() -> None:
|
|
56
|
+
r = _router([_chunk(None, "先想一下"), _chunk("你好"), _chunk("世界")])
|
|
57
|
+
think: list[str] = []
|
|
58
|
+
text = "".join(r.stream("sys", [{"role": "user", "content": "hi"}],
|
|
59
|
+
on_thinking=think.append))
|
|
60
|
+
assert "".join(think) == "先想一下", "原生 reasoning_content 必须进推理通道"
|
|
61
|
+
assert text == "你好世界", "正文不得混入推理"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_embedded_think_routed_to_callback() -> None:
|
|
65
|
+
r = _router([_chunk("<think>内嵌思考</think>答案是 42<think>补一句")])
|
|
66
|
+
think: list[str] = []
|
|
67
|
+
text = "".join(r.stream("sys", [{"role": "user", "content": "hi"}],
|
|
68
|
+
on_thinking=think.append))
|
|
69
|
+
assert text == "答案是 42"
|
|
70
|
+
assert "内嵌思考" in "".join(think)
|
|
71
|
+
assert "补一句" not in text, "未闭合的 think 尾巴不得漏进正文"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_no_callback_keeps_old_behavior() -> None:
|
|
75
|
+
r = _router([_chunk("<think>x</think>ok")])
|
|
76
|
+
assert "".join(r.stream("sys", [{"role": "user", "content": "hi"}])) == "ok"
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""工具中心第1期 · 技能台真实化 + 安装审核门 + 治理 端到端验证。
|
|
2
|
+
判定:assert 失败→Traceback→run_all 记失败;正常退出=通过。"""
|
|
3
|
+
import sys, os, tempfile
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
6
|
+
|
|
7
|
+
def test_tool_center():
|
|
8
|
+
os.environ["FOXCLAW_SHADOW_ROUNDS"] = "6"
|
|
9
|
+
_DATA = tempfile.mkdtemp(prefix="foxclaw_toolc_")
|
|
10
|
+
os.environ["FOXCLAW_DATA_DIR"] = _DATA
|
|
11
|
+
|
|
12
|
+
from fastapi.testclient import TestClient
|
|
13
|
+
from foxclaw.api import app
|
|
14
|
+
|
|
15
|
+
WEBAPI = {"id": "wx_weather", "name": "天气", "description": "查天气", "kind": "web_api",
|
|
16
|
+
"triggers": ["天气"], "side_effect": "read",
|
|
17
|
+
"endpoint": "https://example.com/api/weather", "http_method": "GET"}
|
|
18
|
+
|
|
19
|
+
with TestClient(app) as c:
|
|
20
|
+
print("=" * 60); print("工具中心 · 技能台 + 审核门 + 治理 验证"); print("=" * 60)
|
|
21
|
+
|
|
22
|
+
r = c.post("/api/ahs/skills", json=WEBAPI)
|
|
23
|
+
assert r.status_code == 200, r.text
|
|
24
|
+
assert r.json()["created"] is True
|
|
25
|
+
print("[1] 安装 web_api 技能 OK")
|
|
26
|
+
|
|
27
|
+
r = c.get("/api/ahs/skills"); assert r.status_code == 200
|
|
28
|
+
ids = {s["id"]: s for s in r.json()["skills"]}
|
|
29
|
+
assert "wx_weather" in ids and ids["wx_weather"]["source"] == "user"
|
|
30
|
+
print("[2] 列表含新技能且 source=user OK")
|
|
31
|
+
|
|
32
|
+
r = c.post("/api/ahs/skills", json={**WEBAPI, "id": "x_local", "kind": "local"})
|
|
33
|
+
assert r.status_code == 400
|
|
34
|
+
print("[3] 拒绝 local kind(400) OK")
|
|
35
|
+
|
|
36
|
+
r = c.post("/api/ahs/skills", json={**WEBAPI, "id": "x_bad", "endpoint": "ftp://x"})
|
|
37
|
+
assert r.status_code == 400
|
|
38
|
+
print("[4] 拒非法 endpoint(400) OK")
|
|
39
|
+
|
|
40
|
+
r = c.post("/api/ahs/skills", json={"id": "x_mcp", "name": "m", "kind": "mcp", "side_effect": "read"})
|
|
41
|
+
assert r.status_code == 400
|
|
42
|
+
print("[5] mcp 缺 server/tool(400) OK")
|
|
43
|
+
|
|
44
|
+
r = c.patch("/api/ahs/skills/wx_weather", json={"enabled": False})
|
|
45
|
+
assert r.status_code == 200 and r.json()["skill"]["enabled"] is False
|
|
46
|
+
r = c.patch("/api/ahs/skills/wx_weather", json={"enabled": True})
|
|
47
|
+
assert r.json()["skill"]["enabled"] is True
|
|
48
|
+
print("[6] 启停切换 OK")
|
|
49
|
+
|
|
50
|
+
r = c.get("/api/ahs/skills")
|
|
51
|
+
builtin = next((s["id"] for s in r.json()["skills"] if s["source"] == "builtin"), None)
|
|
52
|
+
if builtin:
|
|
53
|
+
rd = c.delete(f"/api/ahs/skills/{builtin}")
|
|
54
|
+
assert rd.status_code == 409, rd.text
|
|
55
|
+
print("[7] 内置技能不可删(409) OK")
|
|
56
|
+
rc = c.post("/api/ahs/skills", json={**WEBAPI, "id": builtin})
|
|
57
|
+
assert rc.status_code == 409, rc.text
|
|
58
|
+
print("[7b] 用内置 id 安装被拒(409) OK")
|
|
59
|
+
else:
|
|
60
|
+
print("[7] 跳过(无内置技能)")
|
|
61
|
+
|
|
62
|
+
r = c.post("/api/ahs/skills/wx_weather/test", json={"args": {}})
|
|
63
|
+
assert r.status_code == 200 and "success" in r.json()
|
|
64
|
+
print("[8] 测试调用返回 SkillResult OK")
|
|
65
|
+
|
|
66
|
+
r = c.post("/api/ahs/skills", json={"id": "ssrf_t", "name": "s", "description": "", "kind": "web_api",
|
|
67
|
+
"triggers": ["x"], "side_effect": "read", "endpoint": "http://127.0.0.1:8000/api/status"})
|
|
68
|
+
assert r.status_code == 403, r.text
|
|
69
|
+
assert r.json()["detail"]["error"] == "install_denied"
|
|
70
|
+
print("[9] 安装审核门 deny 本地地址(403) OK")
|
|
71
|
+
|
|
72
|
+
_send = {"id": "send_x", "name": "发信", "description": "", "kind": "web_api",
|
|
73
|
+
"triggers": ["发"], "side_effect": "send", "endpoint": "https://example.com/send"}
|
|
74
|
+
r = c.post("/api/ahs/skills", json=_send)
|
|
75
|
+
assert r.status_code == 409 and r.json()["detail"]["needs_ack"] is True, r.text
|
|
76
|
+
r = c.post("/api/ahs/skills", json={**_send, "acknowledged": True})
|
|
77
|
+
assert r.status_code == 200, r.text
|
|
78
|
+
print("[9b] 副作用技能需确认(409→200) OK")
|
|
79
|
+
|
|
80
|
+
assert c.get("/api/ahs/breaker").status_code == 200
|
|
81
|
+
assert c.get("/api/ahs/hooks/audit").status_code == 200
|
|
82
|
+
print("[10] 治理端点(breaker/audit) OK")
|
|
83
|
+
|
|
84
|
+
sk = {s["id"]: s for s in c.get("/api/ahs/skills").json()["skills"]}
|
|
85
|
+
assert sk["calculator"]["trust"] == "high", sk["calculator"]["trust"]
|
|
86
|
+
assert sk["gmail_send"]["trust"] == "low", sk["gmail_send"]["trust"]
|
|
87
|
+
assert sk["wx_weather"]["trust"] == "mid"
|
|
88
|
+
print("[10b] 信任分级 high/mid/low OK")
|
|
89
|
+
|
|
90
|
+
c.post("/api/ahs/policy/deny", json={"id": "wx_weather"})
|
|
91
|
+
r = c.post("/api/ahs/skills/wx_weather/test", json={"args": {}, "confirmed": True}).json()
|
|
92
|
+
assert r["status"] == "blocked" and "封杀" in r["message"], r
|
|
93
|
+
assert next(s for s in c.get("/api/ahs/skills").json()["skills"] if s["id"] == "wx_weather")["blocked"] is True
|
|
94
|
+
assert "wx_weather" in c.get("/api/ahs/policy").json()["deny"]
|
|
95
|
+
assert c.delete("/api/ahs/policy/deny/wx_weather").status_code == 200
|
|
96
|
+
assert "wx_weather" not in c.get("/api/ahs/policy").json()["deny"]
|
|
97
|
+
assert next(s for s in c.get("/api/ahs/skills").json()["skills"] if s["id"] == "wx_weather")["blocked"] is False
|
|
98
|
+
print("[10c] deny 封杀/解封 kill-switch OK")
|
|
99
|
+
|
|
100
|
+
assert c.delete("/api/ahs/skills/wx_weather").status_code == 200
|
|
101
|
+
r = c.get("/api/ahs/skills"); assert "wx_weather" not in {s["id"] for s in r.json()["skills"]}
|
|
102
|
+
print("[11] 删除用户技能 OK")
|
|
103
|
+
|
|
104
|
+
from foxclaw import Foxclaw
|
|
105
|
+
os2 = Foxclaw(data_dir=_DATA)
|
|
106
|
+
assert os2.skill_registry.get("send_x") is not None, "重启后用户技能丢失"
|
|
107
|
+
assert os2.skill_registry.get("send_x").source == "user"
|
|
108
|
+
print("[12] 重启后用户技能仍在(持久化 round-trip) OK")
|
|
109
|
+
|
|
110
|
+
print("\n工具中心第1期验证全部通过")
|
|
111
|
+
|
|
112
|
+
if __name__ == "__main__":
|
|
113
|
+
test_tool_center()
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""工具调度:只读并发 / 写与未声明串行 / 单工具超时 / 污点回主线程合并。
|
|
2
|
+
|
|
3
|
+
背景:`agents/mixins/tool_exec.py` 原本逐个 `_dispatch_tool` 串行,一个慢工具阻塞整轮,
|
|
4
|
+
且完全无单工具超时——模型一次发两个只读工具(联网+召回)要等两倍时间,网络卡死则整轮挂住。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import threading
|
|
10
|
+
import time
|
|
11
|
+
|
|
12
|
+
from foxclaw.agents.mixins import tool_exec as _te
|
|
13
|
+
from foxclaw.agents.mixins.tool_exec import ToolExecMixin
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class _Fake(ToolExecMixin):
|
|
17
|
+
"""只保留调度层:闸门走真实 registry + permission,执行被 monkeypatch 替换。"""
|
|
18
|
+
|
|
19
|
+
def __init__(self):
|
|
20
|
+
self.calls: list[str] = []
|
|
21
|
+
self._taint_level = 0
|
|
22
|
+
|
|
23
|
+
def _think(self, **kwargs):
|
|
24
|
+
return None, None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _make(monkeypatch, behavior) -> _Fake:
|
|
28
|
+
fake = _Fake()
|
|
29
|
+
|
|
30
|
+
def _invoke(agent, name, args, taint_sink=None):
|
|
31
|
+
agent.calls.append(name)
|
|
32
|
+
return behavior(name, args, taint_sink)
|
|
33
|
+
|
|
34
|
+
monkeypatch.setattr(_te, "_invoke_tool", _invoke)
|
|
35
|
+
return fake
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _calls(*pairs):
|
|
39
|
+
return [{"name": n, "args": a} for n, a in pairs]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_readonly_tools_run_concurrently(monkeypatch) -> None:
|
|
43
|
+
barrier = threading.Barrier(2, timeout=3)
|
|
44
|
+
|
|
45
|
+
def behave(name, args, sink):
|
|
46
|
+
barrier.wait()
|
|
47
|
+
return f"{name}:ok"
|
|
48
|
+
|
|
49
|
+
fake = _make(monkeypatch, behave)
|
|
50
|
+
t0 = time.monotonic()
|
|
51
|
+
out = fake._run_tools(_calls(("web_search", {"query": "python 3.14"}),
|
|
52
|
+
("recall_memory", {"query": "项目约定"})))
|
|
53
|
+
elapsed = time.monotonic() - t0
|
|
54
|
+
assert out == ["web_search:ok", "recall_memory:ok"]
|
|
55
|
+
assert elapsed < 2.0, f"两个只读工具应并发执行,实际耗时 {elapsed:.2f}s"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_slow_tool_is_cut_off(monkeypatch) -> None:
|
|
59
|
+
monkeypatch.setenv("FOXCLAW_TOOL_TIMEOUT", "0.3")
|
|
60
|
+
|
|
61
|
+
def behave(name, args, sink):
|
|
62
|
+
time.sleep(2)
|
|
63
|
+
return "late"
|
|
64
|
+
|
|
65
|
+
fake = _make(monkeypatch, behave)
|
|
66
|
+
t0 = time.monotonic()
|
|
67
|
+
out = fake._dispatch_tool("web_search", {"query": "x"})
|
|
68
|
+
assert "超过" in out and "未返回" in out
|
|
69
|
+
assert "不要编造" in out
|
|
70
|
+
assert time.monotonic() - t0 < 1.5, "超时后必须立刻返回,不能等工具自己结束"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_taint_collected_in_worker_and_merged_in_main(monkeypatch) -> None:
|
|
74
|
+
def behave(name, args, sink):
|
|
75
|
+
sink.append(3 if name == "web_search" else 2)
|
|
76
|
+
return "ok"
|
|
77
|
+
|
|
78
|
+
fake = _make(monkeypatch, behave)
|
|
79
|
+
fake._run_tools(_calls(("web_search", {"query": "x"}),
|
|
80
|
+
("recall_memory", {"query": "y"})))
|
|
81
|
+
assert fake._taint_level == 3, "并发执行后污点应取最大值合并回会话"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_single_readonly_call_still_merged(monkeypatch) -> None:
|
|
85
|
+
def behave(name, args, sink):
|
|
86
|
+
sink.append(2)
|
|
87
|
+
return "ok"
|
|
88
|
+
|
|
89
|
+
fake = _make(monkeypatch, behave)
|
|
90
|
+
assert fake._dispatch_tool("recall_memory", {"query": "y"}) == "ok"
|
|
91
|
+
assert fake._taint_level == 2
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_undeclared_tool_never_executed_and_batch_serialized(monkeypatch) -> None:
|
|
95
|
+
def behave(name, args, sink):
|
|
96
|
+
return "ok"
|
|
97
|
+
|
|
98
|
+
fake = _make(monkeypatch, behave)
|
|
99
|
+
out = fake._run_tools(_calls(("web_search", {"query": "x"}),
|
|
100
|
+
("write_file", {"path": "a.md"})))
|
|
101
|
+
assert "未知工具 write_file" in out[1]
|
|
102
|
+
assert fake.calls == ["web_search"], "未声明能力的工具必须被闸门挡住,不进入执行"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_missing_required_arg_reports_instead_of_executing(monkeypatch) -> None:
|
|
106
|
+
def behave(name, args, sink): # pragma: no cover - 不应被调用
|
|
107
|
+
raise AssertionError("入参校验失败时不该执行工具")
|
|
108
|
+
|
|
109
|
+
fake = _make(monkeypatch, behave)
|
|
110
|
+
out = fake._run_tools([{"name": "web_search", "args": {}}])
|
|
111
|
+
assert "入参不合法" in out[0]
|
|
112
|
+
assert "query" in out[0]
|
|
113
|
+
assert fake.calls == []
|