foxclaw 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -29
- package/bin/foxclaw.cjs +51 -0
- package/index.js +11 -10
- package/package.json +46 -38
- package/python/.dockerignore +35 -0
- package/python/.env.example +165 -0
- package/python/.gitattributes +46 -0
- package/python/.github/workflows/ci.yml +122 -0
- package/python/.github/workflows/release.yml +183 -0
- package/python/Dockerfile +64 -0
- package/python/LICENSE +21 -0
- package/python/MANIFEST.in +14 -0
- package/python/README.md +192 -0
- package/python/README_CN.md +324 -0
- package/python/bench/bm25_audit.py +199 -0
- package/python/bench/bm25_regression.py +178 -0
- package/python/bench/bm25_stress.py +159 -0
- package/python/bench/cd_audit.py +238 -0
- package/python/bench/chaos.py +142 -0
- package/python/bench/concurrency.py +103 -0
- package/python/bench/e2e_real_models.py +95 -0
- package/python/bench/llm_retry_test.py +248 -0
- package/python/bench/load_test.py +196 -0
- package/python/bench/vector_ivf_upgrade.py +134 -0
- package/python/docs/CHANNELS.md +195 -0
- package/python/docs/GOVERNANCE.md +164 -0
- package/python/docs/RELEASE.md +109 -0
- package/python/docs/RUN.md +75 -0
- package/python/docs/RUN_CN.md +121 -0
- package/python/docs//351/205/215/347/275/256/346/214/207/345/215/227.md +152 -0
- package/python/evals/__init__.py +0 -0
- package/python/evals/governance/README.md +61 -0
- package/python/evals/governance/__init__.py +0 -0
- package/python/evals/governance/cases.py +152 -0
- package/python/evals/governance/compare_embeddings.py +94 -0
- package/python/evals/governance/convergence.svg +11 -0
- package/python/evals/governance/report.json +371 -0
- package/python/evals/governance/run_evals.py +400 -0
- package/python/examples/governance_middleware_demo.py +77 -0
- package/python/examples/langgraph_governance_demo.py +108 -0
- package/python/examples/sample_itinerary.csv +6 -0
- package/python/examples/sample_knowledge.csv +9 -0
- package/python/examples/sample_transactions.csv +14 -0
- package/python/foxclaw/__init__.py +526 -0
- package/python/foxclaw/agents/__init__.py +12 -0
- package/python/foxclaw/agents/agents.py +35 -0
- package/python/foxclaw/agents/base.py +865 -0
- package/python/foxclaw/agents/impl/__init__.py +1 -0
- package/python/foxclaw/agents/impl/_helpers.py +78 -0
- package/python/foxclaw/agents/impl/finance.py +248 -0
- package/python/foxclaw/agents/impl/office.py +367 -0
- package/python/foxclaw/agents/impl/supervisor.py +659 -0
- package/python/foxclaw/agents/mixins/_context.py +151 -0
- package/python/foxclaw/agents/mixins/augment.py +149 -0
- package/python/foxclaw/agents/mixins/llm.py +66 -0
- package/python/foxclaw/agents/mixins/memory.py +35 -0
- package/python/foxclaw/agents/mixins/preference.py +81 -0
- package/python/foxclaw/agents/mixins/prompt.py +77 -0
- package/python/foxclaw/agents/mixins/skillbox.py +58 -0
- package/python/foxclaw/agents/mixins/tool_exec.py +443 -0
- package/python/foxclaw/api/__init__.py +283 -0
- package/python/foxclaw/api/app.py +123 -0
- package/python/foxclaw/api/middleware.py +317 -0
- package/python/foxclaw/api/routes/ahs.py +417 -0
- package/python/foxclaw/api/routes/attachments.py +117 -0
- package/python/foxclaw/api/routes/browser.py +39 -0
- package/python/foxclaw/api/routes/domain.py +314 -0
- package/python/foxclaw/api/routes/knowledge.py +163 -0
- package/python/foxclaw/api/routes/llm_admin.py +461 -0
- package/python/foxclaw/api/routes/memory.py +89 -0
- package/python/foxclaw/api/routes/orchestration.py +41 -0
- package/python/foxclaw/api/routes/recovery.py +43 -0
- package/python/foxclaw/api/routes/review.py +275 -0
- package/python/foxclaw/api/routes/search.py +37 -0
- package/python/foxclaw/api/routes/system.py +327 -0
- package/python/foxclaw/api/routes/task.py +281 -0
- package/python/foxclaw/api/routes/ws.py +68 -0
- package/python/foxclaw/api/spine.py +55 -0
- package/python/foxclaw/browser/__init__.py +13 -0
- package/python/foxclaw/browser/action/__init__.py +1 -0
- package/python/foxclaw/browser/action/action_executor.py +54 -0
- package/python/foxclaw/browser/action/action_types.py +45 -0
- package/python/foxclaw/browser/action/action_verifier.py +68 -0
- package/python/foxclaw/browser/action/url_policy.py +147 -0
- package/python/foxclaw/browser/adapter/__init__.py +1 -0
- package/python/foxclaw/browser/adapter/base_adapter.py +23 -0
- package/python/foxclaw/browser/adapter/web_adapter.py +46 -0
- package/python/foxclaw/browser/driver/__init__.py +1 -0
- package/python/foxclaw/browser/driver/base_driver.py +61 -0
- package/python/foxclaw/browser/driver/playwright_driver.py +166 -0
- package/python/foxclaw/browser/perception/__init__.py +1 -0
- package/python/foxclaw/browser/perception/dom_extractor.py +43 -0
- package/python/foxclaw/browser/perception/element_indexer.py +57 -0
- package/python/foxclaw/browser/perception/semantic_reducer.py +33 -0
- package/python/foxclaw/browser/perception/snapshot.py +29 -0
- package/python/foxclaw/browser/service.py +148 -0
- package/python/foxclaw/browser/session/__init__.py +1 -0
- package/python/foxclaw/browser/session/session_manager.py +57 -0
- package/python/foxclaw/capability/__init__.py +12 -0
- package/python/foxclaw/capability/embedding_service.py +67 -0
- package/python/foxclaw/channels/__init__.py +140 -0
- package/python/foxclaw/channels/discord.py +184 -0
- package/python/foxclaw/channels/dispatcher.py +204 -0
- package/python/foxclaw/channels/feishu.py +225 -0
- package/python/foxclaw/channels/openclaw_bridge.py +156 -0
- package/python/foxclaw/channels/signing.py +187 -0
- package/python/foxclaw/channels/telegram.py +121 -0
- package/python/foxclaw/channels/weixin.py +225 -0
- package/python/foxclaw/cli.py +154 -0
- package/python/foxclaw/cognitive/__init__.py +4 -0
- package/python/foxclaw/cognitive/reasoner.py +82 -0
- package/python/foxclaw/cognitive/state.py +147 -0
- package/python/foxclaw/core/__init__.py +9 -0
- package/python/foxclaw/core/audit_chain.py +133 -0
- package/python/foxclaw/core/audit_sink.py +100 -0
- package/python/foxclaw/core/compaction.py +283 -0
- package/python/foxclaw/core/config.py +65 -0
- package/python/foxclaw/core/confirm_nonce.py +157 -0
- package/python/foxclaw/core/dispatch.py +27 -0
- package/python/foxclaw/core/envfile.py +48 -0
- package/python/foxclaw/core/failure_policy.py +151 -0
- package/python/foxclaw/core/gateway.py +85 -0
- package/python/foxclaw/core/governance_audit.py +175 -0
- package/python/foxclaw/core/idempotency.py +289 -0
- package/python/foxclaw/core/injection_guard.py +221 -0
- package/python/foxclaw/core/memory_adapter.py +516 -0
- package/python/foxclaw/core/metrics.py +89 -0
- package/python/foxclaw/core/model_router.py +1097 -0
- package/python/foxclaw/core/paths.py +103 -0
- package/python/foxclaw/core/recovery.py +362 -0
- package/python/foxclaw/core/scheduler.py +144 -0
- package/python/foxclaw/core/secrets.py +134 -0
- package/python/foxclaw/core/sessions.py +157 -0
- package/python/foxclaw/core/settings.py +430 -0
- package/python/foxclaw/core/shared_write.py +198 -0
- package/python/foxclaw/core/steering_queue.py +153 -0
- package/python/foxclaw/core/system_bus.py +84 -0
- package/python/foxclaw/core/thought_chain.py +318 -0
- package/python/foxclaw/core/token_meter.py +235 -0
- package/python/foxclaw/core/tools.py +172 -0
- package/python/foxclaw/core/tracing.py +390 -0
- package/python/foxclaw/core/truth_pipeline.py +114 -0
- package/python/foxclaw/core/vec_recall_adapter.py +503 -0
- package/python/foxclaw/core/verify.py +181 -0
- package/python/foxclaw/core/web_search.py +242 -0
- package/python/foxclaw/finance/__init__.py +15 -0
- package/python/foxclaw/finance/evidence.py +153 -0
- package/python/foxclaw/finance/fx.py +100 -0
- package/python/foxclaw/finance/graph.py +188 -0
- package/python/foxclaw/finance/models.py +150 -0
- package/python/foxclaw/finance/nlparse.py +575 -0
- package/python/foxclaw/finance/policy.py +329 -0
- package/python/foxclaw/finance/reasoner.py +129 -0
- package/python/foxclaw/finance/reasoning.py +340 -0
- package/python/foxclaw/finance/reconcile.py +180 -0
- package/python/foxclaw/finance/runtime.py +527 -0
- package/python/foxclaw/governance/__init__.py +24 -0
- package/python/foxclaw/governance/middleware.py +184 -0
- package/python/foxclaw/harness/__init__.py +17 -0
- package/python/foxclaw/harness/_base.py +72 -0
- package/python/foxclaw/harness/_policy.py +359 -0
- package/python/foxclaw/harness/drift.py +89 -0
- package/python/foxclaw/harness/event_bus.py +31 -0
- package/python/foxclaw/harness/harness.py +446 -0
- package/python/foxclaw/harness/loop_detection.py +152 -0
- package/python/foxclaw/itinerary/__init__.py +10 -0
- package/python/foxclaw/itinerary/amap_client.py +117 -0
- package/python/foxclaw/itinerary/models.py +80 -0
- package/python/foxclaw/itinerary/reasoner.py +101 -0
- package/python/foxclaw/itinerary/reasoning.py +174 -0
- package/python/foxclaw/itinerary/runtime.py +148 -0
- package/python/foxclaw/kernel/__init__.py +13 -0
- package/python/foxclaw/kernel/capabilities.py +73 -0
- package/python/foxclaw/kernel/policy_gate.py +114 -0
- package/python/foxclaw/kernel/process_manager.py +205 -0
- package/python/foxclaw/kernel/scheduler.py +131 -0
- package/python/foxclaw/kernel/sentinel.py +453 -0
- package/python/foxclaw/kernel/sentinel_client.py +246 -0
- package/python/foxclaw/kernel/spine.py +97 -0
- package/python/foxclaw/knowledge/__init__.py +376 -0
- package/python/foxclaw/knowledge/truth_crystallizer.py +92 -0
- package/python/foxclaw/learning/__init__.py +12 -0
- package/python/foxclaw/learning/knowledge_graph.py +149 -0
- package/python/foxclaw/learning/models.py +93 -0
- package/python/foxclaw/learning/reasoner.py +70 -0
- package/python/foxclaw/learning/runtime.py +363 -0
- package/python/foxclaw/learning/spaced_repetition.py +96 -0
- package/python/foxclaw/mcp_server.py +189 -0
- package/python/foxclaw/orchestration/__init__.py +184 -0
- package/python/foxclaw/pipeline/__init__.py +23 -0
- package/python/foxclaw/pipeline/cognitive_pipeline.py +481 -0
- package/python/foxclaw/pipeline/supervisor_stages.py +230 -0
- package/python/foxclaw/planning/__init__.py +17 -0
- package/python/foxclaw/planning/llm_planner.py +232 -0
- package/python/foxclaw/planning/planner.py +99 -0
- package/python/foxclaw/search/__init__.py +24 -0
- package/python/foxclaw/search/agent_port.py +194 -0
- package/python/foxclaw/search/evidence.py +113 -0
- package/python/foxclaw/search/external_search.py +50 -0
- package/python/foxclaw/search/hkc_read_port.py +60 -0
- package/python/foxclaw/search/internal_search.py +73 -0
- package/python/foxclaw/search/models.py +129 -0
- package/python/foxclaw/search/planner.py +44 -0
- package/python/foxclaw/search/retriever.py +39 -0
- package/python/foxclaw/search/runtime.py +134 -0
- package/python/foxclaw/search/strategy.py +44 -0
- package/python/foxclaw/skills/__init__.py +613 -0
- package/python/foxclaw/skills/hooks_spec.py +73 -0
- package/python/foxclaw/skills/lifecycle.py +278 -0
- package/python/foxclaw/skills/markdown_skill.py +174 -0
- package/python/foxclaw/skills/markdown_store.py +39 -0
- package/python/foxclaw/skills/mcp_client.py +190 -0
- package/python/foxclaw/skills/mcp_store.py +43 -0
- package/python/foxclaw/skills/permission.py +180 -0
- package/python/foxclaw/skills/pipeline.py +386 -0
- package/python/foxclaw/skills/policy.py +80 -0
- package/python/foxclaw/skills/review.py +72 -0
- package/python/foxclaw/skills/store.py +57 -0
- package/python/frontend/assets/core_bg.jpg +0 -0
- package/python/frontend/deck/auth.js +32 -0
- package/python/frontend/deck/chat_model_picker.js +537 -0
- package/python/frontend/deck/domain.js +296 -0
- package/python/frontend/deck/main.js +1950 -0
- package/python/frontend/deck/markdown.js +48 -0
- package/python/frontend/deck/settings.js +324 -0
- package/python/frontend/deck/state.js +6 -0
- package/python/frontend/deck/util.js +6 -0
- package/python/frontend/deck/visuals.js +212 -0
- package/python/frontend/deck.css +855 -0
- package/python/frontend/deck.html +686 -0
- package/python/frontend/deck_v2.html +341 -0
- package/python/frontend/planet/index.html +266 -0
- package/python/frontend/planet/planet.css +579 -0
- package/python/frontend/planet/planet.js +1637 -0
- package/python/frontend/planet/vendor/three.core.min.js +6 -0
- package/python/frontend/planet/vendor/three.module.min.js +6 -0
- package/python/frontend/v3/assets/index-BD7VPbMV.js +33 -0
- package/python/frontend/v3/assets/index-CAvnYWGH.css +1 -0
- package/python/frontend/v3/index.html +15 -0
- package/python/frontend/widget_runtime.js +295 -0
- package/python/frontend/widgets.js +696 -0
- package/python/frontend_src/index.html +14 -0
- package/python/frontend_src/package-lock.json +1376 -0
- package/python/frontend_src/package.json +19 -0
- package/python/frontend_src/src/App.vue +62 -0
- package/python/frontend_src/src/api.js +89 -0
- package/python/frontend_src/src/components/ChatDock.vue +377 -0
- package/python/frontend_src/src/components/ComposerMenu.vue +75 -0
- package/python/frontend_src/src/components/DocViewer.vue +171 -0
- package/python/frontend_src/src/components/Inspector.vue +241 -0
- package/python/frontend_src/src/components/LeftRail.vue +394 -0
- package/python/frontend_src/src/components/MenuPopover.vue +130 -0
- package/python/frontend_src/src/components/MessageStream.vue +401 -0
- package/python/frontend_src/src/components/ModelPicker.vue +355 -0
- package/python/frontend_src/src/components/QuickIngest.vue +118 -0
- package/python/frontend_src/src/components/Stage.vue +226 -0
- package/python/frontend_src/src/components/ToolMenu.vue +144 -0
- package/python/frontend_src/src/components/TopBar.vue +90 -0
- package/python/frontend_src/src/components/panels/ActivityPanel.vue +84 -0
- package/python/frontend_src/src/components/panels/AgentsPanel.vue +86 -0
- package/python/frontend_src/src/components/panels/KnowledgePanel.vue +441 -0
- package/python/frontend_src/src/components/panels/MemoryPanel.vue +118 -0
- package/python/frontend_src/src/components/panels/PanelBox.vue +51 -0
- package/python/frontend_src/src/components/panels/ReviewPanel.vue +308 -0
- package/python/frontend_src/src/components/panels/SettingsPanel.vue +254 -0
- package/python/frontend_src/src/components/panels/TasksPanel.vue +74 -0
- package/python/frontend_src/src/components/panels/ToolsPanel.vue +640 -0
- package/python/frontend_src/src/components/panels/UsagePanel.vue +105 -0
- package/python/frontend_src/src/lib/fmt.js +48 -0
- package/python/frontend_src/src/lib/md.js +204 -0
- package/python/frontend_src/src/main.js +7 -0
- package/python/frontend_src/src/stores/app.js +106 -0
- package/python/frontend_src/src/stores/chat.js +484 -0
- package/python/frontend_src/src/stores/model.js +104 -0
- package/python/frontend_src/src/stores/panels.js +330 -0
- package/python/frontend_src/src/styles/base.css +138 -0
- package/python/frontend_src/src/styles/panel.css +117 -0
- package/python/frontend_src/src/styles/tokens.css +78 -0
- package/python/frontend_src/vite.config.js +46 -0
- package/python/pyproject.toml +265 -0
- package/python/skills/README.md +123 -0
- package/python/skills/execution-contract/SKILL.md +30 -0
- package/python/skills/general-assistant/SKILL.md +34 -0
- package/python/skills/planner/SKILL.md +33 -0
- package/python/skills/schedule-itinerary/SKILL.md +35 -0
- package/python/skills/skill-composition/SKILL.md +31 -0
- package/python/skills/skill-guard/SKILL.md +67 -0
- package/python/skills/skill-memory/SKILL.md +50 -0
- package/python/skills/skill-router/SKILL.md +35 -0
- package/python/tests/conftest.py +76 -0
- package/python/tests/run_all.py +263 -0
- package/python/tests/test_agent_lifecycle.py +360 -0
- package/python/tests/test_ahs_p2.py +414 -0
- package/python/tests/test_anthropic_tools.py +206 -0
- package/python/tests/test_api.py +87 -0
- package/python/tests/test_api_auth.py +83 -0
- package/python/tests/test_api_host_guard.py +69 -0
- package/python/tests/test_attachments.py +167 -0
- package/python/tests/test_audit_chain.py +121 -0
- package/python/tests/test_audit_sink.py +136 -0
- package/python/tests/test_browser.py +153 -0
- package/python/tests/test_browser_request_guard.py +184 -0
- package/python/tests/test_capabilities.py +158 -0
- package/python/tests/test_channel_signing.py +192 -0
- package/python/tests/test_channel_trust.py +109 -0
- package/python/tests/test_channels.py +184 -0
- package/python/tests/test_cli.py +46 -0
- package/python/tests/test_compaction_toolpair.py +119 -0
- package/python/tests/test_config_layering.py +65 -0
- package/python/tests/test_confirm_nonce.py +197 -0
- package/python/tests/test_context_compaction_wiring.py +130 -0
- package/python/tests/test_dispatch.py +95 -0
- package/python/tests/test_domain_evals.py +403 -0
- package/python/tests/test_domain_import.py +78 -0
- package/python/tests/test_domain_reasoners.py +148 -0
- package/python/tests/test_e2e.py +178 -0
- package/python/tests/test_emb_cache.py +107 -0
- package/python/tests/test_embedding_singleton.py +89 -0
- package/python/tests/test_envfile.py +83 -0
- package/python/tests/test_evals_smoke.py +114 -0
- package/python/tests/test_event_spine.py +182 -0
- package/python/tests/test_evidence_semantic.py +101 -0
- package/python/tests/test_evidence_sse.py +97 -0
- package/python/tests/test_file_ingest.py +146 -0
- package/python/tests/test_finance.py +153 -0
- package/python/tests/test_finance_ledger.py +252 -0
- package/python/tests/test_finance_persist.py +178 -0
- package/python/tests/test_gateway.py +144 -0
- package/python/tests/test_governance.py +468 -0
- package/python/tests/test_governance_audit.py +359 -0
- package/python/tests/test_governance_middleware.py +143 -0
- package/python/tests/test_hkc.py +286 -0
- package/python/tests/test_hkc_index_build_manager.py +339 -0
- package/python/tests/test_hkc_manifest_reuse.py +317 -0
- package/python/tests/test_hkc_search_event_sync.py +533 -0
- package/python/tests/test_import_path_guard.py +161 -0
- package/python/tests/test_injection_guard.py +211 -0
- package/python/tests/test_integration.py +141 -0
- package/python/tests/test_itinerary.py +103 -0
- package/python/tests/test_kee_semantic.py +89 -0
- package/python/tests/test_kernel.py +222 -0
- package/python/tests/test_learning.py +110 -0
- package/python/tests/test_llm_admin.py +160 -0
- package/python/tests/test_llm_planner.py +366 -0
- package/python/tests/test_markdown_skills.py +73 -0
- package/python/tests/test_markdown_skills_openclaw.py +71 -0
- package/python/tests/test_mcp_connectors.py +109 -0
- package/python/tests/test_mcp_guardian_discovery.py +91 -0
- package/python/tests/test_mcp_no_memory.py +43 -0
- package/python/tests/test_mcp_policy_gate.py +77 -0
- package/python/tests/test_mcp_server.py +95 -0
- package/python/tests/test_memory_recall.py +95 -0
- package/python/tests/test_metrics.py +66 -0
- package/python/tests/test_multi_chain.py +102 -0
- package/python/tests/test_multimodal.py +135 -0
- package/python/tests/test_negotiation.py +146 -0
- package/python/tests/test_orchestration.py +119 -0
- package/python/tests/test_orchestration_decoupled.py +109 -0
- package/python/tests/test_p3_p5_hardening.py +338 -0
- package/python/tests/test_parallel_dispatch.py +215 -0
- package/python/tests/test_pipeline.py +533 -0
- package/python/tests/test_plan_visible.py +86 -0
- package/python/tests/test_planner.py +95 -0
- package/python/tests/test_policy_gate.py +109 -0
- package/python/tests/test_reasoner.py +185 -0
- package/python/tests/test_release_check.py +263 -0
- package/python/tests/test_resume_checkpoint.py +156 -0
- package/python/tests/test_routing.py +95 -0
- package/python/tests/test_scheduler.py +141 -0
- package/python/tests/test_search.py +204 -0
- package/python/tests/test_secrets.py +159 -0
- package/python/tests/test_semantic.py +78 -0
- package/python/tests/test_sessions.py +91 -0
- package/python/tests/test_settings.py +281 -0
- package/python/tests/test_shadow_learning.py +330 -0
- package/python/tests/test_shared_write.py +207 -0
- package/python/tests/test_skill_taint_gate.py +154 -0
- package/python/tests/test_skills.py +133 -0
- package/python/tests/test_stats_and_events.py +165 -0
- package/python/tests/test_streaming.py +60 -0
- package/python/tests/test_supervisor_pipeline.py +177 -0
- package/python/tests/test_supervisor_triage.py +108 -0
- package/python/tests/test_taint.py +117 -0
- package/python/tests/test_task_stream.py +85 -0
- package/python/tests/test_thinking_sse.py +67 -0
- package/python/tests/test_thinking_stream.py +76 -0
- package/python/tests/test_tool_center.py +129 -0
- package/python/tests/test_tool_parallel.py +113 -0
- package/python/tests/test_tool_params.py +103 -0
- package/python/tests/test_tool_use.py +242 -0
- package/python/tests/test_toplevel_concurrency.py +117 -0
- package/python/tests/test_tracing.py +290 -0
- package/python/tests/test_truth_crystallizer.py +127 -0
- package/python/tests/test_truth_flow.py +93 -0
- package/python/tests/test_url_policy.py +53 -0
- package/python/tests/test_verify_evidence.py +225 -0
- package/python/tests/test_version_sync.py +71 -0
- package/python/tests/test_web_trigger.py +99 -0
- package/python/tools/check_faiss_type.py +31 -0
- package/python/tools/check_log.ps1 +46 -0
- package/python/tools/ivf_status.py +72 -0
- package/python/tools/mypy_baseline.txt +5 -0
- package/python/tools/mypy_ratchet.py +97 -0
- package/python/tools/pack.py +178 -0
- package/python/tools/release_check.py +349 -0
- package/python/tools/restart_foxclaw.ps1 +74 -0
- package/python/tools/shared_write_audit.py +204 -0
- package/python/tools/size_baseline.txt +28 -0
- package/python/tools/size_check.py +102 -0
- package/python/tools/verify_env.bat +2 -0
- package/python/tools/verify_env.py +25 -0
- package/python/vendor/HMR_LICENSE +37 -0
- package/python/vendor/HMR_README.md +65 -0
- package/python/vendor/hkc/LICENSE +37 -0
- package/python/vendor/hkc/hkc_ace/__init__.py +0 -0
- package/python/vendor/hkc/hkc_ace/ace.py +457 -0
- package/python/vendor/hkc/hkc_ace/skill_taxonomy.json +465 -0
- package/python/vendor/hkc/hkc_api/__init__.py +0 -0
- package/python/vendor/hkc/hkc_api/container.py +416 -0
- package/python/vendor/hkc/hkc_api/ingest_jobs.py +103 -0
- package/python/vendor/hkc/hkc_api/main.py +157 -0
- package/python/vendor/hkc/hkc_api/routes/__init__.py +0 -0
- package/python/vendor/hkc/hkc_api/routes/ability.py +151 -0
- package/python/vendor/hkc/hkc_api/routes/config.py +51 -0
- package/python/vendor/hkc/hkc_api/routes/knowledge.py +630 -0
- package/python/vendor/hkc/hkc_api/routes/search.py +88 -0
- package/python/vendor/hkc/hkc_api/schemas.py +153 -0
- package/python/vendor/hkc/hkc_api/synthesis.py +179 -0
- package/python/vendor/hkc/hkc_core/__init__.py +0 -0
- package/python/vendor/hkc/hkc_core/graph/__init__.py +0 -0
- package/python/vendor/hkc/hkc_core/graph/base.py +188 -0
- package/python/vendor/hkc/hkc_core/graph/sqlite_store.py +789 -0
- package/python/vendor/hkc/hkc_core/models/__init__.py +0 -0
- package/python/vendor/hkc/hkc_core/models/enums.py +65 -0
- package/python/vendor/hkc/hkc_core/models/ku.py +307 -0
- package/python/vendor/hkc/hkc_core/utils/__init__.py +0 -0
- package/python/vendor/hkc/hkc_core/utils/id_gen.py +113 -0
- package/python/vendor/hkc/hkc_crystallizer/__init__.py +44 -0
- package/python/vendor/hkc/hkc_crystallizer/adapters.py +94 -0
- package/python/vendor/hkc/hkc_crystallizer/candidate.py +145 -0
- package/python/vendor/hkc/hkc_crystallizer/crystallizer.py +179 -0
- package/python/vendor/hkc/hkc_crystallizer/ingress.py +90 -0
- package/python/vendor/hkc/hkc_kde/__init__.py +0 -0
- package/python/vendor/hkc/hkc_kde/chunker.py +197 -0
- package/python/vendor/hkc/hkc_kde/extractor.py +623 -0
- package/python/vendor/hkc/hkc_kde/kde.py +298 -0
- package/python/vendor/hkc/hkc_kde/loaders/__init__.py +0 -0
- package/python/vendor/hkc/hkc_kde/loaders/ebook_loader.py +153 -0
- package/python/vendor/hkc/hkc_kde/loaders/office_loader.py +194 -0
- package/python/vendor/hkc/hkc_kde/loaders/pdf_loader.py +162 -0
- package/python/vendor/hkc/hkc_kde/loaders/text_loader.py +157 -0
- package/python/vendor/hkc/hkc_kde/loaders/web_loader.py +91 -0
- package/python/vendor/hkc/hkc_kde/models.py +74 -0
- package/python/vendor/hkc/hkc_kde/packager.py +329 -0
- package/python/vendor/hkc/hkc_kde/relation_disc.py +151 -0
- package/python/vendor/hkc/hkc_kee/__init__.py +0 -0
- package/python/vendor/hkc/hkc_kee/dedup.py +227 -0
- package/python/vendor/hkc/hkc_kee/fingerprint.py +106 -0
- package/python/vendor/hkc/hkc_kee/kee.py +541 -0
- package/python/vendor/hkc/hkc_kep/__init__.py +0 -0
- package/python/vendor/hkc/hkc_kep/event_bus.py +205 -0
- package/python/vendor/hkc/hkc_sdk/__init__.py +63 -0
- package/python/vendor/hkc/hkc_sdk/base.py +151 -0
- package/python/vendor/hkc/hkc_sdk/direct_client.py +153 -0
- package/python/vendor/hkc/hkc_sdk/exceptions.py +39 -0
- package/python/vendor/hkc/hkc_sdk/http_client.py +166 -0
- package/python/vendor/hkc/hkc_sdk/models.py +171 -0
- package/python/vendor/hkc/hkc_search/__init__.py +0 -0
- package/python/vendor/hkc/hkc_search/bm25.py +325 -0
- package/python/vendor/hkc/hkc_search/embedding_backends.py +374 -0
- package/python/vendor/hkc/hkc_search/graph_search.py +132 -0
- package/python/vendor/hkc/hkc_search/hybrid.py +331 -0
- package/python/vendor/hkc/hkc_search/index_build_manager.py +378 -0
- package/python/vendor/hkc/hkc_search/vector_search.py +484 -0
- package/python/vendor/hmr/__init__.py +0 -0
- package/python/vendor/hmr/core/__init__.py +0 -0
- package/python/vendor/hmr/core/hmr.py +684 -0
- package/python/vendor/hmr/core/models.py +310 -0
- package/python/vendor/hmr/engines/__init__.py +0 -0
- package/python/vendor/hmr/engines/evolution.py +710 -0
- package/python/vendor/hmr/engines/jit_compiler.py +472 -0
- package/python/vendor/hmr/engines/lifecycle.py +264 -0
- package/python/vendor/hmr/engines/policy.py +625 -0
- package/python/vendor/hmr/engines/recall.py +260 -0
- package/python/vendor/hmr/engines/runtime_state.py +158 -0
- package/python/vendor/hmr/engines/scheduler.py +323 -0
- package/python/vendor/hmr/engines/semantic.py +150 -0
- package/python/vendor/hmr/engines/temporal.py +261 -0
- package/python/vendor/hmr/engines/thought_chain.py +635 -0
- package/python/vendor/hmr/graph/__init__.py +0 -0
- package/python/vendor/hmr/graph/cwg.py +338 -0
- package/python/vendor/hmr/graph/memory_graph.py +530 -0
- package/python/vendor/hmr/storage/__init__.py +0 -0
- package/python/vendor/hmr/storage/memory_fs.py +300 -0
- package/python/vendor/hmr/storage/vector_store.py +520 -0
|
@@ -0,0 +1,533 @@
|
|
|
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 # 第 N 次 _execute 抛错(模拟重试期间 BLOCK/出错)
|
|
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
|
+
# 默认 reflect:接受,不重试(behavior-preserving) → _execute 只调 1 次
|
|
61
|
+
assert ctx.reflect["decision"] == "accept" and ctx.reflect["retries"] == 0
|
|
62
|
+
assert a.exec_count == 1, "默认不重试,_execute 只调一次"
|
|
63
|
+
print(f" ✓ recall→plan→route→act→verify→reflect 按序;plan={ctx.plan['flow']} "
|
|
64
|
+
f"route={ctx.route['target']} reflect={ctx.reflect['decision']}")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_plan_route_stage():
|
|
68
|
+
print("\n【1c】plan/route stage:P1-2 引擎分析结构(informational,不改执行)")
|
|
69
|
+
from foxclaw.pipeline import stage_plan, stage_route
|
|
70
|
+
# 多步任务(顺序连接词)
|
|
71
|
+
ctx = PipelineContext(agent=FakeAgent(), task="先分析支出 然后 给出建议")
|
|
72
|
+
stage_plan(ctx); stage_route(ctx)
|
|
73
|
+
assert ctx.plan["multi_step"] is True and len(ctx.plan["steps"]) == 2, ctx.plan
|
|
74
|
+
assert ctx.plan["flow"] == "sequential" and ctx.route["target"] == "self"
|
|
75
|
+
# 单步任务
|
|
76
|
+
ctx2 = PipelineContext(agent=FakeAgent(), task="分析我的开支")
|
|
77
|
+
stage_plan(ctx2)
|
|
78
|
+
assert ctx2.plan["multi_step"] is False and ctx2.plan["steps"] == ["分析我的开支"]
|
|
79
|
+
print(f" ✓ 多步 plan={ctx.plan['steps']}(flow={ctx.plan['flow']});单步 multi_step=False")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_verify_stage():
|
|
83
|
+
print("\n【1b】verify stage:只读推理节点汇总 warns/max_delta(不新增校验)")
|
|
84
|
+
nodes = [_Node("PASS", 0.1), _Node("WARN", 0.42), _Node("PASS", 0.2), _Node("BASELINE", None)]
|
|
85
|
+
a = FakeAgent(nodes=nodes)
|
|
86
|
+
ctx = PipelineContext(agent=a, task="V", session_id="s2")
|
|
87
|
+
stage_verify(ctx)
|
|
88
|
+
assert ctx.verified == {"passed": True, "nodes": 4, "warns": 1,
|
|
89
|
+
"max_delta": 0.42, "last_delta": 0.2}, ctx.verified
|
|
90
|
+
print(f" ✓ verified={ctx.verified}(max_delta=峰值0.42;last_delta=最近0.2,供 reflect 早退)")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_insertable_stage():
|
|
94
|
+
print("\n【2】可插桩:插入自定义 stage 按位置执行")
|
|
95
|
+
hits = []
|
|
96
|
+
def stage_probe(ctx):
|
|
97
|
+
hits.append(("probe", len(ctx.memories))) # 在 recall 之后、act 之前
|
|
98
|
+
stage_probe.stage_name = "probe"
|
|
99
|
+
a = FakeAgent()
|
|
100
|
+
ctx = PipelineContext(agent=a, task="X")
|
|
101
|
+
pipe = CognitivePipeline([stage_recall, stage_probe, stage_act])
|
|
102
|
+
asyncio.run(pipe.run(ctx))
|
|
103
|
+
assert hits == [("probe", 1)], "probe 应在 recall(已填 memories)之后运行"
|
|
104
|
+
assert ctx.trace == ["recall", "probe", "act"]
|
|
105
|
+
print(f" ✓ 插入 stage 生效,trace={ctx.trace}(后续 verify/reflect 即这样插入)")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_exception_propagates():
|
|
109
|
+
print("\n【3】异常透传:stage 抛错向上传播(管线不吞)")
|
|
110
|
+
def boom(ctx):
|
|
111
|
+
raise RuntimeError("stage 炸了")
|
|
112
|
+
a = FakeAgent()
|
|
113
|
+
raised = False
|
|
114
|
+
try:
|
|
115
|
+
asyncio.run(CognitivePipeline([boom, stage_act]).run(PipelineContext(agent=a, task="Y")))
|
|
116
|
+
except RuntimeError:
|
|
117
|
+
raised = True
|
|
118
|
+
assert raised, "stage 异常应向上传播,由 run_task 统一兜底"
|
|
119
|
+
print(" ✓ stage 异常透传(HarnessBlockedError 即由此上抛给 run_task 兜底)")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class _CapSpine:
|
|
123
|
+
def __init__(self): self.events = []
|
|
124
|
+
def publish(self, topic, payload=None, source="", **kw): self.events.append(topic)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class _Caps:
|
|
128
|
+
def __init__(self, spine): self._spine = spine
|
|
129
|
+
def get(self, name, default=None): return self._spine if name == "event_spine" else default
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_stage_events():
|
|
133
|
+
print("\n【6】逐 stage 事件:每个 stage emit agent.cognition.<stage> 到脊柱")
|
|
134
|
+
spine = _CapSpine()
|
|
135
|
+
a = FakeAgent()
|
|
136
|
+
a._caps = _Caps(spine) # 注入能力中枢(解析出脊柱)
|
|
137
|
+
ctx = PipelineContext(agent=a, task="E", session_id="s3")
|
|
138
|
+
asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(ctx))
|
|
139
|
+
expected = ["agent.cognition." + n for n in
|
|
140
|
+
["perceive", "recall", "plan", "route", "act", "verify", "reflect"]]
|
|
141
|
+
assert spine.events == expected, f"逐 stage 事件,实得 {spine.events}"
|
|
142
|
+
# 无中枢时优雅跳过(不崩)
|
|
143
|
+
a2 = FakeAgent() # 无 _caps
|
|
144
|
+
asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(PipelineContext(agent=a2, task="X")))
|
|
145
|
+
print(" ✓ 7 个 agent.cognition.* 事件按序 emit;无中枢时优雅跳过")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_route_skill_optin_inert():
|
|
149
|
+
print("\n【7】A·Step5:route 默认自动选 skill(仅只读+高阈值);可关;有副作用的不自动选")
|
|
150
|
+
from foxclaw.pipeline import stage_route
|
|
151
|
+
from foxclaw.skills import build_default_registry
|
|
152
|
+
reg = build_default_registry() # 含 calculator(触发词「计算」等)
|
|
153
|
+
|
|
154
|
+
# (a) 【契约变更 2026-07-16】原为"默认关"(behavior-preserving 放量期)。
|
|
155
|
+
# 现默认**开**:此前 route/dispatch 双关导致工具中心/MCP/技能在正常对话里根本够不着
|
|
156
|
+
# (「帮我算 3+5」只会 LLM 硬答)。现默认自动选中**只读**技能。
|
|
157
|
+
a = FakeAgent(); a.skill_registry = reg
|
|
158
|
+
ctx = PipelineContext(agent=a, task="帮我计算 (12+8)*5")
|
|
159
|
+
stage_route(ctx)
|
|
160
|
+
assert ctx.route["target"] == "skill" and ctx.route["skill_id"] == "calculator", \
|
|
161
|
+
f"默认应自动选中只读的 calculator: {ctx.route}"
|
|
162
|
+
|
|
163
|
+
# (a2) 可关(显式 False / 环境变量)→ 回落 self,老行为随时可拿回
|
|
164
|
+
ctx_off = PipelineContext(agent=a, task="帮我计算 (12+8)*5",
|
|
165
|
+
context={"_route_skill": False})
|
|
166
|
+
stage_route(ctx_off)
|
|
167
|
+
assert ctx_off.route["target"] == "self", ctx_off.route
|
|
168
|
+
|
|
169
|
+
# (b) 开启(显式阈值 0.5,避免耦合具体分值):计算类 → router 选中 calculator,target=skill
|
|
170
|
+
ctx2 = PipelineContext(agent=a, task="帮我计算 (12+8)*5",
|
|
171
|
+
context={"_route_skill": {"threshold": 0.5}})
|
|
172
|
+
stage_route(ctx2)
|
|
173
|
+
assert ctx2.route["target"] == "skill" and ctx2.route["skill_id"] == "calculator", ctx2.route
|
|
174
|
+
assert ctx2.route["confidence"] >= 0.5, ctx2.route
|
|
175
|
+
|
|
176
|
+
# (c) 【契约变更】原断言"act 不消费 route"(Step1 铺线期)。Step2 起 act 会真派发;
|
|
177
|
+
# 此处验证:选中 skill 但**无 skill_adapter**(FakeAgent 没挂)→ _act_via_skill 回落
|
|
178
|
+
# _execute,fail-safe 不崩。
|
|
179
|
+
a3 = FakeAgent(); a3.skill_registry = reg
|
|
180
|
+
ctx3 = PipelineContext(agent=a3, task="帮我计算 (12+8)*5",
|
|
181
|
+
context={"_route_skill": {"threshold": 0.5}})
|
|
182
|
+
asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(ctx3))
|
|
183
|
+
assert ctx3.route["target"] == "skill", "route 已选中 skill"
|
|
184
|
+
assert ctx3.result == "done:帮我计算 (12+8)*5#1" and a3.exec_count == 1, \
|
|
185
|
+
"无 skill_adapter → 回落 _execute(fail-safe)"
|
|
186
|
+
|
|
187
|
+
# (d) 无 skill_registry 的 Agent(finance/admin/office)→ 恒 self(fail-safe 回落)
|
|
188
|
+
a4 = FakeAgent()
|
|
189
|
+
ctx4 = PipelineContext(agent=a4, task="计算 1+1", context={"_route_skill": True})
|
|
190
|
+
stage_route(ctx4)
|
|
191
|
+
assert ctx4.route["target"] == "self", "无 skill_registry → 回落 self"
|
|
192
|
+
|
|
193
|
+
# (e) 高阈值(>1)→ 实际不选,回落 self(阈值可调证明)
|
|
194
|
+
ctx5 = PipelineContext(agent=a, task="帮我计算 1+1",
|
|
195
|
+
context={"_route_skill": {"threshold": 1.01}})
|
|
196
|
+
stage_route(ctx5)
|
|
197
|
+
assert ctx5.route["target"] == "self", "阈值>1 → 不选,回落 self"
|
|
198
|
+
print(" ✓ 默认关=self;开启(阈值0.5)选中 calculator 但 act 仍 _execute(执行不变);"
|
|
199
|
+
"无registry/高阈值→回落 self")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def test_act_skill_dispatch_step2():
|
|
203
|
+
print("\n【8】A·Step2:act 按 ctx.route 经 PolicyGate dispatch skill(opt-in;非成功 fail-safe 无副作用)")
|
|
204
|
+
from foxclaw.skills import build_default_registry, SkillAdapter
|
|
205
|
+
reg = build_default_registry()
|
|
206
|
+
adapter = SkillAdapter(reg)
|
|
207
|
+
|
|
208
|
+
def mk_agent():
|
|
209
|
+
a = FakeAgent(); a.skill_registry = reg; a.skill_adapter = adapter
|
|
210
|
+
return a
|
|
211
|
+
|
|
212
|
+
# (a) 开启 dispatch + 计算类 → 真正执行 calculator,结果=100,且领域 _execute(LLM) 不被调
|
|
213
|
+
a = mk_agent()
|
|
214
|
+
ctx = PipelineContext(agent=a, task="帮我计算 (12+8)*5",
|
|
215
|
+
context={"_route_skill": {"threshold": 0.5}, "_skill_dispatch": True})
|
|
216
|
+
asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(ctx))
|
|
217
|
+
assert ctx.route["target"] == "skill" and ctx.route["skill_id"] == "calculator", ctx.route
|
|
218
|
+
assert ctx.extra.get("via") == "skill", ctx.extra
|
|
219
|
+
assert ctx.result["output"]["result"] == 100, ctx.result
|
|
220
|
+
assert a.exec_count == 0, "skill 执行了 → 领域 _execute(LLM) 不再被调(首个真改执行)"
|
|
221
|
+
|
|
222
|
+
# (b) 【安全铁律】开启 dispatch + 发邮件(destructive/send)未确认 → needs_confirmation,零副作用、不回落 LLM
|
|
223
|
+
# 注:默认的只读闸会挡住 gmail_send(有副作用不自动选),故此处**显式** read_only=False
|
|
224
|
+
# 表示"我就是要测有副作用的路由被 PolicyGate 拦住"。
|
|
225
|
+
a2 = mk_agent()
|
|
226
|
+
ctx2 = PipelineContext(agent=a2, task="帮我发送邮件给客户",
|
|
227
|
+
context={"_route_skill": {"threshold": 0.5, "read_only": False},
|
|
228
|
+
"_skill_dispatch": True})
|
|
229
|
+
asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(ctx2))
|
|
230
|
+
assert ctx2.route["skill_id"] == "gmail_send", ctx2.route
|
|
231
|
+
assert ctx2.result["status"] == "needs_confirmation" and ctx2.result["pending"] is True, ctx2.result
|
|
232
|
+
assert a2.exec_count == 0, "未确认的危险 skill → 不执行、不回落 LLM(绝不 fail-open)"
|
|
233
|
+
|
|
234
|
+
# (c) 显式关 dispatch(只 route)→ 即便 target=skill,act 仍走 _execute(两道门相互独立)
|
|
235
|
+
# 注:dispatch 现在**默认开**,故此处显式 False 才能验证"关掉"这条路径。
|
|
236
|
+
a3 = mk_agent()
|
|
237
|
+
ctx3 = PipelineContext(agent=a3, task="帮我计算 (12+8)*5",
|
|
238
|
+
context={"_route_skill": {"threshold": 0.5}, "_skill_dispatch": False})
|
|
239
|
+
asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(ctx3))
|
|
240
|
+
assert ctx3.route["target"] == "skill" and a3.exec_count == 1, "dispatch 关 → 回落 _execute"
|
|
241
|
+
|
|
242
|
+
# (d) 开 dispatch 但 Agent 无 skill_adapter → 回落 _execute(fail-safe)
|
|
243
|
+
a4 = FakeAgent(); a4.skill_registry = reg # 有 registry 无 adapter
|
|
244
|
+
ctx4 = PipelineContext(agent=a4, task="帮我计算 1+1",
|
|
245
|
+
context={"_route_skill": {"threshold": 0.5}, "_skill_dispatch": True})
|
|
246
|
+
asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(ctx4))
|
|
247
|
+
assert a4.exec_count == 1, "无 adapter → 回落 _execute"
|
|
248
|
+
|
|
249
|
+
# (b2) 【新安全闸·A·Step5】默认(只读闸开)下,发邮件这类有副作用的技能**根本不会被自动选中**
|
|
250
|
+
# —— 自动执行不得越过"会改变世界"的线;要用仍可显式 use_skill(那时用户是知情的)。
|
|
251
|
+
a2b = mk_agent()
|
|
252
|
+
ctx2b = PipelineContext(agent=a2b, task="帮我发送邮件给客户") # 全默认
|
|
253
|
+
asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(ctx2b))
|
|
254
|
+
assert ctx2b.route["target"] == "self", f"有副作用的技能不该被自动选中: {ctx2b.route}"
|
|
255
|
+
assert a2b.exec_count == 1, "未自动动手 → 由 LLM 作答"
|
|
256
|
+
|
|
257
|
+
# (e) 【fail-closed 加固】invoke 意外抛错 → 结构化 error,不崩、不回落 LLM
|
|
258
|
+
class _BoomAdapter:
|
|
259
|
+
def invoke(self, skill, args, confirmed=False, mode="strict"):
|
|
260
|
+
raise RuntimeError("dispatch 内部炸了")
|
|
261
|
+
a5 = FakeAgent(); a5.skill_registry = reg; a5.skill_adapter = _BoomAdapter()
|
|
262
|
+
ctx5 = PipelineContext(agent=a5, task="帮我计算 1+1",
|
|
263
|
+
context={"_route_skill": {"threshold": 0.5}, "_skill_dispatch": True})
|
|
264
|
+
asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(ctx5)) # 不抛异常
|
|
265
|
+
assert ctx5.result["status"] == "error" and "异常" in ctx5.result["message"], ctx5.result
|
|
266
|
+
assert a5.exec_count == 0, "dispatch 异常 → fail-closed 结构化 error,绝不回落 LLM"
|
|
267
|
+
print(" ✓ 开启:calculator 真执行(=100,不调LLM);发邮件未确认→needs_confirmation零副作用;"
|
|
268
|
+
"dispatch关/无adapter→回落_execute;invoke异常→fail-closed error(不崩不回落LLM)")
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def test_run_task_confirm_suspend_step3():
|
|
272
|
+
print("\n【9】A·Step3:skill needs_confirmation 提升为 run_task 顶层 status + _confirmed 恢复执行")
|
|
273
|
+
import tempfile, shutil
|
|
274
|
+
from foxclaw.agents.base import CognitiveAgent
|
|
275
|
+
from foxclaw.core.thought_chain import ThoughtChain
|
|
276
|
+
from foxclaw.skills import build_default_registry, SkillAdapter, SkillSpec
|
|
277
|
+
|
|
278
|
+
class _FakeHMR:
|
|
279
|
+
def recall(self, query="", agent_id=None, top_k=5, **kw): return []
|
|
280
|
+
def ingest(self, content, **kw): return None
|
|
281
|
+
def get_workspace(self, agent_id):
|
|
282
|
+
return type("W", (), {"active_goal": "", "push_task": lambda s, t: None})()
|
|
283
|
+
def save_workspace(self, agent_id): pass
|
|
284
|
+
|
|
285
|
+
class _Mini(CognitiveAgent):
|
|
286
|
+
AGENT_ID = "mini"
|
|
287
|
+
ALLOWED_TOOL_CATEGORIES = {"read", "write", "network"}
|
|
288
|
+
@property
|
|
289
|
+
def _role_prompt(self): return "test"
|
|
290
|
+
async def _execute(self, task, context, session_id, memories):
|
|
291
|
+
return f"llm:{task}", {"via": "llm"}
|
|
292
|
+
|
|
293
|
+
d = tempfile.mkdtemp(prefix="step3_")
|
|
294
|
+
try:
|
|
295
|
+
reg = build_default_registry()
|
|
296
|
+
saved = []
|
|
297
|
+
reg.register(SkillSpec(
|
|
298
|
+
id="save_note", name="记笔记", description="保存一条笔记", kind="local",
|
|
299
|
+
triggers=["记一下", "记笔记", "保存笔记"], side_effect="write",
|
|
300
|
+
handler=lambda text="": (saved.append(text) or {"saved": text}),
|
|
301
|
+
params={"text": "str"}))
|
|
302
|
+
adapter = SkillAdapter(reg)
|
|
303
|
+
ag = _Mini(hmr=_FakeHMR(), thought_chain=ThoughtChain(db_path=f"{d}/tc.db"),
|
|
304
|
+
data_dir=d, shadow_rounds=5)
|
|
305
|
+
ag.skill_registry = reg; ag.skill_adapter = adapter
|
|
306
|
+
# read_only=False:本测试专门验证**写入类** skill 的 confirm 挂起路径,
|
|
307
|
+
# 而默认的只读闸不会自动选中它 —— 故显式声明(调用方自担风险的显式选择)。
|
|
308
|
+
base_ctx = {"_route_skill": {"threshold": 0.4, "read_only": False},
|
|
309
|
+
"_skill_dispatch": True}
|
|
310
|
+
|
|
311
|
+
# 未确认:写 skill → run_task 顶层 status=needs_confirmation + pending_action,零副作用
|
|
312
|
+
r1 = asyncio.run(ag.run_task("帮我记一下 买牛奶", dict(base_ctx)))
|
|
313
|
+
assert r1.get("status") == "needs_confirmation", r1
|
|
314
|
+
assert r1.get("pending_action", {}).get("skill_id") == "save_note", r1
|
|
315
|
+
assert saved == [], "未确认 → handler 不执行(零副作用)"
|
|
316
|
+
|
|
317
|
+
# 【深审加固】确认绑定错 skill_id → 仍 needs_confirmation、不执行(防确认漂移)
|
|
318
|
+
r_bad = asyncio.run(ag.run_task("帮我记一下 买牛奶",
|
|
319
|
+
{**base_ctx, "_confirmed": "gmail_send"}))
|
|
320
|
+
assert r_bad.get("status") == "needs_confirmation", r_bad
|
|
321
|
+
assert saved == [], "确认绑定到别的 skill → 不执行本 skill(绑定优先)"
|
|
322
|
+
|
|
323
|
+
# 确认恢复:绑定到正确 skill_id 重跑 → skill 真执行,无 needs_confirmation
|
|
324
|
+
r2 = asyncio.run(ag.run_task("帮我记一下 买牛奶",
|
|
325
|
+
{**base_ctx, "_confirmed": "save_note"}))
|
|
326
|
+
assert r2.get("status") != "needs_confirmation", r2
|
|
327
|
+
assert len(saved) == 1, f"确认后 handler 执行(有副作用),saved={saved}"
|
|
328
|
+
|
|
329
|
+
# 向后兼容:_confirmed=True 概括确认仍生效
|
|
330
|
+
saved.clear()
|
|
331
|
+
r2b = asyncio.run(ag.run_task("帮我记一下 买酸奶", {**base_ctx, "_confirmed": True}))
|
|
332
|
+
assert r2b.get("status") != "needs_confirmation" and len(saved) == 1, (r2b, saved)
|
|
333
|
+
|
|
334
|
+
# 纯 LLM 路径(dispatch 关)→ 无 status 字段(行为逐字不变)
|
|
335
|
+
r3 = asyncio.run(ag.run_task("随便聊聊"))
|
|
336
|
+
assert "status" not in r3 and r3["result"] == "llm:随便聊聊", r3
|
|
337
|
+
print(f" ✓ 未确认→顶层 needs_confirmation 零副作用;_confirmed 重跑→执行(saved={saved});"
|
|
338
|
+
f"LLM 路径无 status(行为不变)")
|
|
339
|
+
finally:
|
|
340
|
+
shutil.rmtree(d, ignore_errors=True)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def test_reflect_retry_optin():
|
|
344
|
+
print("\n【4】reflect opt-in:漂移超阈值 → 有上限重试")
|
|
345
|
+
from foxclaw.pipeline import stage_verify # noqa
|
|
346
|
+
a = FakeAgent(nodes=[_Node("WARN", 0.8)]) # verify 得 max_delta=0.8 > 阈值 0.5
|
|
347
|
+
ctx = PipelineContext(agent=a, task="V",
|
|
348
|
+
context={"_reflect": {"max_retries": 2, "drift_threshold": 0.5}})
|
|
349
|
+
asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(ctx))
|
|
350
|
+
assert a.exec_count == 3, f"1 初次 + 2 重试,实得 {a.exec_count}"
|
|
351
|
+
assert ctx.reflect["decision"] == "retry" and ctx.reflect["retries"] == 2, ctx.reflect
|
|
352
|
+
assert ctx.result == "done:V#3", "最终结果=最后一次重试"
|
|
353
|
+
print(f" ✓ 重试到上限:exec={a.exec_count} reflect={ctx.reflect}")
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
class _DynChain:
|
|
357
|
+
"""动态 chain:重试后追加一个低漂移新节点(旧高节点仍在)。用于证明 reflect 用 last_delta 早退。"""
|
|
358
|
+
def __init__(self, agent): self.agent = agent
|
|
359
|
+
def get_chain(self, session_id, agent_id=None):
|
|
360
|
+
if self.agent.exec_count <= 1:
|
|
361
|
+
return [_Node("WARN", 0.8)] # 初次:高漂移
|
|
362
|
+
return [_Node("WARN", 0.8), _Node("PASS", 0.2)] # 重试后:旧高(0.8) + 新低(0.2)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def test_reflect_early_exit_on_improve():
|
|
366
|
+
print("\n【5b】深审修复:重试改善后早退(用 last_delta,非累积 max_delta)")
|
|
367
|
+
a = FakeAgent()
|
|
368
|
+
a.thought_chain = _DynChain(a)
|
|
369
|
+
ctx = PipelineContext(agent=a, task="D",
|
|
370
|
+
context={"_reflect": {"max_retries": 3, "drift_threshold": 0.5}})
|
|
371
|
+
asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(ctx))
|
|
372
|
+
# 初次 exec=1 → last_delta=0.8>0.5 → 重试1 exec=2 → last_delta=0.2<=0.5 → 早退(非到上限3)
|
|
373
|
+
assert a.exec_count == 2, f"改善后应只重试 1 次(而非到上限 3),实得 {a.exec_count}"
|
|
374
|
+
assert ctx.reflect["decision"] == "retry_accepted" and ctx.reflect["retries"] == 1, ctx.reflect
|
|
375
|
+
# 关键:此时 max_delta 仍是 0.8(旧节点),若用它会错误地重试到上限;last_delta=0.2 才对
|
|
376
|
+
assert ctx.verified["max_delta"] == 0.8 and ctx.verified["last_delta"] == 0.2
|
|
377
|
+
print(f" ✓ 早退:exec={a.exec_count} reflect={ctx.reflect['decision']} "
|
|
378
|
+
f"(max_delta={ctx.verified['max_delta']} 仍高,但 last_delta={ctx.verified['last_delta']} 达标)")
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def test_reflect_failsafe_rollback():
|
|
382
|
+
print("\n【5】reflect fail-safe:重试自身出错 → 回滚到重试前结果")
|
|
383
|
+
a = FakeAgent(nodes=[_Node("WARN", 0.8)], fail_on=2) # 第 2 次 _execute(首个重试)抛错
|
|
384
|
+
ctx = PipelineContext(agent=a, task="X",
|
|
385
|
+
context={"_reflect": {"max_retries": 2, "drift_threshold": 0.5}})
|
|
386
|
+
asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(ctx))
|
|
387
|
+
assert a.exec_count == 2, "初次 + 1 次失败重试"
|
|
388
|
+
assert ctx.result == "done:X#1", "回滚到重试前的好结果(重试不让结果变差)"
|
|
389
|
+
assert ctx.reflect["decision"] == "retry_reverted" and ctx.reflect["retries"] == 1, ctx.reflect
|
|
390
|
+
print(f" ✓ 重试出错回滚:result={ctx.result!r} reflect={ctx.reflect}(fail-safe)")
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def test_concurrent_ctx_isolation():
|
|
394
|
+
"""【P4 证伪】并发多任务打同一 agent:ctx 是 run_task 内**每任务新建的局部对象**、
|
|
395
|
+
stage **顺序 await**(无 stage 并行),故可变 ctx **并发安全、不串**。
|
|
396
|
+
|
|
397
|
+
范式原则五"不可变状态流转"反对的是"**跨任务共享**可变状态"(工具 A 改一半、工具 B
|
|
398
|
+
读到不一致);PipelineContext 不属于该反模式——它不跨任务共享。本测试用 24 个并发任务
|
|
399
|
+
+ 每个 _execute await 让出控制权(最大化交错、逼出潜在串状态)来证伪。"""
|
|
400
|
+
print("\n【6】P4 证伪:并发多任务 ctx 隔离(可变但不串)")
|
|
401
|
+
|
|
402
|
+
class SlowAgent(FakeAgent):
|
|
403
|
+
async def _execute(self, task, context, session_id, memories):
|
|
404
|
+
await asyncio.sleep(0.005) # 让出控制权 → 制造交错 → 暴露串状态
|
|
405
|
+
return f"done:{task}", {"task_echo": task}
|
|
406
|
+
|
|
407
|
+
a = SlowAgent() # 同一 agent 实例,被 24 个任务并发打
|
|
408
|
+
|
|
409
|
+
async def run_one(task):
|
|
410
|
+
ctx = PipelineContext(agent=a, task=task, session_id=f"s_{task}")
|
|
411
|
+
await CognitivePipeline(DEFAULT_STAGES).run(ctx)
|
|
412
|
+
return task, ctx
|
|
413
|
+
|
|
414
|
+
async def _main():
|
|
415
|
+
tasks = [f"T{i}" for i in range(24)]
|
|
416
|
+
results = await asyncio.gather(*[run_one(t) for t in tasks])
|
|
417
|
+
for t, ctx in results:
|
|
418
|
+
assert ctx.task == t, f"ctx.task 串了: {ctx.task} != {t}"
|
|
419
|
+
assert ctx.result == f"done:{t}", f"ctx.result 串了: {ctx.result} != done:{t}"
|
|
420
|
+
assert ctx.extra["task_echo"] == t, f"ctx.extra 串了: {ctx.extra}"
|
|
421
|
+
assert ctx.route.get("target") == "self"
|
|
422
|
+
assert ctx.trace == ["perceive", "recall", "plan", "route", "act", "verify", "reflect"]
|
|
423
|
+
return len(results)
|
|
424
|
+
|
|
425
|
+
n = asyncio.run(_main())
|
|
426
|
+
print(f" ✓ {n} 并发任务 ctx 零串——每个 ctx.result/task/extra/trace 都属自己的 task")
|
|
427
|
+
print(" 结论:ctx 每任务新建 + stage 顺序执行 → 可变但并发安全(P4 证实为伪风险)")
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def test_cooperative_interrupt():
|
|
431
|
+
"""【P3】协作式中断:stop_event 置位 → 管线在下一 stage 边界 / reflect 重试前优雅停止。
|
|
432
|
+
opt-in——默认 stop_event=None 时行为逐字不变。覆盖:(a)不置位跑满 (b)起步即停
|
|
433
|
+
(c)中途 stage 边界停 (d)reflect 重试循环停(范式"每轮迭代检查中断"的本地对应)。"""
|
|
434
|
+
import threading
|
|
435
|
+
print("\n【7】P3 协作式中断:stop_event 在 stage 边界 / 重试迭代前优雅停")
|
|
436
|
+
|
|
437
|
+
# (a) 无 stop_event → 跑满 7 stage、interrupted=False(behavior-preserving)
|
|
438
|
+
a = FakeAgent()
|
|
439
|
+
ctx = PipelineContext(agent=a, task="T")
|
|
440
|
+
asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(ctx))
|
|
441
|
+
assert ctx.interrupted is False and len(ctx.trace) == 7, (ctx.interrupted, ctx.trace)
|
|
442
|
+
|
|
443
|
+
# (b) 起步即置位 → 0 个 stage 执行、interrupted=True、无结果
|
|
444
|
+
ev = threading.Event(); ev.set()
|
|
445
|
+
ctx2 = PipelineContext(agent=FakeAgent(), task="T", stop_event=ev)
|
|
446
|
+
asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(ctx2))
|
|
447
|
+
assert ctx2.interrupted is True and ctx2.trace == [] and ctx2.result is None, \
|
|
448
|
+
(ctx2.interrupted, ctx2.trace, ctx2.result)
|
|
449
|
+
|
|
450
|
+
# (c) 中途置位(自定义 stage 跑完就 set)→ 停在下一边界,后续 stage 全不跑
|
|
451
|
+
ev2 = threading.Event()
|
|
452
|
+
def _tripwire(c): ev2.set()
|
|
453
|
+
_tripwire.stage_name = "tripwire"
|
|
454
|
+
stages = DEFAULT_STAGES[:2] + [_tripwire] + DEFAULT_STAGES[2:] # perceive,recall,tripwire,plan...
|
|
455
|
+
ctx3 = PipelineContext(agent=FakeAgent(), task="T", stop_event=ev2)
|
|
456
|
+
asyncio.run(CognitivePipeline(stages).run(ctx3))
|
|
457
|
+
assert ctx3.trace == ["perceive", "recall", "tripwire"], ctx3.trace
|
|
458
|
+
assert ctx3.interrupted is True and ctx3.result is None, (ctx3.interrupted, ctx3.result)
|
|
459
|
+
print(f" ✓ 边界中断:无 stop 跑满 7;起步停 0 stage;中途停 trace={ctx3.trace}(后续未跑)")
|
|
460
|
+
|
|
461
|
+
# (d) reflect 重试循环中断:agent 在**首次重试的 _execute** 时置位 → 下一轮迭代前停,
|
|
462
|
+
# 证明重试在用尽 max_retries(=5)之前被截断。这是本管线最接近"agentic loop 每轮检查中断"处。
|
|
463
|
+
ev3 = threading.Event()
|
|
464
|
+
class _StopOnRetryAgent(FakeAgent):
|
|
465
|
+
def __init__(self, e):
|
|
466
|
+
super().__init__(nodes=[_Node("WARN", 0.8)]) # 恒高漂移 → 不中断会一直重试到上限
|
|
467
|
+
self._e = e
|
|
468
|
+
async def _execute(self, task, context, session_id, memories):
|
|
469
|
+
self.exec_count += 1
|
|
470
|
+
if self.exec_count == 2: # 第 1 次重试的 LLM 调用时请求中断
|
|
471
|
+
self._e.set()
|
|
472
|
+
return f"done:{task}#{self.exec_count}", {"call": self.exec_count}
|
|
473
|
+
a4 = _StopOnRetryAgent(ev3)
|
|
474
|
+
ctx4 = PipelineContext(agent=a4, task="R", stop_event=ev3,
|
|
475
|
+
context={"_reflect": {"max_retries": 5, "drift_threshold": 0.5}})
|
|
476
|
+
asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(ctx4))
|
|
477
|
+
assert a4.exec_count == 2, f"初次 act + 1 次重试后即中断(上限 5 未跑满),实得 {a4.exec_count}"
|
|
478
|
+
assert ctx4.reflect["decision"] == "interrupted" and ctx4.reflect["retries"] == 1, ctx4.reflect
|
|
479
|
+
assert ctx4.interrupted is True
|
|
480
|
+
print(f" ✓ 重试中断:exec={a4.exec_count}(上限5未满) reflect={ctx4.reflect['decision']} "
|
|
481
|
+
f"→ 省下 4 次 LLM 调用")
|
|
482
|
+
print(" 结论:stop_event 在 stage 边界与重试迭代前生效;默认 None 时全部行为逐字不变")
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def test_on_stage_callback():
|
|
486
|
+
"""【8】D1:`pipeline.run(on_stage)` 逐 stage 回调(opt-in);无回调=行为不变;回调异常 fail-safe。"""
|
|
487
|
+
print("\n【8】D1 on_stage 回调:逐 stage 通知 + opt-in + fail-safe")
|
|
488
|
+
# (a) 有回调:每 stage 完成后回调一次,顺序与 trace 一致
|
|
489
|
+
seen = []
|
|
490
|
+
ctx = PipelineContext(agent=FakeAgent(), task="T")
|
|
491
|
+
asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(
|
|
492
|
+
ctx, on_stage=lambda name, c: seen.append(name)))
|
|
493
|
+
assert seen == ctx.trace == ["perceive", "recall", "plan", "route", "act", "verify", "reflect"], \
|
|
494
|
+
(seen, ctx.trace)
|
|
495
|
+
# (b) 无回调:行为逐字不变(默认路径)
|
|
496
|
+
ctx2 = PipelineContext(agent=FakeAgent(), task="T")
|
|
497
|
+
asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(ctx2))
|
|
498
|
+
assert len(ctx2.trace) == 7 and ctx2.result == "done:T#1"
|
|
499
|
+
# (c) 回调抛异常:被吞,不影响管线跑完(fail-safe:流式反馈绝不拖垮任务)
|
|
500
|
+
def _boom(name, c):
|
|
501
|
+
raise RuntimeError("回调故障")
|
|
502
|
+
ctx3 = PipelineContext(agent=FakeAgent(), task="T")
|
|
503
|
+
asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(ctx3, on_stage=_boom))
|
|
504
|
+
assert len(ctx3.trace) == 7 and ctx3.result == "done:T#1", "回调故障不拖垮管线"
|
|
505
|
+
print(f" ✓ on_stage 逐 stage 回调({len(seen)} 次)、无回调行为不变、回调异常 fail-safe")
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def main():
|
|
509
|
+
print("=" * 60)
|
|
510
|
+
print("认知管线 · Cognitive Pipeline(v2 ② recall/act/verify/reflect)")
|
|
511
|
+
print("=" * 60)
|
|
512
|
+
test_default_pipeline()
|
|
513
|
+
test_plan_route_stage()
|
|
514
|
+
test_verify_stage()
|
|
515
|
+
test_insertable_stage()
|
|
516
|
+
test_exception_propagates()
|
|
517
|
+
test_stage_events()
|
|
518
|
+
test_route_skill_optin_inert()
|
|
519
|
+
test_act_skill_dispatch_step2()
|
|
520
|
+
test_run_task_confirm_suspend_step3()
|
|
521
|
+
test_reflect_retry_optin()
|
|
522
|
+
test_reflect_early_exit_on_improve()
|
|
523
|
+
test_reflect_failsafe_rollback()
|
|
524
|
+
test_concurrent_ctx_isolation()
|
|
525
|
+
test_cooperative_interrupt()
|
|
526
|
+
test_on_stage_callback()
|
|
527
|
+
print("\n" + "=" * 60)
|
|
528
|
+
print("✓ Step1 验证完成:PipelineContext + 顺序 + 可插桩 + 异常透传 + 并发隔离 + 中断 + on_stage")
|
|
529
|
+
print("=" * 60)
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
if __name__ == "__main__":
|
|
533
|
+
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)) # 无 _think → 静默跳过
|
|
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) # 管线在 plan stage 完成后回调
|
|
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 事件"
|