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,492 @@
|
|
|
1
|
+
"""
|
|
2
|
+
认知管线(Cognitive Pipeline, v2 ② Step1 抽壳)测试 —— 全离线,不建整机。
|
|
3
|
+
|
|
4
|
+
覆盖:
|
|
5
|
+
1. 默认管线 recall→act:stage 按序执行,ctx 逐步填充(memories→result/extra),trace 记录顺序
|
|
6
|
+
2. 同步/异步 stage 混跑
|
|
7
|
+
3. 可插桩:往 stages 里插入一个自定义 stage,按位置执行(为后续 verify/reflect/plan 铺路)
|
|
8
|
+
4. 异常透传:stage 抛异常向上传播(由 run_task 统一兜底,管线本身不吞)
|
|
9
|
+
"""
|
|
10
|
+
import sys, asyncio
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
13
|
+
|
|
14
|
+
from foxclaw.pipeline import (
|
|
15
|
+
PipelineContext, CognitivePipeline, DEFAULT_STAGES,
|
|
16
|
+
stage_recall, stage_act, stage_verify)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class _Node:
|
|
20
|
+
def __init__(self, status, delta): self.harness_status = status; self.harness_delta = delta
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class _FakeChain:
|
|
24
|
+
def __init__(self, nodes): self._nodes = nodes
|
|
25
|
+
def get_chain(self, session_id, agent_id=None): return self._nodes
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class FakeAgent:
|
|
29
|
+
"""最小假 Agent:只提供管线 stage 需要的钩子。"""
|
|
30
|
+
AGENT_ID = "fake"
|
|
31
|
+
_recall_topk = 5
|
|
32
|
+
def __init__(self, nodes=None, fail_on=None):
|
|
33
|
+
self.thought_chain = _FakeChain(nodes or [])
|
|
34
|
+
self.exec_count = 0
|
|
35
|
+
self._fail_on = fail_on
|
|
36
|
+
def _recall_query(self, task): return f"q:{task}"
|
|
37
|
+
def _recall_memory(self, query, top_k=5):
|
|
38
|
+
self.last_recall = (query, top_k)
|
|
39
|
+
return [{"content": f"mem for {query}"}]
|
|
40
|
+
async def _execute(self, task, context, session_id, memories):
|
|
41
|
+
self.exec_count += 1
|
|
42
|
+
if self._fail_on and self.exec_count == self._fail_on:
|
|
43
|
+
raise RuntimeError("重试期间出错")
|
|
44
|
+
return f"done:{task}#{self.exec_count}", {"call": self.exec_count}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_default_pipeline():
|
|
48
|
+
print("\n【1】默认管线 recall→act→verify:按序 + ctx 流动 + trace")
|
|
49
|
+
a = FakeAgent()
|
|
50
|
+
ctx = PipelineContext(agent=a, task="T", session_id="s1")
|
|
51
|
+
asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(ctx))
|
|
52
|
+
assert a.last_recall == ("q:T", 5), "recall stage 用 _recall_query/_recall_topk"
|
|
53
|
+
assert ctx.memories and ctx.memories[0]["content"] == "mem for q:T"
|
|
54
|
+
assert ctx.result == "done:T#1" and ctx.extra == {"call": 1}
|
|
55
|
+
assert ctx.trace == ["perceive", "recall", "plan", "route", "act", "verify", "reflect"], \
|
|
56
|
+
f"stage 顺序,实得 {ctx.trace}"
|
|
57
|
+
assert ctx.perceived.get("task_len") == 1, "perceive 记录输入特征"
|
|
58
|
+
assert ctx.plan.get("multi_step") is False and ctx.route.get("target") == "self"
|
|
59
|
+
assert ctx.verified.get("passed") is True, "verify 汇总校验结果到 ctx.verified"
|
|
60
|
+
assert ctx.reflect["decision"] == "accept" and ctx.reflect["retries"] == 0
|
|
61
|
+
assert a.exec_count == 1, "默认不重试,_execute 只调一次"
|
|
62
|
+
print(f" ✓ recall→plan→route→act→verify→reflect 按序;plan={ctx.plan['flow']} "
|
|
63
|
+
f"route={ctx.route['target']} reflect={ctx.reflect['decision']}")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_plan_route_stage():
|
|
67
|
+
print("\n【1c】plan/route stage:P1-2 引擎分析结构(informational,不改执行)")
|
|
68
|
+
from foxclaw.pipeline import stage_plan, stage_route
|
|
69
|
+
ctx = PipelineContext(agent=FakeAgent(), task="先分析支出 然后 给出建议")
|
|
70
|
+
stage_plan(ctx); stage_route(ctx)
|
|
71
|
+
assert ctx.plan["multi_step"] is True and len(ctx.plan["steps"]) == 2, ctx.plan
|
|
72
|
+
assert ctx.plan["flow"] == "sequential" and ctx.route["target"] == "self"
|
|
73
|
+
ctx2 = PipelineContext(agent=FakeAgent(), task="分析我的开支")
|
|
74
|
+
stage_plan(ctx2)
|
|
75
|
+
assert ctx2.plan["multi_step"] is False and ctx2.plan["steps"] == ["分析我的开支"]
|
|
76
|
+
print(f" ✓ 多步 plan={ctx.plan['steps']}(flow={ctx.plan['flow']});单步 multi_step=False")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_verify_stage():
|
|
80
|
+
print("\n【1b】verify stage:只读推理节点汇总 warns/max_delta(不新增校验)")
|
|
81
|
+
nodes = [_Node("PASS", 0.1), _Node("WARN", 0.42), _Node("PASS", 0.2), _Node("BASELINE", None)]
|
|
82
|
+
a = FakeAgent(nodes=nodes)
|
|
83
|
+
ctx = PipelineContext(agent=a, task="V", session_id="s2")
|
|
84
|
+
stage_verify(ctx)
|
|
85
|
+
assert ctx.verified == {"passed": True, "nodes": 4, "warns": 1,
|
|
86
|
+
"max_delta": 0.42, "last_delta": 0.2}, ctx.verified
|
|
87
|
+
print(f" ✓ verified={ctx.verified}(max_delta=峰值0.42;last_delta=最近0.2,供 reflect 早退)")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_insertable_stage():
|
|
91
|
+
print("\n【2】可插桩:插入自定义 stage 按位置执行")
|
|
92
|
+
hits = []
|
|
93
|
+
def stage_probe(ctx):
|
|
94
|
+
hits.append(("probe", len(ctx.memories)))
|
|
95
|
+
stage_probe.stage_name = "probe"
|
|
96
|
+
a = FakeAgent()
|
|
97
|
+
ctx = PipelineContext(agent=a, task="X")
|
|
98
|
+
pipe = CognitivePipeline([stage_recall, stage_probe, stage_act])
|
|
99
|
+
asyncio.run(pipe.run(ctx))
|
|
100
|
+
assert hits == [("probe", 1)], "probe 应在 recall(已填 memories)之后运行"
|
|
101
|
+
assert ctx.trace == ["recall", "probe", "act"]
|
|
102
|
+
print(f" ✓ 插入 stage 生效,trace={ctx.trace}(后续 verify/reflect 即这样插入)")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_exception_propagates():
|
|
106
|
+
print("\n【3】异常透传:stage 抛错向上传播(管线不吞)")
|
|
107
|
+
def boom(ctx):
|
|
108
|
+
raise RuntimeError("stage 炸了")
|
|
109
|
+
a = FakeAgent()
|
|
110
|
+
raised = False
|
|
111
|
+
try:
|
|
112
|
+
asyncio.run(CognitivePipeline([boom, stage_act]).run(PipelineContext(agent=a, task="Y")))
|
|
113
|
+
except RuntimeError:
|
|
114
|
+
raised = True
|
|
115
|
+
assert raised, "stage 异常应向上传播,由 run_task 统一兜底"
|
|
116
|
+
print(" ✓ stage 异常透传(HarnessBlockedError 即由此上抛给 run_task 兜底)")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class _CapSpine:
|
|
120
|
+
def __init__(self): self.events = []
|
|
121
|
+
def publish(self, topic, payload=None, source="", **kw): self.events.append(topic)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class _Caps:
|
|
125
|
+
def __init__(self, spine): self._spine = spine
|
|
126
|
+
def get(self, name, default=None): return self._spine if name == "event_spine" else default
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def test_stage_events():
|
|
130
|
+
print("\n【6】逐 stage 事件:每个 stage emit agent.cognition.<stage> 到脊柱")
|
|
131
|
+
spine = _CapSpine()
|
|
132
|
+
a = FakeAgent()
|
|
133
|
+
a._caps = _Caps(spine)
|
|
134
|
+
ctx = PipelineContext(agent=a, task="E", session_id="s3")
|
|
135
|
+
asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(ctx))
|
|
136
|
+
expected = ["agent.cognition." + n for n in
|
|
137
|
+
["perceive", "recall", "plan", "route", "act", "verify", "reflect"]]
|
|
138
|
+
assert spine.events == expected, f"逐 stage 事件,实得 {spine.events}"
|
|
139
|
+
a2 = FakeAgent()
|
|
140
|
+
asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(PipelineContext(agent=a2, task="X")))
|
|
141
|
+
print(" ✓ 7 个 agent.cognition.* 事件按序 emit;无中枢时优雅跳过")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def test_route_skill_optin_inert():
|
|
145
|
+
print("\n【7】A·Step5:route 默认自动选 skill(仅只读+高阈值);可关;有副作用的不自动选")
|
|
146
|
+
from foxclaw.pipeline import stage_route
|
|
147
|
+
from foxclaw.skills import build_default_registry
|
|
148
|
+
reg = build_default_registry()
|
|
149
|
+
|
|
150
|
+
a = FakeAgent(); a.skill_registry = reg
|
|
151
|
+
ctx = PipelineContext(agent=a, task="帮我计算 (12+8)*5")
|
|
152
|
+
stage_route(ctx)
|
|
153
|
+
assert ctx.route["target"] == "skill" and ctx.route["skill_id"] == "calculator", \
|
|
154
|
+
f"默认应自动选中只读的 calculator: {ctx.route}"
|
|
155
|
+
|
|
156
|
+
ctx_off = PipelineContext(agent=a, task="帮我计算 (12+8)*5",
|
|
157
|
+
context={"_route_skill": False})
|
|
158
|
+
stage_route(ctx_off)
|
|
159
|
+
assert ctx_off.route["target"] == "self", ctx_off.route
|
|
160
|
+
|
|
161
|
+
ctx2 = PipelineContext(agent=a, task="帮我计算 (12+8)*5",
|
|
162
|
+
context={"_route_skill": {"threshold": 0.5}})
|
|
163
|
+
stage_route(ctx2)
|
|
164
|
+
assert ctx2.route["target"] == "skill" and ctx2.route["skill_id"] == "calculator", ctx2.route
|
|
165
|
+
assert ctx2.route["confidence"] >= 0.5, ctx2.route
|
|
166
|
+
|
|
167
|
+
a3 = FakeAgent(); a3.skill_registry = reg
|
|
168
|
+
ctx3 = PipelineContext(agent=a3, task="帮我计算 (12+8)*5",
|
|
169
|
+
context={"_route_skill": {"threshold": 0.5}})
|
|
170
|
+
asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(ctx3))
|
|
171
|
+
assert ctx3.route["target"] == "skill", "route 已选中 skill"
|
|
172
|
+
assert ctx3.result == "done:帮我计算 (12+8)*5#1" and a3.exec_count == 1, \
|
|
173
|
+
"无 skill_adapter → 回落 _execute(fail-safe)"
|
|
174
|
+
|
|
175
|
+
a4 = FakeAgent()
|
|
176
|
+
ctx4 = PipelineContext(agent=a4, task="计算 1+1", context={"_route_skill": True})
|
|
177
|
+
stage_route(ctx4)
|
|
178
|
+
assert ctx4.route["target"] == "self", "无 skill_registry → 回落 self"
|
|
179
|
+
|
|
180
|
+
ctx5 = PipelineContext(agent=a, task="帮我计算 1+1",
|
|
181
|
+
context={"_route_skill": {"threshold": 1.01}})
|
|
182
|
+
stage_route(ctx5)
|
|
183
|
+
assert ctx5.route["target"] == "self", "阈值>1 → 不选,回落 self"
|
|
184
|
+
print(" ✓ 默认关=self;开启(阈值0.5)选中 calculator 但 act 仍 _execute(执行不变);"
|
|
185
|
+
"无registry/高阈值→回落 self")
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def test_act_skill_dispatch_step2():
|
|
189
|
+
print("\n【8】A·Step2:act 按 ctx.route 经 PolicyGate dispatch skill(opt-in;非成功 fail-safe 无副作用)")
|
|
190
|
+
from foxclaw.skills import build_default_registry, SkillAdapter
|
|
191
|
+
reg = build_default_registry()
|
|
192
|
+
adapter = SkillAdapter(reg)
|
|
193
|
+
|
|
194
|
+
def mk_agent():
|
|
195
|
+
a = FakeAgent(); a.skill_registry = reg; a.skill_adapter = adapter
|
|
196
|
+
return a
|
|
197
|
+
|
|
198
|
+
a = mk_agent()
|
|
199
|
+
ctx = PipelineContext(agent=a, task="帮我计算 (12+8)*5",
|
|
200
|
+
context={"_route_skill": {"threshold": 0.5}, "_skill_dispatch": True})
|
|
201
|
+
asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(ctx))
|
|
202
|
+
assert ctx.route["target"] == "skill" and ctx.route["skill_id"] == "calculator", ctx.route
|
|
203
|
+
assert ctx.extra.get("via") == "skill", ctx.extra
|
|
204
|
+
assert ctx.result["output"]["result"] == 100, ctx.result
|
|
205
|
+
assert a.exec_count == 0, "skill 执行了 → 领域 _execute(LLM) 不再被调(首个真改执行)"
|
|
206
|
+
|
|
207
|
+
a2 = mk_agent()
|
|
208
|
+
ctx2 = PipelineContext(agent=a2, task="帮我发送邮件给客户",
|
|
209
|
+
context={"_route_skill": {"threshold": 0.5, "read_only": False},
|
|
210
|
+
"_skill_dispatch": True})
|
|
211
|
+
asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(ctx2))
|
|
212
|
+
assert ctx2.route["skill_id"] == "gmail_send", ctx2.route
|
|
213
|
+
assert ctx2.result["status"] == "needs_confirmation" and ctx2.result["pending"] is True, ctx2.result
|
|
214
|
+
assert a2.exec_count == 0, "未确认的危险 skill → 不执行、不回落 LLM(绝不 fail-open)"
|
|
215
|
+
|
|
216
|
+
a3 = mk_agent()
|
|
217
|
+
ctx3 = PipelineContext(agent=a3, task="帮我计算 (12+8)*5",
|
|
218
|
+
context={"_route_skill": {"threshold": 0.5}, "_skill_dispatch": False})
|
|
219
|
+
asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(ctx3))
|
|
220
|
+
assert ctx3.route["target"] == "skill" and a3.exec_count == 1, "dispatch 关 → 回落 _execute"
|
|
221
|
+
|
|
222
|
+
a4 = FakeAgent(); a4.skill_registry = reg
|
|
223
|
+
ctx4 = PipelineContext(agent=a4, task="帮我计算 1+1",
|
|
224
|
+
context={"_route_skill": {"threshold": 0.5}, "_skill_dispatch": True})
|
|
225
|
+
asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(ctx4))
|
|
226
|
+
assert a4.exec_count == 1, "无 adapter → 回落 _execute"
|
|
227
|
+
|
|
228
|
+
a2b = mk_agent()
|
|
229
|
+
ctx2b = PipelineContext(agent=a2b, task="帮我发送邮件给客户")
|
|
230
|
+
asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(ctx2b))
|
|
231
|
+
assert ctx2b.route["target"] == "self", f"有副作用的技能不该被自动选中: {ctx2b.route}"
|
|
232
|
+
assert a2b.exec_count == 1, "未自动动手 → 由 LLM 作答"
|
|
233
|
+
|
|
234
|
+
class _BoomAdapter:
|
|
235
|
+
def invoke(self, skill, args, confirmed=False, mode="strict"):
|
|
236
|
+
raise RuntimeError("dispatch 内部炸了")
|
|
237
|
+
a5 = FakeAgent(); a5.skill_registry = reg; a5.skill_adapter = _BoomAdapter()
|
|
238
|
+
ctx5 = PipelineContext(agent=a5, task="帮我计算 1+1",
|
|
239
|
+
context={"_route_skill": {"threshold": 0.5}, "_skill_dispatch": True})
|
|
240
|
+
asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(ctx5))
|
|
241
|
+
assert ctx5.result["status"] == "error" and "异常" in ctx5.result["message"], ctx5.result
|
|
242
|
+
assert a5.exec_count == 0, "dispatch 异常 → fail-closed 结构化 error,绝不回落 LLM"
|
|
243
|
+
print(" ✓ 开启:calculator 真执行(=100,不调LLM);发邮件未确认→needs_confirmation零副作用;"
|
|
244
|
+
"dispatch关/无adapter→回落_execute;invoke异常→fail-closed error(不崩不回落LLM)")
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def test_run_task_confirm_suspend_step3():
|
|
248
|
+
print("\n【9】A·Step3:skill needs_confirmation 提升为 run_task 顶层 status + _confirmed 恢复执行")
|
|
249
|
+
import tempfile, shutil
|
|
250
|
+
from foxclaw.agents.base import CognitiveAgent
|
|
251
|
+
from foxclaw.core.thought_chain import ThoughtChain
|
|
252
|
+
from foxclaw.skills import build_default_registry, SkillAdapter, SkillSpec
|
|
253
|
+
|
|
254
|
+
class _FakeHMR:
|
|
255
|
+
def recall(self, query="", agent_id=None, top_k=5, **kw): return []
|
|
256
|
+
def ingest(self, content, **kw): return None
|
|
257
|
+
def get_workspace(self, agent_id):
|
|
258
|
+
return type("W", (), {"active_goal": "", "push_task": lambda s, t: None})()
|
|
259
|
+
def save_workspace(self, agent_id): pass
|
|
260
|
+
|
|
261
|
+
class _Mini(CognitiveAgent):
|
|
262
|
+
AGENT_ID = "mini"
|
|
263
|
+
ALLOWED_TOOL_CATEGORIES = {"read", "write", "network"}
|
|
264
|
+
@property
|
|
265
|
+
def _role_prompt(self): return "test"
|
|
266
|
+
async def _execute(self, task, context, session_id, memories):
|
|
267
|
+
return f"llm:{task}", {"via": "llm"}
|
|
268
|
+
|
|
269
|
+
d = tempfile.mkdtemp(prefix="step3_")
|
|
270
|
+
try:
|
|
271
|
+
reg = build_default_registry()
|
|
272
|
+
saved = []
|
|
273
|
+
reg.register(SkillSpec(
|
|
274
|
+
id="save_note", name="记笔记", description="保存一条笔记", kind="local",
|
|
275
|
+
triggers=["记一下", "记笔记", "保存笔记"], side_effect="write",
|
|
276
|
+
handler=lambda text="": (saved.append(text) or {"saved": text}),
|
|
277
|
+
params={"text": "str"}))
|
|
278
|
+
adapter = SkillAdapter(reg)
|
|
279
|
+
ag = _Mini(hmr=_FakeHMR(), thought_chain=ThoughtChain(db_path=f"{d}/tc.db"),
|
|
280
|
+
data_dir=d, shadow_rounds=5)
|
|
281
|
+
ag.skill_registry = reg; ag.skill_adapter = adapter
|
|
282
|
+
base_ctx = {"_route_skill": {"threshold": 0.4, "read_only": False},
|
|
283
|
+
"_skill_dispatch": True}
|
|
284
|
+
|
|
285
|
+
r1 = asyncio.run(ag.run_task("帮我记一下 买牛奶", dict(base_ctx)))
|
|
286
|
+
assert r1.get("status") == "needs_confirmation", r1
|
|
287
|
+
assert r1.get("pending_action", {}).get("skill_id") == "save_note", r1
|
|
288
|
+
assert saved == [], "未确认 → handler 不执行(零副作用)"
|
|
289
|
+
|
|
290
|
+
r_bad = asyncio.run(ag.run_task("帮我记一下 买牛奶",
|
|
291
|
+
{**base_ctx, "_confirmed": "gmail_send"}))
|
|
292
|
+
assert r_bad.get("status") == "needs_confirmation", r_bad
|
|
293
|
+
assert saved == [], "确认绑定到别的 skill → 不执行本 skill(绑定优先)"
|
|
294
|
+
|
|
295
|
+
r2 = asyncio.run(ag.run_task("帮我记一下 买牛奶",
|
|
296
|
+
{**base_ctx, "_confirmed": "save_note"}))
|
|
297
|
+
assert r2.get("status") != "needs_confirmation", r2
|
|
298
|
+
assert len(saved) == 1, f"确认后 handler 执行(有副作用),saved={saved}"
|
|
299
|
+
|
|
300
|
+
saved.clear()
|
|
301
|
+
r2b = asyncio.run(ag.run_task("帮我记一下 买酸奶", {**base_ctx, "_confirmed": True}))
|
|
302
|
+
assert r2b.get("status") != "needs_confirmation" and len(saved) == 1, (r2b, saved)
|
|
303
|
+
|
|
304
|
+
r3 = asyncio.run(ag.run_task("随便聊聊"))
|
|
305
|
+
assert "status" not in r3 and r3["result"] == "llm:随便聊聊", r3
|
|
306
|
+
print(f" ✓ 未确认→顶层 needs_confirmation 零副作用;_confirmed 重跑→执行(saved={saved});"
|
|
307
|
+
f"LLM 路径无 status(行为不变)")
|
|
308
|
+
finally:
|
|
309
|
+
shutil.rmtree(d, ignore_errors=True)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def test_reflect_retry_optin():
|
|
313
|
+
print("\n【4】reflect opt-in:漂移超阈值 → 有上限重试")
|
|
314
|
+
from foxclaw.pipeline import stage_verify # noqa
|
|
315
|
+
a = FakeAgent(nodes=[_Node("WARN", 0.8)])
|
|
316
|
+
ctx = PipelineContext(agent=a, task="V",
|
|
317
|
+
context={"_reflect": {"max_retries": 2, "drift_threshold": 0.5}})
|
|
318
|
+
asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(ctx))
|
|
319
|
+
assert a.exec_count == 3, f"1 初次 + 2 重试,实得 {a.exec_count}"
|
|
320
|
+
assert ctx.reflect["decision"] == "retry" and ctx.reflect["retries"] == 2, ctx.reflect
|
|
321
|
+
assert ctx.result == "done:V#3", "最终结果=最后一次重试"
|
|
322
|
+
print(f" ✓ 重试到上限:exec={a.exec_count} reflect={ctx.reflect}")
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
class _DynChain:
|
|
326
|
+
"""动态 chain:重试后追加一个低漂移新节点(旧高节点仍在)。用于证明 reflect 用 last_delta 早退。"""
|
|
327
|
+
def __init__(self, agent): self.agent = agent
|
|
328
|
+
def get_chain(self, session_id, agent_id=None):
|
|
329
|
+
if self.agent.exec_count <= 1:
|
|
330
|
+
return [_Node("WARN", 0.8)]
|
|
331
|
+
return [_Node("WARN", 0.8), _Node("PASS", 0.2)]
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def test_reflect_early_exit_on_improve():
|
|
335
|
+
print("\n【5b】深审修复:重试改善后早退(用 last_delta,非累积 max_delta)")
|
|
336
|
+
a = FakeAgent()
|
|
337
|
+
a.thought_chain = _DynChain(a)
|
|
338
|
+
ctx = PipelineContext(agent=a, task="D",
|
|
339
|
+
context={"_reflect": {"max_retries": 3, "drift_threshold": 0.5}})
|
|
340
|
+
asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(ctx))
|
|
341
|
+
assert a.exec_count == 2, f"改善后应只重试 1 次(而非到上限 3),实得 {a.exec_count}"
|
|
342
|
+
assert ctx.reflect["decision"] == "retry_accepted" and ctx.reflect["retries"] == 1, ctx.reflect
|
|
343
|
+
assert ctx.verified["max_delta"] == 0.8 and ctx.verified["last_delta"] == 0.2
|
|
344
|
+
print(f" ✓ 早退:exec={a.exec_count} reflect={ctx.reflect['decision']} "
|
|
345
|
+
f"(max_delta={ctx.verified['max_delta']} 仍高,但 last_delta={ctx.verified['last_delta']} 达标)")
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def test_reflect_failsafe_rollback():
|
|
349
|
+
print("\n【5】reflect fail-safe:重试自身出错 → 回滚到重试前结果")
|
|
350
|
+
a = FakeAgent(nodes=[_Node("WARN", 0.8)], fail_on=2)
|
|
351
|
+
ctx = PipelineContext(agent=a, task="X",
|
|
352
|
+
context={"_reflect": {"max_retries": 2, "drift_threshold": 0.5}})
|
|
353
|
+
asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(ctx))
|
|
354
|
+
assert a.exec_count == 2, "初次 + 1 次失败重试"
|
|
355
|
+
assert ctx.result == "done:X#1", "回滚到重试前的好结果(重试不让结果变差)"
|
|
356
|
+
assert ctx.reflect["decision"] == "retry_reverted" and ctx.reflect["retries"] == 1, ctx.reflect
|
|
357
|
+
print(f" ✓ 重试出错回滚:result={ctx.result!r} reflect={ctx.reflect}(fail-safe)")
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def test_concurrent_ctx_isolation():
|
|
361
|
+
"""【P4 证伪】并发多任务打同一 agent:ctx 是 run_task 内**每任务新建的局部对象**、
|
|
362
|
+
stage **顺序 await**(无 stage 并行),故可变 ctx **并发安全、不串**。
|
|
363
|
+
|
|
364
|
+
范式原则五"不可变状态流转"反对的是"**跨任务共享**可变状态"(工具 A 改一半、工具 B
|
|
365
|
+
读到不一致);PipelineContext 不属于该反模式——它不跨任务共享。本测试用 24 个并发任务
|
|
366
|
+
+ 每个 _execute await 让出控制权(最大化交错、逼出潜在串状态)来证伪。"""
|
|
367
|
+
print("\n【6】P4 证伪:并发多任务 ctx 隔离(可变但不串)")
|
|
368
|
+
|
|
369
|
+
class SlowAgent(FakeAgent):
|
|
370
|
+
async def _execute(self, task, context, session_id, memories):
|
|
371
|
+
await asyncio.sleep(0.005)
|
|
372
|
+
return f"done:{task}", {"task_echo": task}
|
|
373
|
+
|
|
374
|
+
a = SlowAgent()
|
|
375
|
+
|
|
376
|
+
async def run_one(task):
|
|
377
|
+
ctx = PipelineContext(agent=a, task=task, session_id=f"s_{task}")
|
|
378
|
+
await CognitivePipeline(DEFAULT_STAGES).run(ctx)
|
|
379
|
+
return task, ctx
|
|
380
|
+
|
|
381
|
+
async def _main():
|
|
382
|
+
tasks = [f"T{i}" for i in range(24)]
|
|
383
|
+
results = await asyncio.gather(*[run_one(t) for t in tasks])
|
|
384
|
+
for t, ctx in results:
|
|
385
|
+
assert ctx.task == t, f"ctx.task 串了: {ctx.task} != {t}"
|
|
386
|
+
assert ctx.result == f"done:{t}", f"ctx.result 串了: {ctx.result} != done:{t}"
|
|
387
|
+
assert ctx.extra["task_echo"] == t, f"ctx.extra 串了: {ctx.extra}"
|
|
388
|
+
assert ctx.route.get("target") == "self"
|
|
389
|
+
assert ctx.trace == ["perceive", "recall", "plan", "route", "act", "verify", "reflect"]
|
|
390
|
+
return len(results)
|
|
391
|
+
|
|
392
|
+
n = asyncio.run(_main())
|
|
393
|
+
print(f" ✓ {n} 并发任务 ctx 零串——每个 ctx.result/task/extra/trace 都属自己的 task")
|
|
394
|
+
print(" 结论:ctx 每任务新建 + stage 顺序执行 → 可变但并发安全(P4 证实为伪风险)")
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def test_cooperative_interrupt():
|
|
398
|
+
"""【P3】协作式中断:stop_event 置位 → 管线在下一 stage 边界 / reflect 重试前优雅停止。
|
|
399
|
+
opt-in——默认 stop_event=None 时行为逐字不变。覆盖:(a)不置位跑满 (b)起步即停
|
|
400
|
+
(c)中途 stage 边界停 (d)reflect 重试循环停(范式"每轮迭代检查中断"的本地对应)。"""
|
|
401
|
+
import threading
|
|
402
|
+
print("\n【7】P3 协作式中断:stop_event 在 stage 边界 / 重试迭代前优雅停")
|
|
403
|
+
|
|
404
|
+
a = FakeAgent()
|
|
405
|
+
ctx = PipelineContext(agent=a, task="T")
|
|
406
|
+
asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(ctx))
|
|
407
|
+
assert ctx.interrupted is False and len(ctx.trace) == 7, (ctx.interrupted, ctx.trace)
|
|
408
|
+
|
|
409
|
+
ev = threading.Event(); ev.set()
|
|
410
|
+
ctx2 = PipelineContext(agent=FakeAgent(), task="T", stop_event=ev)
|
|
411
|
+
asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(ctx2))
|
|
412
|
+
assert ctx2.interrupted is True and ctx2.trace == [] and ctx2.result is None, \
|
|
413
|
+
(ctx2.interrupted, ctx2.trace, ctx2.result)
|
|
414
|
+
|
|
415
|
+
ev2 = threading.Event()
|
|
416
|
+
def _tripwire(c): ev2.set()
|
|
417
|
+
_tripwire.stage_name = "tripwire"
|
|
418
|
+
stages = DEFAULT_STAGES[:2] + [_tripwire] + DEFAULT_STAGES[2:]
|
|
419
|
+
ctx3 = PipelineContext(agent=FakeAgent(), task="T", stop_event=ev2)
|
|
420
|
+
asyncio.run(CognitivePipeline(stages).run(ctx3))
|
|
421
|
+
assert ctx3.trace == ["perceive", "recall", "tripwire"], ctx3.trace
|
|
422
|
+
assert ctx3.interrupted is True and ctx3.result is None, (ctx3.interrupted, ctx3.result)
|
|
423
|
+
print(f" ✓ 边界中断:无 stop 跑满 7;起步停 0 stage;中途停 trace={ctx3.trace}(后续未跑)")
|
|
424
|
+
|
|
425
|
+
ev3 = threading.Event()
|
|
426
|
+
class _StopOnRetryAgent(FakeAgent):
|
|
427
|
+
def __init__(self, e):
|
|
428
|
+
super().__init__(nodes=[_Node("WARN", 0.8)])
|
|
429
|
+
self._e = e
|
|
430
|
+
async def _execute(self, task, context, session_id, memories):
|
|
431
|
+
self.exec_count += 1
|
|
432
|
+
if self.exec_count == 2:
|
|
433
|
+
self._e.set()
|
|
434
|
+
return f"done:{task}#{self.exec_count}", {"call": self.exec_count}
|
|
435
|
+
a4 = _StopOnRetryAgent(ev3)
|
|
436
|
+
ctx4 = PipelineContext(agent=a4, task="R", stop_event=ev3,
|
|
437
|
+
context={"_reflect": {"max_retries": 5, "drift_threshold": 0.5}})
|
|
438
|
+
asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(ctx4))
|
|
439
|
+
assert a4.exec_count == 2, f"初次 act + 1 次重试后即中断(上限 5 未跑满),实得 {a4.exec_count}"
|
|
440
|
+
assert ctx4.reflect["decision"] == "interrupted" and ctx4.reflect["retries"] == 1, ctx4.reflect
|
|
441
|
+
assert ctx4.interrupted is True
|
|
442
|
+
print(f" ✓ 重试中断:exec={a4.exec_count}(上限5未满) reflect={ctx4.reflect['decision']} "
|
|
443
|
+
f"→ 省下 4 次 LLM 调用")
|
|
444
|
+
print(" 结论:stop_event 在 stage 边界与重试迭代前生效;默认 None 时全部行为逐字不变")
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def test_on_stage_callback():
|
|
448
|
+
"""【8】D1:`pipeline.run(on_stage)` 逐 stage 回调(opt-in);无回调=行为不变;回调异常 fail-safe。"""
|
|
449
|
+
print("\n【8】D1 on_stage 回调:逐 stage 通知 + opt-in + fail-safe")
|
|
450
|
+
seen = []
|
|
451
|
+
ctx = PipelineContext(agent=FakeAgent(), task="T")
|
|
452
|
+
asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(
|
|
453
|
+
ctx, on_stage=lambda name, c: seen.append(name)))
|
|
454
|
+
assert seen == ctx.trace == ["perceive", "recall", "plan", "route", "act", "verify", "reflect"], \
|
|
455
|
+
(seen, ctx.trace)
|
|
456
|
+
ctx2 = PipelineContext(agent=FakeAgent(), task="T")
|
|
457
|
+
asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(ctx2))
|
|
458
|
+
assert len(ctx2.trace) == 7 and ctx2.result == "done:T#1"
|
|
459
|
+
def _boom(name, c):
|
|
460
|
+
raise RuntimeError("回调故障")
|
|
461
|
+
ctx3 = PipelineContext(agent=FakeAgent(), task="T")
|
|
462
|
+
asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(ctx3, on_stage=_boom))
|
|
463
|
+
assert len(ctx3.trace) == 7 and ctx3.result == "done:T#1", "回调故障不拖垮管线"
|
|
464
|
+
print(f" ✓ on_stage 逐 stage 回调({len(seen)} 次)、无回调行为不变、回调异常 fail-safe")
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def main():
|
|
468
|
+
print("=" * 60)
|
|
469
|
+
print("认知管线 · Cognitive Pipeline(v2 ② recall/act/verify/reflect)")
|
|
470
|
+
print("=" * 60)
|
|
471
|
+
test_default_pipeline()
|
|
472
|
+
test_plan_route_stage()
|
|
473
|
+
test_verify_stage()
|
|
474
|
+
test_insertable_stage()
|
|
475
|
+
test_exception_propagates()
|
|
476
|
+
test_stage_events()
|
|
477
|
+
test_route_skill_optin_inert()
|
|
478
|
+
test_act_skill_dispatch_step2()
|
|
479
|
+
test_run_task_confirm_suspend_step3()
|
|
480
|
+
test_reflect_retry_optin()
|
|
481
|
+
test_reflect_early_exit_on_improve()
|
|
482
|
+
test_reflect_failsafe_rollback()
|
|
483
|
+
test_concurrent_ctx_isolation()
|
|
484
|
+
test_cooperative_interrupt()
|
|
485
|
+
test_on_stage_callback()
|
|
486
|
+
print("\n" + "=" * 60)
|
|
487
|
+
print("✓ Step1 验证完成:PipelineContext + 顺序 + 可插桩 + 异常透传 + 并发隔离 + 中断 + on_stage")
|
|
488
|
+
print("=" * 60)
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
if __name__ == "__main__":
|
|
492
|
+
main()
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""计划可见:plan 节点入链 + SSE `{"plan":…}` 事件 + 阻塞返回体带 plan。
|
|
2
|
+
|
|
3
|
+
背景:`stage_plan` 早就算出了计划(steps/flow),但只写 `ctx.plan` 供审计,既不落 thought
|
|
4
|
+
chain 也不送前端,调用方"跑完才知道它打算怎么做"。本组用例锁住三条出口:
|
|
5
|
+
① stage_plan 落 `plan` 类型节点(metadata 带 steps);
|
|
6
|
+
② run_task_stream 在 plan stage 后补发 `{"plan": …}`;
|
|
7
|
+
③ 阻塞路径 `_run_task_core` 的返回体带 `plan`(由 `_plan_payload` 统一提取)。
|
|
8
|
+
均**不改变执行**:act 仍整段交给 LLM,计划不驱动执行。
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import types
|
|
14
|
+
|
|
15
|
+
from foxclaw.agents.base import CognitiveAgent, _plan_payload
|
|
16
|
+
from foxclaw.pipeline.cognitive_pipeline import _record_plan_node, stage_plan
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _ctx(task, agent=None):
|
|
20
|
+
from foxclaw.pipeline import PipelineContext
|
|
21
|
+
return PipelineContext(agent=agent or types.SimpleNamespace(), task=task)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class _ThinkRec:
|
|
25
|
+
def __init__(self):
|
|
26
|
+
self.calls: list[dict] = []
|
|
27
|
+
|
|
28
|
+
def __call__(self, **kwargs):
|
|
29
|
+
self.calls.append(kwargs)
|
|
30
|
+
return None, None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_stage_plan_records_plan_node() -> None:
|
|
34
|
+
rec = _ThinkRec()
|
|
35
|
+
ctx = _ctx("先查资料然后写总结", agent=types.SimpleNamespace(_think=rec))
|
|
36
|
+
stage_plan(ctx)
|
|
37
|
+
assert ctx.plan["steps"], "plan stage 仍应算出步骤"
|
|
38
|
+
plans = [c for c in rec.calls if c.get("node_type") == "plan"]
|
|
39
|
+
assert len(plans) == 1, "应落恰好一条 plan 节点"
|
|
40
|
+
node = plans[0]
|
|
41
|
+
assert node["metadata"]["steps"] == ctx.plan["steps"]
|
|
42
|
+
assert node["metadata"]["flow"] == ctx.plan["flow"]
|
|
43
|
+
assert node["content"].startswith("计划[")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_record_plan_node_never_breaks_execution() -> None:
|
|
47
|
+
_record_plan_node(_ctx("x", agent=None))
|
|
48
|
+
|
|
49
|
+
def _boom(**kwargs):
|
|
50
|
+
raise RuntimeError("harness 挂了")
|
|
51
|
+
|
|
52
|
+
_record_plan_node(_ctx("x", agent=types.SimpleNamespace(_think=_boom)))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_plan_payload_accepts_both_shapes() -> None:
|
|
56
|
+
domain = types.SimpleNamespace(plan={"steps": ["a", "b"], "flow": "sequential",
|
|
57
|
+
"multi_step": True})
|
|
58
|
+
assert _plan_payload(domain) == {"steps": ["a", "b"], "flow": "sequential",
|
|
59
|
+
"multi_step": True}
|
|
60
|
+
sup = types.SimpleNamespace(plan_obj=types.SimpleNamespace(steps=["x", "y"], flow="parallel"))
|
|
61
|
+
assert _plan_payload(sup)["steps"] == ["x", "y"]
|
|
62
|
+
assert _plan_payload(sup)["multi_step"] is True
|
|
63
|
+
one = types.SimpleNamespace(plan_obj=types.SimpleNamespace(steps=["x"]))
|
|
64
|
+
assert _plan_payload(one)["multi_step"] is False
|
|
65
|
+
assert _plan_payload(types.SimpleNamespace(plan=None)) is None
|
|
66
|
+
assert _plan_payload(types.SimpleNamespace()) is None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_stream_emits_plan_event_after_plan_stage() -> None:
|
|
70
|
+
ctx = types.SimpleNamespace(plan={"steps": ["a", "b"], "flow": "sequential",
|
|
71
|
+
"multi_step": True})
|
|
72
|
+
|
|
73
|
+
async def _core(task, context=None, stop_event=None, on_stage=None):
|
|
74
|
+
on_stage("recall", ctx)
|
|
75
|
+
on_stage("plan", ctx)
|
|
76
|
+
return {"result": "done"}
|
|
77
|
+
|
|
78
|
+
fake = types.SimpleNamespace(_run_task_core=_core)
|
|
79
|
+
events = list(CognitiveAgent.run_task_stream(fake, "任务"))
|
|
80
|
+
assert {"stage": "recall"} in events
|
|
81
|
+
assert {"stage": "plan"} in events
|
|
82
|
+
plans = [e for e in events if "plan" in e]
|
|
83
|
+
assert plans == [{"plan": {"steps": ["a", "b"], "flow": "sequential",
|
|
84
|
+
"multi_step": True}}], "plan 事件应紧跟 plan stage 且只发一次"
|
|
85
|
+
assert events.index({"stage": "plan"}) < events.index(plans[0]), "plan 事件必须在 stage 之后"
|
|
86
|
+
assert events[-1] == {"result": {"result": "done"}}, "流式末尾必带 result 事件"
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""
|
|
2
|
+
统一规划原语(P1-2:三个 Planner 合并)测试 —— 全离线。
|
|
3
|
+
|
|
4
|
+
覆盖:
|
|
5
|
+
1. 共享原语:is_negated / split_on / keyword_hits(否定开关)
|
|
6
|
+
2. trigger_route 引擎:否定过滤、打分、兜底、near-ties→needs_composition、确定性平局
|
|
7
|
+
3. 三处委托同一引擎(单一真源):
|
|
8
|
+
· AHS pipeline.route 否定生效(不选被否定的 skill)
|
|
9
|
+
· search QueryPlanner._decompose 用共享拆分
|
|
10
|
+
· 主管关键词命中走 keyword_hits
|
|
11
|
+
4. 结构:pipeline 不再持有本地 _negated/_split_on(已删除,证明去重)
|
|
12
|
+
"""
|
|
13
|
+
import os, sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
os.environ.setdefault("HF_HUB_OFFLINE", "1")
|
|
16
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
17
|
+
|
|
18
|
+
from foxclaw.planning import planner as P
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_primitives():
|
|
22
|
+
print("\n【1】共享原语:否定 / 拆分 / 关键词命中")
|
|
23
|
+
assert P.is_negated("不要计算这个", "计算") is True
|
|
24
|
+
assert P.is_negated("请计算这个", "计算") is False
|
|
25
|
+
assert P.split_on("查北京,查上海、查广州", P.LIST_SEPS) == ["查北京", "查上海", "查广州"]
|
|
26
|
+
assert P.split_on("先做A 然后 做B", P.SPLIT_MARKERS) == ["先做A", "做B"]
|
|
27
|
+
assert P.keyword_hits("分析财务收支", ["财务", "收支", "行程"]) == 2
|
|
28
|
+
assert P.keyword_hits("不要分析财务", ["财务"], negation=True) == 0
|
|
29
|
+
assert P.keyword_hits("不要分析财务", ["财务"], negation=False) == 1
|
|
30
|
+
print(" ✓ is_negated / split_on / keyword_hits(否定开关) 均正确")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_trigger_route_engine():
|
|
34
|
+
print("\n【2】trigger_route 引擎:否定/打分/兜底/near-ties/确定性")
|
|
35
|
+
C = P.ScoredCandidate
|
|
36
|
+
cands = [
|
|
37
|
+
C("calculator", ["计算", "算"], "四则运算 计算器"),
|
|
38
|
+
C("web", ["搜索", "查"], "联网 搜索 网页"),
|
|
39
|
+
C("email", ["发邮件", "email"], "发送 邮件"),
|
|
40
|
+
]
|
|
41
|
+
r = P.trigger_route("不要计算一下", cands)
|
|
42
|
+
assert r.primary != "calculator", f"否定应排除 calculator,实得 {r.primary}"
|
|
43
|
+
r2 = P.trigger_route("帮我搜索北京", cands)
|
|
44
|
+
assert r2.primary == "web" and r2.confidence >= 0.15
|
|
45
|
+
r3 = P.trigger_route("xyzzy 无关内容", cands)
|
|
46
|
+
assert r3.primary == cands[-1].id and r3.confidence < 0.15
|
|
47
|
+
assert P.trigger_route("x", []).primary is None
|
|
48
|
+
print(f" ✓ 否定排除 / 命中选中 / 兜底(={r3.primary}) / 空候选")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_three_sites_delegate():
|
|
52
|
+
print("\n【3】三处委托同一引擎(单一真源)")
|
|
53
|
+
from foxclaw.skills import build_default_registry
|
|
54
|
+
from foxclaw.skills.pipeline import route, plan
|
|
55
|
+
reg = build_default_registry()
|
|
56
|
+
assert route("不要计算", reg).primary != "calculator", "AHS route 否定生效"
|
|
57
|
+
assert len(plan("先算 5+5 然后再算 9*9", reg).tasks) == 2, "AHS plan 拆解"
|
|
58
|
+
from foxclaw.search.planner import QueryPlanner
|
|
59
|
+
sub = QueryPlanner()._decompose("查A,查B、查C")
|
|
60
|
+
assert len(sub) == 3, "search 拆解走共享原语"
|
|
61
|
+
assert P.keyword_hits("买基金理财", ["理财", "基金", "收支"]) == 2
|
|
62
|
+
print(" ✓ AHS / search / 主管 三处均由 foxclaw.planning 单一真源驱动")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_dedup_structural():
|
|
66
|
+
print("\n【4】结构去重:pipeline 不再持有本地 _negated/_split_on")
|
|
67
|
+
import foxclaw.skills.pipeline as pipe
|
|
68
|
+
assert not hasattr(pipe, "_negated"), "pipeline 应删除本地 _negated(改用共享)"
|
|
69
|
+
assert not hasattr(pipe, "_split_on"), "pipeline 应删除本地 _split_on(改用共享)"
|
|
70
|
+
assert not hasattr(pipe, "_NEGATIONS"), "pipeline 应删除本地 _NEGATIONS 常量"
|
|
71
|
+
print(" ✓ pipeline 的重复否定/拆分实现已删除,收敛到 foxclaw.planning")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def main():
|
|
75
|
+
print("=" * 60)
|
|
76
|
+
print("统一规划原语 · 三个 Planner 合并(P1-2)")
|
|
77
|
+
print("=" * 60)
|
|
78
|
+
test_primitives()
|
|
79
|
+
test_trigger_route_engine()
|
|
80
|
+
test_three_sites_delegate()
|
|
81
|
+
test_dedup_structural()
|
|
82
|
+
print("\n" + "=" * 60)
|
|
83
|
+
print("✓ P1-2 验证完成:一个规划引擎,主管/AHS/search 三处委托")
|
|
84
|
+
print("=" * 60)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
if __name__ == "__main__":
|
|
88
|
+
main()
|