foxclaw 0.1.2 → 0.1.4
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/index.js +1 -8
- package/package.json +1 -1
- package/python/.env.example +8 -0
- package/python/.github/workflows/ci.yml +47 -2
- package/python/.github/workflows/release.yml +5 -0
- package/python/CHANGELOG.md +100 -0
- package/python/CHANGELOG_CN.md +100 -0
- package/python/README.md +17 -2
- package/python/README_CN.md +38 -20
- package/python/bench/bm25_audit.py +1 -14
- package/python/bench/bm25_regression.py +0 -1
- package/python/bench/bm25_stress.py +0 -4
- package/python/bench/cd_audit.py +0 -23
- package/python/bench/chaos.py +6 -8
- package/python/bench/e2e_real_models.py +0 -1
- package/python/bench/llm_retry_test.py +2 -9
- package/python/bench/load_test.py +5 -8
- package/python/bench/vector_ivf_upgrade.py +1 -7
- package/python/docs/CONTENT_GUARD.md +189 -0
- package/python/docs/CONTENT_GUARD_CN.md +189 -0
- package/python/docs/RELEASE.md +78 -37
- package/python/docs/RUN_CN.md +2 -2
- package/python/evals/governance/cases.py +0 -37
- package/python/evals/governance/run_evals.py +5 -37
- package/python/examples/governance_middleware_demo.py +1 -6
- package/python/examples/langgraph_governance_demo.py +7 -11
- package/python/foxclaw/__init__.py +74 -152
- package/python/foxclaw/agents/base.py +82 -136
- package/python/foxclaw/agents/impl/_helpers.py +2 -10
- package/python/foxclaw/agents/impl/finance.py +4 -21
- package/python/foxclaw/agents/impl/office.py +7 -24
- package/python/foxclaw/agents/impl/supervisor.py +12 -79
- package/python/foxclaw/agents/mixins/_context.py +0 -23
- package/python/foxclaw/agents/mixins/augment.py +10 -16
- package/python/foxclaw/agents/mixins/llm.py +7 -6
- package/python/foxclaw/agents/mixins/memory.py +23 -3
- package/python/foxclaw/agents/mixins/preference.py +58 -8
- package/python/foxclaw/agents/mixins/prompt.py +59 -12
- package/python/foxclaw/agents/mixins/skillbox.py +6 -6
- package/python/foxclaw/agents/mixins/tool_exec.py +16 -42
- package/python/foxclaw/agents/rules/finance/RULES.md +131 -0
- package/python/foxclaw/api/__init__.py +18 -77
- package/python/foxclaw/api/_state.py +43 -0
- package/python/foxclaw/api/bootstrap.py +114 -0
- package/python/foxclaw/api/middleware.py +5 -28
- package/python/foxclaw/api/routes/ahs.py +60 -73
- package/python/foxclaw/api/routes/attachments.py +5 -6
- package/python/foxclaw/api/routes/browser.py +7 -11
- package/python/foxclaw/api/routes/domain.py +18 -26
- package/python/foxclaw/api/routes/knowledge.py +3 -21
- package/python/foxclaw/api/routes/llm_admin.py +21 -33
- package/python/foxclaw/api/routes/memory.py +50 -12
- package/python/foxclaw/api/routes/orchestration.py +3 -5
- package/python/foxclaw/api/routes/recovery.py +4 -3
- package/python/foxclaw/api/routes/review.py +2 -11
- package/python/foxclaw/api/routes/search.py +3 -7
- package/python/foxclaw/api/routes/system.py +33 -49
- package/python/foxclaw/api/routes/task.py +31 -49
- package/python/foxclaw/api/routes/ws.py +2 -6
- package/python/foxclaw/api/spine.py +1 -4
- package/python/foxclaw/behavior/__init__.py +20 -0
- package/python/foxclaw/behavior/behavior.yaml +62 -0
- package/python/foxclaw/behavior/spec.py +287 -0
- package/python/foxclaw/browser/action/action_executor.py +1 -5
- package/python/foxclaw/browser/action/action_types.py +0 -1
- package/python/foxclaw/browser/action/url_policy.py +1 -5
- package/python/foxclaw/browser/adapter/web_adapter.py +1 -1
- package/python/foxclaw/browser/driver/playwright_driver.py +20 -16
- package/python/foxclaw/browser/perception/dom_extractor.py +0 -1
- package/python/foxclaw/browser/perception/element_indexer.py +1 -2
- package/python/foxclaw/browser/service.py +7 -11
- package/python/foxclaw/capability/embedding_service.py +4 -6
- package/python/foxclaw/channels/__init__.py +9 -11
- package/python/foxclaw/channels/discord.py +4 -13
- package/python/foxclaw/channels/dispatcher.py +2 -27
- package/python/foxclaw/channels/feishu.py +10 -20
- package/python/foxclaw/channels/openclaw_bridge.py +4 -9
- package/python/foxclaw/channels/signing.py +9 -9
- package/python/foxclaw/channels/telegram.py +3 -4
- package/python/foxclaw/channels/weixin.py +7 -17
- package/python/foxclaw/cli.py +42 -13
- package/python/foxclaw/cognitive/reasoner.py +9 -10
- package/python/foxclaw/cognitive/state.py +18 -24
- package/python/foxclaw/core/audit_chain.py +1 -3
- package/python/foxclaw/core/audit_sink.py +3 -3
- package/python/foxclaw/core/compaction.py +8 -26
- package/python/foxclaw/core/config.py +2 -2
- package/python/foxclaw/core/confirm_nonce.py +6 -12
- package/python/foxclaw/core/content_guard.py +341 -0
- package/python/foxclaw/core/envfile.py +2 -2
- package/python/foxclaw/core/failure_policy.py +17 -26
- package/python/foxclaw/core/gateway.py +6 -10
- package/python/foxclaw/core/governance_audit.py +3 -7
- package/python/foxclaw/core/idempotency.py +15 -22
- package/python/foxclaw/core/injection_guard.py +10 -29
- package/python/foxclaw/core/memory_adapter.py +53 -60
- package/python/foxclaw/core/metrics.py +4 -5
- package/python/foxclaw/core/model_router.py +74 -150
- package/python/foxclaw/core/paths.py +1 -1
- package/python/foxclaw/core/recovery.py +21 -34
- package/python/foxclaw/core/scheduler.py +23 -26
- package/python/foxclaw/core/secrets.py +0 -3
- package/python/foxclaw/core/sensitive_write.py +283 -0
- package/python/foxclaw/core/sessions.py +4 -6
- package/python/foxclaw/core/settings.py +16 -32
- package/python/foxclaw/core/shared_write.py +11 -23
- package/python/foxclaw/core/steering_queue.py +12 -11
- package/python/foxclaw/core/system_bus.py +1 -1
- package/python/foxclaw/core/thought_chain.py +30 -32
- package/python/foxclaw/core/token_meter.py +9 -13
- package/python/foxclaw/core/tools.py +4 -12
- package/python/foxclaw/core/tracing.py +25 -40
- package/python/foxclaw/core/truth_pipeline.py +5 -9
- package/python/foxclaw/core/update_check.py +149 -0
- package/python/foxclaw/core/vec_recall_adapter.py +11 -37
- package/python/foxclaw/core/verify.py +6 -14
- package/python/foxclaw/core/web_search.py +4 -28
- package/python/foxclaw/finance/evidence.py +14 -18
- package/python/foxclaw/finance/fx.py +1 -3
- package/python/foxclaw/finance/graph.py +9 -18
- package/python/foxclaw/finance/models.py +20 -24
- package/python/foxclaw/finance/nlparse.py +12 -72
- package/python/foxclaw/finance/policy.py +8 -36
- package/python/foxclaw/finance/reasoner.py +3 -11
- package/python/foxclaw/finance/reasoning.py +13 -34
- package/python/foxclaw/finance/reconcile.py +5 -10
- package/python/foxclaw/finance/runtime.py +19 -46
- package/python/foxclaw/governance/middleware.py +13 -21
- package/python/foxclaw/harness/_base.py +2 -8
- package/python/foxclaw/harness/_policy.py +19 -38
- package/python/foxclaw/harness/drift.py +1 -13
- package/python/foxclaw/harness/event_bus.py +2 -5
- package/python/foxclaw/harness/harness.py +9 -97
- package/python/foxclaw/harness/loop_detection.py +11 -18
- package/python/foxclaw/itinerary/amap_client.py +2 -5
- package/python/foxclaw/itinerary/models.py +7 -7
- package/python/foxclaw/itinerary/reasoner.py +1 -6
- package/python/foxclaw/itinerary/reasoning.py +15 -21
- package/python/foxclaw/itinerary/runtime.py +8 -12
- package/python/foxclaw/kernel/capabilities.py +5 -6
- package/python/foxclaw/kernel/policy_gate.py +4 -10
- package/python/foxclaw/kernel/process_manager.py +17 -36
- package/python/foxclaw/kernel/scheduler.py +3 -9
- package/python/foxclaw/kernel/sentinel.py +13 -23
- package/python/foxclaw/kernel/sentinel_client.py +12 -15
- package/python/foxclaw/kernel/spine.py +9 -9
- package/python/foxclaw/knowledge/__init__.py +4 -20
- package/python/foxclaw/knowledge/truth_crystallizer.py +5 -8
- package/python/foxclaw/learning/knowledge_graph.py +4 -9
- package/python/foxclaw/learning/models.py +13 -13
- package/python/foxclaw/learning/reasoner.py +1 -4
- package/python/foxclaw/learning/runtime.py +19 -31
- package/python/foxclaw/learning/spaced_repetition.py +2 -7
- package/python/foxclaw/mcp_server.py +2 -3
- package/python/foxclaw/orchestration/__init__.py +16 -33
- package/python/foxclaw/pipeline/cognitive_pipeline.py +45 -97
- package/python/foxclaw/pipeline/stage_meta.py +21 -0
- package/python/foxclaw/pipeline/supervisor_stages.py +13 -42
- package/python/foxclaw/planning/llm_planner.py +3 -3
- package/python/foxclaw/planning/planner.py +6 -11
- package/python/foxclaw/search/agent_port.py +7 -11
- package/python/foxclaw/search/evidence.py +0 -4
- package/python/foxclaw/search/internal_search.py +3 -5
- package/python/foxclaw/search/models.py +11 -13
- package/python/foxclaw/search/planner.py +3 -3
- package/python/foxclaw/search/retriever.py +1 -1
- package/python/foxclaw/search/runtime.py +1 -8
- package/python/foxclaw/skills/__init__.py +46 -100
- package/python/foxclaw/skills/archive.py +111 -0
- package/python/foxclaw/skills/fetch.py +226 -0
- package/python/foxclaw/skills/hooks_spec.py +21 -25
- package/python/foxclaw/skills/install_cmd.py +216 -0
- package/python/foxclaw/skills/lifecycle.py +16 -42
- package/python/foxclaw/skills/markdown_skill.py +10 -9
- package/python/foxclaw/skills/mcp_client.py +5 -11
- package/python/foxclaw/skills/mcp_store.py +0 -3
- package/python/foxclaw/skills/permission.py +13 -48
- package/python/foxclaw/skills/pipeline.py +21 -53
- package/python/foxclaw/skills/policy.py +4 -10
- package/python/foxclaw/skills/review.py +3 -3
- package/python/frontend/deck/auth.js +0 -7
- package/python/frontend/deck/chat_model_picker.js +0 -54
- package/python/frontend/deck/domain.js +0 -20
- package/python/frontend/deck/main.js +0 -144
- package/python/frontend/deck/markdown.js +0 -4
- package/python/frontend/deck/settings.js +1 -18
- package/python/frontend/deck/state.js +0 -1
- package/python/frontend/deck/util.js +0 -2
- package/python/frontend/deck/visuals.js +0 -22
- package/python/frontend/planet/planet.js +18 -201
- package/python/frontend/v3/assets/index-JKfHlb-a.css +1 -0
- package/python/frontend/v3/assets/index-tN90dpUH.js +34 -0
- package/python/frontend/v3/index.html +3 -3
- package/python/frontend/widget_runtime.js +0 -43
- package/python/frontend/widgets.js +0 -57
- package/python/frontend_src/index.html +1 -1
- package/python/frontend_src/package-lock.json +2 -2
- package/python/frontend_src/package.json +1 -1
- package/python/frontend_src/src/App.vue +3 -7
- package/python/frontend_src/src/api.js +0 -7
- package/python/frontend_src/src/components/ChatDock.vue +27 -39
- package/python/frontend_src/src/components/DocViewer.vue +0 -9
- package/python/frontend_src/src/components/Inspector.vue +0 -6
- package/python/frontend_src/src/components/LeftRail.vue +60 -39
- package/python/frontend_src/src/components/MenuPopover.vue +9 -11
- package/python/frontend_src/src/components/MessageStream.vue +18 -30
- package/python/frontend_src/src/components/ModelPicker.vue +0 -5
- package/python/frontend_src/src/components/QuickIngest.vue +0 -6
- package/python/frontend_src/src/components/Stage.vue +71 -25
- package/python/frontend_src/src/components/ToolMenu.vue +0 -3
- package/python/frontend_src/src/components/TopBar.vue +38 -10
- package/python/frontend_src/src/components/panels/ActivityPanel.vue +0 -2
- package/python/frontend_src/src/components/panels/AgentsPanel.vue +2 -5
- package/python/frontend_src/src/components/panels/KnowledgePanel.vue +0 -16
- package/python/frontend_src/src/components/panels/MemoryPanel.vue +42 -3
- package/python/frontend_src/src/components/panels/PanelBox.vue +6 -7
- package/python/frontend_src/src/components/panels/ReviewPanel.vue +0 -16
- package/python/frontend_src/src/components/panels/SettingsPanel.vue +142 -38
- package/python/frontend_src/src/components/panels/TasksPanel.vue +0 -1
- package/python/frontend_src/src/components/panels/ToolsPanel.vue +125 -19
- package/python/frontend_src/src/lib/fmt.js +0 -4
- 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 +0 -14
- package/python/frontend_src/src/main.js +2 -0
- package/python/frontend_src/src/stores/app.js +17 -32
- package/python/frontend_src/src/stores/chat.js +0 -34
- package/python/frontend_src/src/stores/model.js +0 -8
- package/python/frontend_src/src/stores/panels.js +16 -44
- package/python/frontend_src/src/stores/profile.js +31 -0
- package/python/frontend_src/src/stores/system.js +25 -0
- package/python/frontend_src/vite.config.js +0 -7
- package/python/pyproject.toml +17 -3
- package/python/requirements.txt +311 -0
- package/python/tests/conftest.py +0 -22
- package/python/tests/run_all.py +4 -66
- package/python/tests/test_agent_lifecycle.py +4 -38
- package/python/tests/test_agent_rules.py +81 -0
- package/python/tests/test_ahs_p2.py +4 -25
- package/python/tests/test_anthropic_tools.py +1 -15
- package/python/tests/test_api.py +19 -9
- package/python/tests/test_api_auth.py +1 -5
- package/python/tests/test_attachments.py +9 -25
- package/python/tests/test_audit_chain.py +2 -7
- package/python/tests/test_audit_sink.py +0 -6
- package/python/tests/test_behavior_spec.py +198 -0
- package/python/tests/test_browser.py +2 -6
- package/python/tests/test_browser_request_guard.py +0 -4
- package/python/tests/test_capabilities.py +0 -6
- package/python/tests/test_channel_signing.py +2 -9
- package/python/tests/test_channels.py +1 -11
- package/python/tests/test_config_layering.py +0 -4
- package/python/tests/test_confirm_nonce.py +1 -12
- package/python/tests/test_conflict_dismiss.py +143 -0
- package/python/tests/test_content_guard.py +177 -0
- package/python/tests/test_context_compaction_wiring.py +1 -7
- package/python/tests/test_dispatch.py +0 -6
- package/python/tests/test_domain_evals.py +11 -80
- package/python/tests/test_domain_import.py +0 -6
- package/python/tests/test_domain_reasoners.py +0 -14
- package/python/tests/test_e2e.py +0 -17
- package/python/tests/test_emb_cache.py +7 -17
- package/python/tests/test_embedding_singleton.py +1 -3
- package/python/tests/test_evals_smoke.py +0 -24
- package/python/tests/test_event_spine.py +10 -18
- package/python/tests/test_evidence_semantic.py +1 -14
- package/python/tests/test_evidence_sse.py +3 -6
- package/python/tests/test_file_ingest.py +0 -11
- package/python/tests/test_finance.py +0 -9
- package/python/tests/test_finance_ledger.py +5 -12
- package/python/tests/test_finance_persist.py +8 -12
- package/python/tests/test_gateway.py +3 -22
- package/python/tests/test_governance.py +5 -52
- package/python/tests/test_governance_audit.py +4 -39
- package/python/tests/test_governance_middleware.py +1 -7
- package/python/tests/test_hkc.py +6 -24
- package/python/tests/test_hkc_index_build_manager.py +0 -33
- package/python/tests/test_hkc_manifest_reuse.py +4 -27
- package/python/tests/test_hkc_search_event_sync.py +3 -54
- package/python/tests/test_import_path_guard.py +0 -6
- package/python/tests/test_injection_guard.py +18 -36
- package/python/tests/test_integration.py +5 -29
- package/python/tests/test_itinerary.py +0 -3
- package/python/tests/test_kee_semantic.py +0 -5
- package/python/tests/test_kernel.py +1 -11
- package/python/tests/test_learning.py +3 -12
- package/python/tests/test_llm_admin.py +4 -19
- package/python/tests/test_llm_planner.py +1 -23
- package/python/tests/test_markdown_skills.py +3 -14
- package/python/tests/test_markdown_skills_openclaw.py +4 -9
- package/python/tests/test_mcp_connectors.py +3 -15
- package/python/tests/test_mcp_guardian_discovery.py +2 -6
- package/python/tests/test_mcp_no_memory.py +3 -3
- package/python/tests/test_mcp_policy_gate.py +0 -1
- package/python/tests/test_mcp_server.py +6 -9
- package/python/tests/test_memory_recall.py +1 -4
- package/python/tests/test_metrics.py +0 -8
- package/python/tests/test_multi_chain.py +1 -13
- package/python/tests/test_multimodal.py +5 -18
- package/python/tests/test_negotiation.py +3 -13
- package/python/tests/test_orchestration.py +1 -11
- package/python/tests/test_orchestration_decoupled.py +0 -6
- package/python/tests/test_p3_p5_hardening.py +3 -17
- package/python/tests/test_parallel_dispatch.py +4 -12
- package/python/tests/test_pipeline.py +17 -58
- package/python/tests/test_plan_visible.py +4 -4
- package/python/tests/test_planner.py +0 -7
- package/python/tests/test_policy_gate.py +2 -3
- package/python/tests/test_profile.py +67 -0
- package/python/tests/test_readme_claims.py +163 -0
- package/python/tests/test_reasoner.py +2 -17
- package/python/tests/test_release_check.py +2 -13
- package/python/tests/test_resume_checkpoint.py +7 -15
- package/python/tests/test_routing.py +2 -12
- package/python/tests/test_scheduler.py +7 -10
- package/python/tests/test_search.py +1 -11
- package/python/tests/test_secrets.py +4 -10
- package/python/tests/test_semantic.py +1 -10
- package/python/tests/test_sensitive_write.py +181 -0
- package/python/tests/test_sessions.py +3 -4
- package/python/tests/test_settings.py +2 -18
- package/python/tests/test_shadow_learning.py +3 -43
- package/python/tests/test_shared_write.py +0 -7
- package/python/tests/test_skill_taint_gate.py +1 -10
- package/python/tests/test_skills.py +2 -18
- package/python/tests/test_skills_fetch.py +282 -0
- package/python/tests/test_stats_and_events.py +2 -5
- package/python/tests/test_streaming.py +2 -7
- package/python/tests/test_supervisor_pipeline.py +4 -5
- package/python/tests/test_supervisor_triage.py +4 -10
- package/python/tests/test_taint.py +2 -7
- package/python/tests/test_task_stream.py +1 -12
- package/python/tests/test_thinking_sse.py +0 -1
- package/python/tests/test_tool_center.py +1 -17
- package/python/tests/test_tool_parallel.py +2 -2
- package/python/tests/test_tool_params.py +1 -5
- package/python/tests/test_tool_use.py +0 -13
- package/python/tests/test_toplevel_concurrency.py +3 -4
- package/python/tests/test_tracing.py +7 -41
- package/python/tests/test_truth_crystallizer.py +4 -6
- package/python/tests/test_truth_flow.py +2 -7
- package/python/tests/test_update_check.py +95 -0
- package/python/tests/test_verify_evidence.py +8 -21
- package/python/tests/test_version_sync.py +35 -6
- package/python/tests/test_web_trigger.py +0 -17
- package/python/tools/build_content_lexicon.py +218 -0
- package/python/tools/bundle_web.py +98 -0
- package/python/tools/check_faiss_type.py +0 -1
- package/python/tools/check_log.ps1 +0 -8
- package/python/tools/mypy_baseline.txt +12 -4
- package/python/tools/mypy_ratchet.py +52 -6
- package/python/tools/pack.py +0 -5
- package/python/tools/recheck_conflicts.py +156 -0
- package/python/tools/release_check.py +24 -15
- package/python/tools/restart_foxclaw.ps1 +5 -8
- package/python/tools/shared_write_audit.py +1 -15
- package/python/tools/size_check.py +1 -1
- package/python/tools/strip_comments.py +228 -0
- package/python/vendor/hkc/hkc_api/routes/ability.py +31 -0
- package/python/vendor/hkc/hkc_api/schemas.py +11 -0
- package/python/vendor/hkc/hkc_kee/kee.py +124 -3
- package/python/vendor/hkc/hkc_sdk/base.py +22 -0
- package/python/vendor/hkc/hkc_sdk/direct_client.py +10 -0
- package/python/vendor/hkc/hkc_sdk/http_client.py +19 -0
- package/python/foxclaw/api/app.py +0 -123
- package/python/frontend/v3/assets/index-BD7VPbMV.js +0 -33
- package/python/frontend/v3/assets/index-CAvnYWGH.css +0 -1
|
@@ -20,7 +20,7 @@ from pathlib import Path
|
|
|
20
20
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
21
21
|
|
|
22
22
|
import foxclaw.core.web_search as ws
|
|
23
|
-
ws.needs_search = lambda q: False
|
|
23
|
+
ws.needs_search = lambda q: False
|
|
24
24
|
|
|
25
25
|
from foxclaw import Foxclaw
|
|
26
26
|
from foxclaw.core.thought_chain import ThoughtNode
|
|
@@ -43,7 +43,7 @@ def _slow(system, messages, max_tokens=2048, agent_id=""):
|
|
|
43
43
|
|
|
44
44
|
def _build(data_dir=None):
|
|
45
45
|
os_ = Foxclaw(data_dir=data_dir or tempfile.mkdtemp())
|
|
46
|
-
os_.router.complete = _slow
|
|
46
|
+
os_.router.complete = _slow
|
|
47
47
|
return os_
|
|
48
48
|
|
|
49
49
|
|
|
@@ -55,12 +55,10 @@ def os_(tmp_path_factory):
|
|
|
55
55
|
|
|
56
56
|
@pytest.mark.xfail(
|
|
57
57
|
reason="pre-existing 失败:HKC_EMBEDDING=stub 下 LLM 全部本地 stub 调用极快,并行线程/await 开销反而把并行拖慢于顺序,断言 par<seq*0.75 在 stub 环境不可靠。test_hkc_index_build_manager 等 P1 测试仍覆盖冷构建核心。",
|
|
58
|
-
strict=False,
|
|
58
|
+
strict=False,
|
|
59
59
|
)
|
|
60
60
|
def test_fork_join(os_):
|
|
61
61
|
print("\n【1】并行 fork-join vs 顺序")
|
|
62
|
-
# 深审硬化:register_subagents 已在 setup 期预建 _scheduler/_steering_queue,
|
|
63
|
-
# 并发任务到达前属性就绪 → 消除懒建 check-then-act race
|
|
64
62
|
check(hasattr(os_.supervisor, "_scheduler") and hasattr(os_.supervisor, "_steering_queue"),
|
|
65
63
|
"主管 setup 期预建 scheduler/steering_queue(消除并发懒建 race)")
|
|
66
64
|
r = asyncio.run(os_.supervisor.run_task(PAR_TASK))
|
|
@@ -83,7 +81,7 @@ def test_fork_join(os_):
|
|
|
83
81
|
def test_fault_isolation(os_):
|
|
84
82
|
print("\n【2】故障隔离:分支超时不拖垮整批")
|
|
85
83
|
r = asyncio.run(os_.supervisor.run_task(
|
|
86
|
-
PAR_TASK, context={"parallel_timeout": 0.05}))
|
|
84
|
+
PAR_TASK, context={"parallel_timeout": 0.05}))
|
|
87
85
|
res = r["result"]
|
|
88
86
|
check(res["dispatched_to"] == "parallel", "仍走并行")
|
|
89
87
|
steps = res["parallel_steps"]
|
|
@@ -126,7 +124,6 @@ def test_concurrency_safety(os_):
|
|
|
126
124
|
check(cnt >= N * M, f"ThoughtChain 并发写不丢({cnt} ≥ {N*M})")
|
|
127
125
|
st = pm.stats()
|
|
128
126
|
check(st["by_state"].get("running", 0) == 0, "ProcessManager 无残留 running(状态机并发一致)")
|
|
129
|
-
# os_ 为多个子测试共享,故用 ≥(本测试贡献 N*M 个 completed,叠加此前的)
|
|
130
127
|
check(st["by_state"].get("completed", 0) >= N * M, f"≥{N*M} 进程 completed(并发流转一致)")
|
|
131
128
|
check(len(seen) == N * M, f"SystemBus 事件回调零丢失({len(seen)}/{N*M})")
|
|
132
129
|
|
|
@@ -152,7 +149,6 @@ def test_memory_isolation_concurrent(os_):
|
|
|
152
149
|
for t in ts: t.start()
|
|
153
150
|
for t in ts: t.join()
|
|
154
151
|
|
|
155
|
-
# 从 office 视角召回"保密草稿":只能拿到自己的 + 共享,绝不能泄露 finance/admin 私有
|
|
156
152
|
res = hmr.recall(query="保密草稿", agent_id="office", top_k=10)
|
|
157
153
|
def foreign_private(r):
|
|
158
154
|
tags = r.get("tags", [])
|
|
@@ -174,9 +170,6 @@ def test_gatekeeper_no_race(os_):
|
|
|
174
170
|
|
|
175
171
|
def fire():
|
|
176
172
|
try:
|
|
177
|
-
# 金额取 5000:**远离**任何阈值边界。本测试验的是"并发裁决无竞态",
|
|
178
|
-
# 与阈值定多少无关 —— 原先写 500 恰好卡在 large_purchase_check 边界上
|
|
179
|
-
# (规则是 > 500,500 不大于 500),调整默认阈值就把它误伤了。
|
|
180
173
|
req = CrossAgentRequest(
|
|
181
174
|
from_agent="admin", to_agent="finance", request_type="budget_request",
|
|
182
175
|
payload={"amount": 5000, "category": "travel"}, reason="差旅")
|
|
@@ -192,7 +185,6 @@ def test_gatekeeper_no_race(os_):
|
|
|
192
185
|
for t in ts: t.join()
|
|
193
186
|
|
|
194
187
|
check(not errs, f"并发守门人裁决无异常(否则: {errs[:2]})")
|
|
195
|
-
# 大额确定性触发 large_purchase_check → 全部一致(不写死具体阈值:那是可调项)
|
|
196
188
|
check(set(results) == {"needs_confirmation"},
|
|
197
189
|
f"{N} 次并发裁决结果一致(无竞态): {set(results)}")
|
|
198
190
|
check(len(orch.request_log) - before == N, f"请求日志无丢失(+{N})")
|
|
@@ -32,7 +32,7 @@ class FakeAgent:
|
|
|
32
32
|
def __init__(self, nodes=None, fail_on=None):
|
|
33
33
|
self.thought_chain = _FakeChain(nodes or [])
|
|
34
34
|
self.exec_count = 0
|
|
35
|
-
self._fail_on = fail_on
|
|
35
|
+
self._fail_on = fail_on
|
|
36
36
|
def _recall_query(self, task): return f"q:{task}"
|
|
37
37
|
def _recall_memory(self, query, top_k=5):
|
|
38
38
|
self.last_recall = (query, top_k)
|
|
@@ -57,7 +57,6 @@ def test_default_pipeline():
|
|
|
57
57
|
assert ctx.perceived.get("task_len") == 1, "perceive 记录输入特征"
|
|
58
58
|
assert ctx.plan.get("multi_step") is False and ctx.route.get("target") == "self"
|
|
59
59
|
assert ctx.verified.get("passed") is True, "verify 汇总校验结果到 ctx.verified"
|
|
60
|
-
# 默认 reflect:接受,不重试(behavior-preserving) → _execute 只调 1 次
|
|
61
60
|
assert ctx.reflect["decision"] == "accept" and ctx.reflect["retries"] == 0
|
|
62
61
|
assert a.exec_count == 1, "默认不重试,_execute 只调一次"
|
|
63
62
|
print(f" ✓ recall→plan→route→act→verify→reflect 按序;plan={ctx.plan['flow']} "
|
|
@@ -67,12 +66,10 @@ def test_default_pipeline():
|
|
|
67
66
|
def test_plan_route_stage():
|
|
68
67
|
print("\n【1c】plan/route stage:P1-2 引擎分析结构(informational,不改执行)")
|
|
69
68
|
from foxclaw.pipeline import stage_plan, stage_route
|
|
70
|
-
# 多步任务(顺序连接词)
|
|
71
69
|
ctx = PipelineContext(agent=FakeAgent(), task="先分析支出 然后 给出建议")
|
|
72
70
|
stage_plan(ctx); stage_route(ctx)
|
|
73
71
|
assert ctx.plan["multi_step"] is True and len(ctx.plan["steps"]) == 2, ctx.plan
|
|
74
72
|
assert ctx.plan["flow"] == "sequential" and ctx.route["target"] == "self"
|
|
75
|
-
# 单步任务
|
|
76
73
|
ctx2 = PipelineContext(agent=FakeAgent(), task="分析我的开支")
|
|
77
74
|
stage_plan(ctx2)
|
|
78
75
|
assert ctx2.plan["multi_step"] is False and ctx2.plan["steps"] == ["分析我的开支"]
|
|
@@ -94,7 +91,7 @@ def test_insertable_stage():
|
|
|
94
91
|
print("\n【2】可插桩:插入自定义 stage 按位置执行")
|
|
95
92
|
hits = []
|
|
96
93
|
def stage_probe(ctx):
|
|
97
|
-
hits.append(("probe", len(ctx.memories)))
|
|
94
|
+
hits.append(("probe", len(ctx.memories)))
|
|
98
95
|
stage_probe.stage_name = "probe"
|
|
99
96
|
a = FakeAgent()
|
|
100
97
|
ctx = PipelineContext(agent=a, task="X")
|
|
@@ -133,14 +130,13 @@ def test_stage_events():
|
|
|
133
130
|
print("\n【6】逐 stage 事件:每个 stage emit agent.cognition.<stage> 到脊柱")
|
|
134
131
|
spine = _CapSpine()
|
|
135
132
|
a = FakeAgent()
|
|
136
|
-
a._caps = _Caps(spine)
|
|
133
|
+
a._caps = _Caps(spine)
|
|
137
134
|
ctx = PipelineContext(agent=a, task="E", session_id="s3")
|
|
138
135
|
asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(ctx))
|
|
139
136
|
expected = ["agent.cognition." + n for n in
|
|
140
137
|
["perceive", "recall", "plan", "route", "act", "verify", "reflect"]]
|
|
141
138
|
assert spine.events == expected, f"逐 stage 事件,实得 {spine.events}"
|
|
142
|
-
|
|
143
|
-
a2 = FakeAgent() # 无 _caps
|
|
139
|
+
a2 = FakeAgent()
|
|
144
140
|
asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(PipelineContext(agent=a2, task="X")))
|
|
145
141
|
print(" ✓ 7 个 agent.cognition.* 事件按序 emit;无中枢时优雅跳过")
|
|
146
142
|
|
|
@@ -149,33 +145,25 @@ def test_route_skill_optin_inert():
|
|
|
149
145
|
print("\n【7】A·Step5:route 默认自动选 skill(仅只读+高阈值);可关;有副作用的不自动选")
|
|
150
146
|
from foxclaw.pipeline import stage_route
|
|
151
147
|
from foxclaw.skills import build_default_registry
|
|
152
|
-
reg = build_default_registry()
|
|
148
|
+
reg = build_default_registry()
|
|
153
149
|
|
|
154
|
-
# (a) 【契约变更 2026-07-16】原为"默认关"(behavior-preserving 放量期)。
|
|
155
|
-
# 现默认**开**:此前 route/dispatch 双关导致工具中心/MCP/技能在正常对话里根本够不着
|
|
156
|
-
# (「帮我算 3+5」只会 LLM 硬答)。现默认自动选中**只读**技能。
|
|
157
150
|
a = FakeAgent(); a.skill_registry = reg
|
|
158
151
|
ctx = PipelineContext(agent=a, task="帮我计算 (12+8)*5")
|
|
159
152
|
stage_route(ctx)
|
|
160
153
|
assert ctx.route["target"] == "skill" and ctx.route["skill_id"] == "calculator", \
|
|
161
154
|
f"默认应自动选中只读的 calculator: {ctx.route}"
|
|
162
155
|
|
|
163
|
-
# (a2) 可关(显式 False / 环境变量)→ 回落 self,老行为随时可拿回
|
|
164
156
|
ctx_off = PipelineContext(agent=a, task="帮我计算 (12+8)*5",
|
|
165
157
|
context={"_route_skill": False})
|
|
166
158
|
stage_route(ctx_off)
|
|
167
159
|
assert ctx_off.route["target"] == "self", ctx_off.route
|
|
168
160
|
|
|
169
|
-
# (b) 开启(显式阈值 0.5,避免耦合具体分值):计算类 → router 选中 calculator,target=skill
|
|
170
161
|
ctx2 = PipelineContext(agent=a, task="帮我计算 (12+8)*5",
|
|
171
162
|
context={"_route_skill": {"threshold": 0.5}})
|
|
172
163
|
stage_route(ctx2)
|
|
173
164
|
assert ctx2.route["target"] == "skill" and ctx2.route["skill_id"] == "calculator", ctx2.route
|
|
174
165
|
assert ctx2.route["confidence"] >= 0.5, ctx2.route
|
|
175
166
|
|
|
176
|
-
# (c) 【契约变更】原断言"act 不消费 route"(Step1 铺线期)。Step2 起 act 会真派发;
|
|
177
|
-
# 此处验证:选中 skill 但**无 skill_adapter**(FakeAgent 没挂)→ _act_via_skill 回落
|
|
178
|
-
# _execute,fail-safe 不崩。
|
|
179
167
|
a3 = FakeAgent(); a3.skill_registry = reg
|
|
180
168
|
ctx3 = PipelineContext(agent=a3, task="帮我计算 (12+8)*5",
|
|
181
169
|
context={"_route_skill": {"threshold": 0.5}})
|
|
@@ -184,13 +172,11 @@ def test_route_skill_optin_inert():
|
|
|
184
172
|
assert ctx3.result == "done:帮我计算 (12+8)*5#1" and a3.exec_count == 1, \
|
|
185
173
|
"无 skill_adapter → 回落 _execute(fail-safe)"
|
|
186
174
|
|
|
187
|
-
# (d) 无 skill_registry 的 Agent(finance/admin/office)→ 恒 self(fail-safe 回落)
|
|
188
175
|
a4 = FakeAgent()
|
|
189
176
|
ctx4 = PipelineContext(agent=a4, task="计算 1+1", context={"_route_skill": True})
|
|
190
177
|
stage_route(ctx4)
|
|
191
178
|
assert ctx4.route["target"] == "self", "无 skill_registry → 回落 self"
|
|
192
179
|
|
|
193
|
-
# (e) 高阈值(>1)→ 实际不选,回落 self(阈值可调证明)
|
|
194
180
|
ctx5 = PipelineContext(agent=a, task="帮我计算 1+1",
|
|
195
181
|
context={"_route_skill": {"threshold": 1.01}})
|
|
196
182
|
stage_route(ctx5)
|
|
@@ -209,7 +195,6 @@ def test_act_skill_dispatch_step2():
|
|
|
209
195
|
a = FakeAgent(); a.skill_registry = reg; a.skill_adapter = adapter
|
|
210
196
|
return a
|
|
211
197
|
|
|
212
|
-
# (a) 开启 dispatch + 计算类 → 真正执行 calculator,结果=100,且领域 _execute(LLM) 不被调
|
|
213
198
|
a = mk_agent()
|
|
214
199
|
ctx = PipelineContext(agent=a, task="帮我计算 (12+8)*5",
|
|
215
200
|
context={"_route_skill": {"threshold": 0.5}, "_skill_dispatch": True})
|
|
@@ -219,9 +204,6 @@ def test_act_skill_dispatch_step2():
|
|
|
219
204
|
assert ctx.result["output"]["result"] == 100, ctx.result
|
|
220
205
|
assert a.exec_count == 0, "skill 执行了 → 领域 _execute(LLM) 不再被调(首个真改执行)"
|
|
221
206
|
|
|
222
|
-
# (b) 【安全铁律】开启 dispatch + 发邮件(destructive/send)未确认 → needs_confirmation,零副作用、不回落 LLM
|
|
223
|
-
# 注:默认的只读闸会挡住 gmail_send(有副作用不自动选),故此处**显式** read_only=False
|
|
224
|
-
# 表示"我就是要测有副作用的路由被 PolicyGate 拦住"。
|
|
225
207
|
a2 = mk_agent()
|
|
226
208
|
ctx2 = PipelineContext(agent=a2, task="帮我发送邮件给客户",
|
|
227
209
|
context={"_route_skill": {"threshold": 0.5, "read_only": False},
|
|
@@ -231,37 +213,31 @@ def test_act_skill_dispatch_step2():
|
|
|
231
213
|
assert ctx2.result["status"] == "needs_confirmation" and ctx2.result["pending"] is True, ctx2.result
|
|
232
214
|
assert a2.exec_count == 0, "未确认的危险 skill → 不执行、不回落 LLM(绝不 fail-open)"
|
|
233
215
|
|
|
234
|
-
# (c) 显式关 dispatch(只 route)→ 即便 target=skill,act 仍走 _execute(两道门相互独立)
|
|
235
|
-
# 注:dispatch 现在**默认开**,故此处显式 False 才能验证"关掉"这条路径。
|
|
236
216
|
a3 = mk_agent()
|
|
237
217
|
ctx3 = PipelineContext(agent=a3, task="帮我计算 (12+8)*5",
|
|
238
218
|
context={"_route_skill": {"threshold": 0.5}, "_skill_dispatch": False})
|
|
239
219
|
asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(ctx3))
|
|
240
220
|
assert ctx3.route["target"] == "skill" and a3.exec_count == 1, "dispatch 关 → 回落 _execute"
|
|
241
221
|
|
|
242
|
-
|
|
243
|
-
a4 = FakeAgent(); a4.skill_registry = reg # 有 registry 无 adapter
|
|
222
|
+
a4 = FakeAgent(); a4.skill_registry = reg
|
|
244
223
|
ctx4 = PipelineContext(agent=a4, task="帮我计算 1+1",
|
|
245
224
|
context={"_route_skill": {"threshold": 0.5}, "_skill_dispatch": True})
|
|
246
225
|
asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(ctx4))
|
|
247
226
|
assert a4.exec_count == 1, "无 adapter → 回落 _execute"
|
|
248
227
|
|
|
249
|
-
# (b2) 【新安全闸·A·Step5】默认(只读闸开)下,发邮件这类有副作用的技能**根本不会被自动选中**
|
|
250
|
-
# —— 自动执行不得越过"会改变世界"的线;要用仍可显式 use_skill(那时用户是知情的)。
|
|
251
228
|
a2b = mk_agent()
|
|
252
|
-
ctx2b = PipelineContext(agent=a2b, task="帮我发送邮件给客户")
|
|
229
|
+
ctx2b = PipelineContext(agent=a2b, task="帮我发送邮件给客户")
|
|
253
230
|
asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(ctx2b))
|
|
254
231
|
assert ctx2b.route["target"] == "self", f"有副作用的技能不该被自动选中: {ctx2b.route}"
|
|
255
232
|
assert a2b.exec_count == 1, "未自动动手 → 由 LLM 作答"
|
|
256
233
|
|
|
257
|
-
# (e) 【fail-closed 加固】invoke 意外抛错 → 结构化 error,不崩、不回落 LLM
|
|
258
234
|
class _BoomAdapter:
|
|
259
235
|
def invoke(self, skill, args, confirmed=False, mode="strict"):
|
|
260
236
|
raise RuntimeError("dispatch 内部炸了")
|
|
261
237
|
a5 = FakeAgent(); a5.skill_registry = reg; a5.skill_adapter = _BoomAdapter()
|
|
262
238
|
ctx5 = PipelineContext(agent=a5, task="帮我计算 1+1",
|
|
263
239
|
context={"_route_skill": {"threshold": 0.5}, "_skill_dispatch": True})
|
|
264
|
-
asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(ctx5))
|
|
240
|
+
asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(ctx5))
|
|
265
241
|
assert ctx5.result["status"] == "error" and "异常" in ctx5.result["message"], ctx5.result
|
|
266
242
|
assert a5.exec_count == 0, "dispatch 异常 → fail-closed 结构化 error,绝不回落 LLM"
|
|
267
243
|
print(" ✓ 开启:calculator 真执行(=100,不调LLM);发邮件未确认→needs_confirmation零副作用;"
|
|
@@ -303,35 +279,28 @@ def test_run_task_confirm_suspend_step3():
|
|
|
303
279
|
ag = _Mini(hmr=_FakeHMR(), thought_chain=ThoughtChain(db_path=f"{d}/tc.db"),
|
|
304
280
|
data_dir=d, shadow_rounds=5)
|
|
305
281
|
ag.skill_registry = reg; ag.skill_adapter = adapter
|
|
306
|
-
# read_only=False:本测试专门验证**写入类** skill 的 confirm 挂起路径,
|
|
307
|
-
# 而默认的只读闸不会自动选中它 —— 故显式声明(调用方自担风险的显式选择)。
|
|
308
282
|
base_ctx = {"_route_skill": {"threshold": 0.4, "read_only": False},
|
|
309
283
|
"_skill_dispatch": True}
|
|
310
284
|
|
|
311
|
-
# 未确认:写 skill → run_task 顶层 status=needs_confirmation + pending_action,零副作用
|
|
312
285
|
r1 = asyncio.run(ag.run_task("帮我记一下 买牛奶", dict(base_ctx)))
|
|
313
286
|
assert r1.get("status") == "needs_confirmation", r1
|
|
314
287
|
assert r1.get("pending_action", {}).get("skill_id") == "save_note", r1
|
|
315
288
|
assert saved == [], "未确认 → handler 不执行(零副作用)"
|
|
316
289
|
|
|
317
|
-
# 【深审加固】确认绑定错 skill_id → 仍 needs_confirmation、不执行(防确认漂移)
|
|
318
290
|
r_bad = asyncio.run(ag.run_task("帮我记一下 买牛奶",
|
|
319
291
|
{**base_ctx, "_confirmed": "gmail_send"}))
|
|
320
292
|
assert r_bad.get("status") == "needs_confirmation", r_bad
|
|
321
293
|
assert saved == [], "确认绑定到别的 skill → 不执行本 skill(绑定优先)"
|
|
322
294
|
|
|
323
|
-
# 确认恢复:绑定到正确 skill_id 重跑 → skill 真执行,无 needs_confirmation
|
|
324
295
|
r2 = asyncio.run(ag.run_task("帮我记一下 买牛奶",
|
|
325
296
|
{**base_ctx, "_confirmed": "save_note"}))
|
|
326
297
|
assert r2.get("status") != "needs_confirmation", r2
|
|
327
298
|
assert len(saved) == 1, f"确认后 handler 执行(有副作用),saved={saved}"
|
|
328
299
|
|
|
329
|
-
# 向后兼容:_confirmed=True 概括确认仍生效
|
|
330
300
|
saved.clear()
|
|
331
301
|
r2b = asyncio.run(ag.run_task("帮我记一下 买酸奶", {**base_ctx, "_confirmed": True}))
|
|
332
302
|
assert r2b.get("status") != "needs_confirmation" and len(saved) == 1, (r2b, saved)
|
|
333
303
|
|
|
334
|
-
# 纯 LLM 路径(dispatch 关)→ 无 status 字段(行为逐字不变)
|
|
335
304
|
r3 = asyncio.run(ag.run_task("随便聊聊"))
|
|
336
305
|
assert "status" not in r3 and r3["result"] == "llm:随便聊聊", r3
|
|
337
306
|
print(f" ✓ 未确认→顶层 needs_confirmation 零副作用;_confirmed 重跑→执行(saved={saved});"
|
|
@@ -343,7 +312,7 @@ def test_run_task_confirm_suspend_step3():
|
|
|
343
312
|
def test_reflect_retry_optin():
|
|
344
313
|
print("\n【4】reflect opt-in:漂移超阈值 → 有上限重试")
|
|
345
314
|
from foxclaw.pipeline import stage_verify # noqa
|
|
346
|
-
a = FakeAgent(nodes=[_Node("WARN", 0.8)])
|
|
315
|
+
a = FakeAgent(nodes=[_Node("WARN", 0.8)])
|
|
347
316
|
ctx = PipelineContext(agent=a, task="V",
|
|
348
317
|
context={"_reflect": {"max_retries": 2, "drift_threshold": 0.5}})
|
|
349
318
|
asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(ctx))
|
|
@@ -358,8 +327,8 @@ class _DynChain:
|
|
|
358
327
|
def __init__(self, agent): self.agent = agent
|
|
359
328
|
def get_chain(self, session_id, agent_id=None):
|
|
360
329
|
if self.agent.exec_count <= 1:
|
|
361
|
-
return [_Node("WARN", 0.8)]
|
|
362
|
-
return [_Node("WARN", 0.8), _Node("PASS", 0.2)]
|
|
330
|
+
return [_Node("WARN", 0.8)]
|
|
331
|
+
return [_Node("WARN", 0.8), _Node("PASS", 0.2)]
|
|
363
332
|
|
|
364
333
|
|
|
365
334
|
def test_reflect_early_exit_on_improve():
|
|
@@ -369,10 +338,8 @@ def test_reflect_early_exit_on_improve():
|
|
|
369
338
|
ctx = PipelineContext(agent=a, task="D",
|
|
370
339
|
context={"_reflect": {"max_retries": 3, "drift_threshold": 0.5}})
|
|
371
340
|
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
341
|
assert a.exec_count == 2, f"改善后应只重试 1 次(而非到上限 3),实得 {a.exec_count}"
|
|
374
342
|
assert ctx.reflect["decision"] == "retry_accepted" and ctx.reflect["retries"] == 1, ctx.reflect
|
|
375
|
-
# 关键:此时 max_delta 仍是 0.8(旧节点),若用它会错误地重试到上限;last_delta=0.2 才对
|
|
376
343
|
assert ctx.verified["max_delta"] == 0.8 and ctx.verified["last_delta"] == 0.2
|
|
377
344
|
print(f" ✓ 早退:exec={a.exec_count} reflect={ctx.reflect['decision']} "
|
|
378
345
|
f"(max_delta={ctx.verified['max_delta']} 仍高,但 last_delta={ctx.verified['last_delta']} 达标)")
|
|
@@ -380,7 +347,7 @@ def test_reflect_early_exit_on_improve():
|
|
|
380
347
|
|
|
381
348
|
def test_reflect_failsafe_rollback():
|
|
382
349
|
print("\n【5】reflect fail-safe:重试自身出错 → 回滚到重试前结果")
|
|
383
|
-
a = FakeAgent(nodes=[_Node("WARN", 0.8)], fail_on=2)
|
|
350
|
+
a = FakeAgent(nodes=[_Node("WARN", 0.8)], fail_on=2)
|
|
384
351
|
ctx = PipelineContext(agent=a, task="X",
|
|
385
352
|
context={"_reflect": {"max_retries": 2, "drift_threshold": 0.5}})
|
|
386
353
|
asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(ctx))
|
|
@@ -401,10 +368,10 @@ def test_concurrent_ctx_isolation():
|
|
|
401
368
|
|
|
402
369
|
class SlowAgent(FakeAgent):
|
|
403
370
|
async def _execute(self, task, context, session_id, memories):
|
|
404
|
-
await asyncio.sleep(0.005)
|
|
371
|
+
await asyncio.sleep(0.005)
|
|
405
372
|
return f"done:{task}", {"task_echo": task}
|
|
406
373
|
|
|
407
|
-
a = SlowAgent()
|
|
374
|
+
a = SlowAgent()
|
|
408
375
|
|
|
409
376
|
async def run_one(task):
|
|
410
377
|
ctx = PipelineContext(agent=a, task=task, session_id=f"s_{task}")
|
|
@@ -434,40 +401,35 @@ def test_cooperative_interrupt():
|
|
|
434
401
|
import threading
|
|
435
402
|
print("\n【7】P3 协作式中断:stop_event 在 stage 边界 / 重试迭代前优雅停")
|
|
436
403
|
|
|
437
|
-
# (a) 无 stop_event → 跑满 7 stage、interrupted=False(behavior-preserving)
|
|
438
404
|
a = FakeAgent()
|
|
439
405
|
ctx = PipelineContext(agent=a, task="T")
|
|
440
406
|
asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(ctx))
|
|
441
407
|
assert ctx.interrupted is False and len(ctx.trace) == 7, (ctx.interrupted, ctx.trace)
|
|
442
408
|
|
|
443
|
-
# (b) 起步即置位 → 0 个 stage 执行、interrupted=True、无结果
|
|
444
409
|
ev = threading.Event(); ev.set()
|
|
445
410
|
ctx2 = PipelineContext(agent=FakeAgent(), task="T", stop_event=ev)
|
|
446
411
|
asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(ctx2))
|
|
447
412
|
assert ctx2.interrupted is True and ctx2.trace == [] and ctx2.result is None, \
|
|
448
413
|
(ctx2.interrupted, ctx2.trace, ctx2.result)
|
|
449
414
|
|
|
450
|
-
# (c) 中途置位(自定义 stage 跑完就 set)→ 停在下一边界,后续 stage 全不跑
|
|
451
415
|
ev2 = threading.Event()
|
|
452
416
|
def _tripwire(c): ev2.set()
|
|
453
417
|
_tripwire.stage_name = "tripwire"
|
|
454
|
-
stages = DEFAULT_STAGES[:2] + [_tripwire] + DEFAULT_STAGES[2:]
|
|
418
|
+
stages = DEFAULT_STAGES[:2] + [_tripwire] + DEFAULT_STAGES[2:]
|
|
455
419
|
ctx3 = PipelineContext(agent=FakeAgent(), task="T", stop_event=ev2)
|
|
456
420
|
asyncio.run(CognitivePipeline(stages).run(ctx3))
|
|
457
421
|
assert ctx3.trace == ["perceive", "recall", "tripwire"], ctx3.trace
|
|
458
422
|
assert ctx3.interrupted is True and ctx3.result is None, (ctx3.interrupted, ctx3.result)
|
|
459
423
|
print(f" ✓ 边界中断:无 stop 跑满 7;起步停 0 stage;中途停 trace={ctx3.trace}(后续未跑)")
|
|
460
424
|
|
|
461
|
-
# (d) reflect 重试循环中断:agent 在**首次重试的 _execute** 时置位 → 下一轮迭代前停,
|
|
462
|
-
# 证明重试在用尽 max_retries(=5)之前被截断。这是本管线最接近"agentic loop 每轮检查中断"处。
|
|
463
425
|
ev3 = threading.Event()
|
|
464
426
|
class _StopOnRetryAgent(FakeAgent):
|
|
465
427
|
def __init__(self, e):
|
|
466
|
-
super().__init__(nodes=[_Node("WARN", 0.8)])
|
|
428
|
+
super().__init__(nodes=[_Node("WARN", 0.8)])
|
|
467
429
|
self._e = e
|
|
468
430
|
async def _execute(self, task, context, session_id, memories):
|
|
469
431
|
self.exec_count += 1
|
|
470
|
-
if self.exec_count == 2:
|
|
432
|
+
if self.exec_count == 2:
|
|
471
433
|
self._e.set()
|
|
472
434
|
return f"done:{task}#{self.exec_count}", {"call": self.exec_count}
|
|
473
435
|
a4 = _StopOnRetryAgent(ev3)
|
|
@@ -485,18 +447,15 @@ def test_cooperative_interrupt():
|
|
|
485
447
|
def test_on_stage_callback():
|
|
486
448
|
"""【8】D1:`pipeline.run(on_stage)` 逐 stage 回调(opt-in);无回调=行为不变;回调异常 fail-safe。"""
|
|
487
449
|
print("\n【8】D1 on_stage 回调:逐 stage 通知 + opt-in + fail-safe")
|
|
488
|
-
# (a) 有回调:每 stage 完成后回调一次,顺序与 trace 一致
|
|
489
450
|
seen = []
|
|
490
451
|
ctx = PipelineContext(agent=FakeAgent(), task="T")
|
|
491
452
|
asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(
|
|
492
453
|
ctx, on_stage=lambda name, c: seen.append(name)))
|
|
493
454
|
assert seen == ctx.trace == ["perceive", "recall", "plan", "route", "act", "verify", "reflect"], \
|
|
494
455
|
(seen, ctx.trace)
|
|
495
|
-
# (b) 无回调:行为逐字不变(默认路径)
|
|
496
456
|
ctx2 = PipelineContext(agent=FakeAgent(), task="T")
|
|
497
457
|
asyncio.run(CognitivePipeline(DEFAULT_STAGES).run(ctx2))
|
|
498
458
|
assert len(ctx2.trace) == 7 and ctx2.result == "done:T#1"
|
|
499
|
-
# (c) 回调抛异常:被吞,不影响管线跑完(fail-safe:流式反馈绝不拖垮任务)
|
|
500
459
|
def _boom(name, c):
|
|
501
460
|
raise RuntimeError("回调故障")
|
|
502
461
|
ctx3 = PipelineContext(agent=FakeAgent(), task="T")
|
|
@@ -44,12 +44,12 @@ def test_stage_plan_records_plan_node() -> None:
|
|
|
44
44
|
|
|
45
45
|
|
|
46
46
|
def test_record_plan_node_never_breaks_execution() -> None:
|
|
47
|
-
_record_plan_node(_ctx("x", agent=None))
|
|
47
|
+
_record_plan_node(_ctx("x", agent=None))
|
|
48
48
|
|
|
49
49
|
def _boom(**kwargs):
|
|
50
50
|
raise RuntimeError("harness 挂了")
|
|
51
51
|
|
|
52
|
-
_record_plan_node(_ctx("x", agent=types.SimpleNamespace(_think=_boom)))
|
|
52
|
+
_record_plan_node(_ctx("x", agent=types.SimpleNamespace(_think=_boom)))
|
|
53
53
|
|
|
54
54
|
|
|
55
55
|
def test_plan_payload_accepts_both_shapes() -> None:
|
|
@@ -59,7 +59,7 @@ def test_plan_payload_accepts_both_shapes() -> None:
|
|
|
59
59
|
"multi_step": True}
|
|
60
60
|
sup = types.SimpleNamespace(plan_obj=types.SimpleNamespace(steps=["x", "y"], flow="parallel"))
|
|
61
61
|
assert _plan_payload(sup)["steps"] == ["x", "y"]
|
|
62
|
-
assert _plan_payload(sup)["multi_step"] is True
|
|
62
|
+
assert _plan_payload(sup)["multi_step"] is True
|
|
63
63
|
one = types.SimpleNamespace(plan_obj=types.SimpleNamespace(steps=["x"]))
|
|
64
64
|
assert _plan_payload(one)["multi_step"] is False
|
|
65
65
|
assert _plan_payload(types.SimpleNamespace(plan=None)) is None
|
|
@@ -72,7 +72,7 @@ def test_stream_emits_plan_event_after_plan_stage() -> None:
|
|
|
72
72
|
|
|
73
73
|
async def _core(task, context=None, stop_event=None, on_stage=None):
|
|
74
74
|
on_stage("recall", ctx)
|
|
75
|
-
on_stage("plan", ctx)
|
|
75
|
+
on_stage("plan", ctx)
|
|
76
76
|
return {"result": "done"}
|
|
77
77
|
|
|
78
78
|
fake = types.SimpleNamespace(_run_task_core=_core)
|
|
@@ -38,33 +38,26 @@ def test_trigger_route_engine():
|
|
|
38
38
|
C("web", ["搜索", "查"], "联网 搜索 网页"),
|
|
39
39
|
C("email", ["发邮件", "email"], "发送 邮件"),
|
|
40
40
|
]
|
|
41
|
-
# 否定:"不要计算" → calculator 触发被否定 → 不选它
|
|
42
41
|
r = P.trigger_route("不要计算一下", cands)
|
|
43
42
|
assert r.primary != "calculator", f"否定应排除 calculator,实得 {r.primary}"
|
|
44
|
-
# 命中:"帮我搜索北京" → web
|
|
45
43
|
r2 = P.trigger_route("帮我搜索北京", cands)
|
|
46
44
|
assert r2.primary == "web" and r2.confidence >= 0.15
|
|
47
|
-
# 兜底:完全不相关 → 用最后一个候选兜底,低置信
|
|
48
45
|
r3 = P.trigger_route("xyzzy 无关内容", cands)
|
|
49
46
|
assert r3.primary == cands[-1].id and r3.confidence < 0.15
|
|
50
|
-
# 确定性:空候选
|
|
51
47
|
assert P.trigger_route("x", []).primary is None
|
|
52
48
|
print(f" ✓ 否定排除 / 命中选中 / 兜底(={r3.primary}) / 空候选")
|
|
53
49
|
|
|
54
50
|
|
|
55
51
|
def test_three_sites_delegate():
|
|
56
52
|
print("\n【3】三处委托同一引擎(单一真源)")
|
|
57
|
-
# AHS
|
|
58
53
|
from foxclaw.skills import build_default_registry
|
|
59
54
|
from foxclaw.skills.pipeline import route, plan
|
|
60
55
|
reg = build_default_registry()
|
|
61
56
|
assert route("不要计算", reg).primary != "calculator", "AHS route 否定生效"
|
|
62
57
|
assert len(plan("先算 5+5 然后再算 9*9", reg).tasks) == 2, "AHS plan 拆解"
|
|
63
|
-
# search
|
|
64
58
|
from foxclaw.search.planner import QueryPlanner
|
|
65
59
|
sub = QueryPlanner()._decompose("查A,查B、查C")
|
|
66
60
|
assert len(sub) == 3, "search 拆解走共享原语"
|
|
67
|
-
# supervisor(用引擎的 keyword_hits;此处直接验证原语等价)
|
|
68
61
|
assert P.keyword_hits("买基金理财", ["理财", "基金", "收支"]) == 2
|
|
69
62
|
print(" ✓ AHS / search / 主管 三处均由 foxclaw.planning 单一真源驱动")
|
|
70
63
|
|
|
@@ -17,7 +17,7 @@ sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
|
17
17
|
from foxclaw.kernel import PolicyGate
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
class _HR:
|
|
20
|
+
class _HR:
|
|
21
21
|
def __init__(self, blocked=False, warned=False, reason=""):
|
|
22
22
|
self._b, self._w, self.reason = blocked, warned, reason
|
|
23
23
|
@property
|
|
@@ -31,7 +31,7 @@ class _Harness:
|
|
|
31
31
|
def check(self, node): self.calls += 1; return self.hr
|
|
32
32
|
|
|
33
33
|
|
|
34
|
-
class _PD:
|
|
34
|
+
class _PD:
|
|
35
35
|
def __init__(self, decision, reason=""):
|
|
36
36
|
self.decision, self.stage, self.reason, self.trust_downgrade = decision, 2, reason, False
|
|
37
37
|
|
|
@@ -81,7 +81,6 @@ def test_integration_skill_path():
|
|
|
81
81
|
os_ = Foxclaw(data_dir=d, shadow_rounds=50)
|
|
82
82
|
seen = []
|
|
83
83
|
os_.event_spine.project(lambda e: seen.append(e.topic))
|
|
84
|
-
# 发邮件是副作用 skill → 需确认 → 经 PolicyGate 发 policy.decision.confirm
|
|
85
84
|
os_.supervisor.use_skill("帮我发邮件给客户",
|
|
86
85
|
args={"to": "a@b.com", "subject": "x", "body": "y"})
|
|
87
86
|
assert any(t.startswith("policy.decision") for t in seen), \
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""称呼(user profile)存取:round-trip、改一次不留旧条目、可清空、不误删别的偏好。
|
|
2
|
+
|
|
3
|
+
用 stub 宿主直接测 mixin,不初始化 Agent / API(那条路径由 test_api 覆盖)。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from foxclaw.agents.mixins.preference import PreferenceMixin
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class _Stub(PreferenceMixin):
|
|
12
|
+
AGENT_ID = "stub"
|
|
13
|
+
|
|
14
|
+
def __init__(self):
|
|
15
|
+
self.items: list[dict] = []
|
|
16
|
+
self._seq = 0
|
|
17
|
+
|
|
18
|
+
def _store_memory(self, content, mem_type="concept", **kwargs):
|
|
19
|
+
self._seq += 1
|
|
20
|
+
self.items.insert(0, {"id": f"m{self._seq}", "content": content})
|
|
21
|
+
|
|
22
|
+
def list_preferences(self, limit=50):
|
|
23
|
+
return self.items[:limit]
|
|
24
|
+
|
|
25
|
+
def delete_preference(self, pref_id):
|
|
26
|
+
before = len(self.items)
|
|
27
|
+
self.items = [p for p in self.items if p["id"] != pref_id]
|
|
28
|
+
return len(self.items) < before
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_nickname_roundtrip():
|
|
32
|
+
s = _Stub()
|
|
33
|
+
assert s.get_nickname() == ""
|
|
34
|
+
assert s.set_nickname(" snow ") == "snow"
|
|
35
|
+
assert s.get_nickname() == "snow"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_set_nickname_replaces_old():
|
|
39
|
+
"""改称呼不能留下两条互相矛盾的偏好。"""
|
|
40
|
+
s = _Stub()
|
|
41
|
+
s.set_nickname("snow")
|
|
42
|
+
s.set_nickname("snow2")
|
|
43
|
+
assert [s._nick_of(p["content"]) for p in s.items] == ["snow2"]
|
|
44
|
+
assert s.get_nickname() == "snow2"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_clear_nickname():
|
|
48
|
+
s = _Stub()
|
|
49
|
+
s.set_nickname("snow")
|
|
50
|
+
assert s.set_nickname("") == ""
|
|
51
|
+
assert s.get_nickname() == ""
|
|
52
|
+
assert s.items == []
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_other_preferences_untouched():
|
|
56
|
+
s = _Stub()
|
|
57
|
+
s.remember_preference("回答尽量简短")
|
|
58
|
+
s.set_nickname("snow")
|
|
59
|
+
contents = [p["content"] for p in s.items]
|
|
60
|
+
assert "用户偏好:回答尽量简短" in contents
|
|
61
|
+
assert "用户偏好:称呼我为 snow" in contents
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_nickname_truncated():
|
|
65
|
+
s = _Stub()
|
|
66
|
+
long = "x" * 60
|
|
67
|
+
assert s.set_nickname(long) == long[:24]
|