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
|
@@ -24,10 +24,6 @@ def test_structural_convergence():
|
|
|
24
24
|
for A in (FinanceAgent, OfficeAgent):
|
|
25
25
|
assert A.run_task is CognitiveAgent.run_task, f"{A.__name__} 不应重写 run_task"
|
|
26
26
|
assert A._execute is not CognitiveAgent._execute, f"{A.__name__} 应实现 _execute"
|
|
27
|
-
# 【P1-1】主管也不再重写 run_task:路由分发拆成 SUPERVISOR_STAGES 七个 pipeline
|
|
28
|
-
# stage,走 base 统一模板(获得 hooks/流式/中断/统一 emit)。主管的差异收敛为
|
|
29
|
-
# _cognitive_pipeline 覆盖 + 领域钩子,而非另起生命周期。(本断言此前锁的是
|
|
30
|
-
# "主管保留自身 run_task" 的旧结构,P1-1 落地时同步翻转——又一处"测试锁旧行为"。)
|
|
31
27
|
assert SupervisorAgent.run_task is CognitiveAgent.run_task, \
|
|
32
28
|
"P1-1:主管不再重写 run_task(路由分发在 pipeline stage 里)"
|
|
33
29
|
assert SupervisorAgent._cognitive_pipeline is not CognitiveAgent._cognitive_pipeline, \
|
|
@@ -37,10 +33,8 @@ def test_structural_convergence():
|
|
|
37
33
|
|
|
38
34
|
def test_domain_hooks():
|
|
39
35
|
print("\n【2】领域钩子:entities / recall 前缀 / 召回条数 逐字保留")
|
|
40
|
-
# 钩子是纯函数,可脱离实例校验(lambda(self,task))
|
|
41
36
|
assert FinanceAgent._task_entities(None, "x") == ["财务", "收支", "储蓄"]
|
|
42
37
|
assert FinanceAgent._recall_query(None, "买车") == "财务 买车"
|
|
43
|
-
# 行政并入办公:同一 Agent 同时覆盖学习 + 日程两个域
|
|
44
38
|
assert OfficeAgent._recall_query(None, "开会") == "学习 知识 日程 行程 开会"
|
|
45
39
|
assert OfficeAgent._task_entities(None, "x") == ["学习", "知识", "文档", "日程", "行程", "会议"]
|
|
46
40
|
assert OfficeAgent._recall_topk == 8, "办公学习 Agent 覆盖为广泛召回 8"
|
|
@@ -58,7 +52,6 @@ def test_unified_block_handling():
|
|
|
58
52
|
async def boom(self, task, context, session_id, memories):
|
|
59
53
|
raise HarnessBlockedError(types.SimpleNamespace(reason="模拟越界"), None)
|
|
60
54
|
|
|
61
|
-
# 只替换 finance 的领域执行,模板的兜底应把它转成结构化错误
|
|
62
55
|
os_.finance._execute = types.MethodType(boom, os_.finance)
|
|
63
56
|
r = asyncio.run(os_.finance.run_task("危险任务"))
|
|
64
57
|
assert r.get("error") == "harness_blocked", f"应兜底为 harness_blocked,实得 {r}"
|
|
@@ -81,16 +74,13 @@ def test_finance_verify_before_recommend():
|
|
|
81
74
|
from foxclaw import Foxclaw
|
|
82
75
|
os_ = Foxclaw(data_dir=d, shadow_rounds=50)
|
|
83
76
|
fin = os_.finance
|
|
84
|
-
# 给账本内核灌入真实交易 → analyze() 有据可依(当前数据源存在)
|
|
85
77
|
os_.finance_runtime.import_csv(
|
|
86
78
|
str(Path(__file__).parent.parent / "examples" / "sample_transactions.csv"))
|
|
87
|
-
# 捕获传给 LLM 的 prompt(不真调模型)
|
|
88
79
|
captured = {}
|
|
89
80
|
async def _capture(self, task, messages):
|
|
90
81
|
captured["prompt"] = messages[-1]["content"]
|
|
91
82
|
return "(模拟回答)"
|
|
92
83
|
fin._call_llm_web = types.MethodType(_capture, fin)
|
|
93
|
-
# 一条**过时**召回记忆:若被当作事实会误导用户
|
|
94
84
|
stale = [{"content": "历史:上月预算结余 8000 元", "id": "m1"}]
|
|
95
85
|
asyncio.run(fin._execute("这个月还能花多少?", {}, "s1", stale))
|
|
96
86
|
p = captured["prompt"]
|
|
@@ -115,32 +105,23 @@ def test_run_task_stream():
|
|
|
115
105
|
try:
|
|
116
106
|
from foxclaw import Foxclaw
|
|
117
107
|
os_ = Foxclaw(data_dir=d, shadow_rounds=50)
|
|
118
|
-
# 单 Agent 认知管线(7 stage)用领域 Agent 验证:主管走的是 meta/schedule/dispatch
|
|
119
|
-
# 另一条管线,不能拿它当"标准 stage 序列"的样本。
|
|
120
108
|
ag = os_.office
|
|
121
|
-
ag.router.provider = "stub"
|
|
122
|
-
# 是否配了 LLM key 影响),且 stub 的 router.stream 走 _chunk_text
|
|
123
|
-
# 无损分块 → 可断言"流式 token 拼接 == 最终答案"
|
|
109
|
+
ag.router.provider = "stub"
|
|
124
110
|
|
|
125
|
-
# (a) 正常流式:collect 所有事件
|
|
126
111
|
events = list(ag.run_task_stream("帮我总结一下今天"))
|
|
127
112
|
stages = [e["stage"] for e in events if "stage" in e]
|
|
128
113
|
deltas = [e["delta"] for e in events if "delta" in e]
|
|
129
114
|
results = [e["result"] for e in events if "result" in e]
|
|
130
115
|
assert stages == ["perceive", "recall", "plan", "route", "act", "verify", "reflect"], stages
|
|
131
|
-
# delta 非空 = _act_stream contextvar 成功跨线程/asyncio 传到了 _call_llm(核心正确性证明)
|
|
132
116
|
assert len(deltas) > 0, "act 应逐 token 出字;为空说明 contextvar 没传到 _call_llm"
|
|
133
117
|
assert len(results) == 1, f"应恰有一个最终 result,实得 {len(results)}"
|
|
134
118
|
res = results[0]
|
|
135
119
|
assert res.get("agent") == ag.AGENT_ID and "session_id" in res and "result" in res
|
|
136
|
-
# 拼接的流式 token == 最终答案(token 透传忠实;_chunk_text 无损)
|
|
137
120
|
assert "".join(deltas) == res["result"], "流式 token 拼接应等于最终答案"
|
|
138
121
|
|
|
139
|
-
# (b) 行为保持:非流式 run_task 仍正常、返回体无 delta 泄漏
|
|
140
122
|
plain = asyncio.run(ag.run_task("帮我总结一下今天"))
|
|
141
123
|
assert "result" in plain and "delta" not in plain and plain.get("agent") == ag.AGENT_ID
|
|
142
124
|
|
|
143
|
-
# (c) stop_event 预置 → 流式在起步即停:无 stage、无 delta、result.interrupted
|
|
144
125
|
import threading as _th
|
|
145
126
|
ev = _th.Event(); ev.set()
|
|
146
127
|
ev_events = list(ag.run_task_stream("这个任务会被中断", stop_event=ev))
|
|
@@ -149,8 +130,6 @@ def test_run_task_stream():
|
|
|
149
130
|
assert ev_stages == [], f"预置 stop → 0 个 stage 事件,实得 {ev_stages}"
|
|
150
131
|
assert len(ev_results) == 1 and ev_results[0].get("interrupted") is True, ev_results
|
|
151
132
|
|
|
152
|
-
# (d) fail-safe:act 流式若抛异常 → 降级非流式 complete、返回正常(不崩、不中断任务)。
|
|
153
|
-
# 锁住 _call_llm docstring 承诺的"流式任何异常 → fail-safe 降级 complete"。
|
|
154
133
|
def _boom(*a, **k):
|
|
155
134
|
raise RuntimeError("流式炸了")
|
|
156
135
|
ag.router.stream = _boom
|
|
@@ -193,12 +172,10 @@ def test_interrupted_task_not_sunk():
|
|
|
193
172
|
captured["blocked"] = blocked
|
|
194
173
|
return orig(task, result, blocked=blocked, context=context)
|
|
195
174
|
ag._emit_execution = _spy
|
|
196
|
-
# (a) 预置 stop → 中断 → blocked=True(不沉淀部分/空结果为真值)
|
|
197
175
|
ev = _th.Event(); ev.set()
|
|
198
176
|
r = asyncio.run(ag.run_task("会被中断的任务", stop_event=ev))
|
|
199
177
|
assert r.get("interrupted") is True, r
|
|
200
178
|
assert captured.get("blocked") is True, "中断任务必须 blocked=True(不把部分/空结果当完成真值沉淀)"
|
|
201
|
-
# (b) 正常任务 → blocked=False(behavior-preserving:照常沉淀)
|
|
202
179
|
captured.clear()
|
|
203
180
|
r2 = asyncio.run(ag.run_task("正常任务"))
|
|
204
181
|
assert "interrupted" not in r2 and captured.get("blocked") is False, \
|
|
@@ -220,7 +197,7 @@ def test_reasoning_lifecycle_hooks():
|
|
|
220
197
|
try:
|
|
221
198
|
from foxclaw import Foxclaw
|
|
222
199
|
os_ = Foxclaw(data_dir=d, shadow_rounds=50)
|
|
223
|
-
ag = os_.office
|
|
200
|
+
ag = os_.office
|
|
224
201
|
ag.router.provider = "stub"
|
|
225
202
|
seen = {}
|
|
226
203
|
def _cap_complete(system, messages, max_tokens, agent_id):
|
|
@@ -228,13 +205,11 @@ def test_reasoning_lifecycle_hooks():
|
|
|
228
205
|
return "(stub 回答)"
|
|
229
206
|
ag.router.complete = _cap_complete
|
|
230
207
|
|
|
231
|
-
# (a) 无钩子基线:不注入(behavior-preserving)
|
|
232
208
|
asyncio.run(ag.run_task("基线任务"))
|
|
233
209
|
base_msgs = seen.get("messages", [])
|
|
234
210
|
assert base_msgs and "【E2注入】" not in base_msgs[0].get("content", ""), \
|
|
235
211
|
f"无 pre_reasoning 钩子时不应注入,实得 {base_msgs[:1]}"
|
|
236
212
|
|
|
237
|
-
# (b) 注册 pre_reasoning(注入)+ task_complete(观测)
|
|
238
213
|
completed = []
|
|
239
214
|
ag.hooks.register("pre_reasoning", lambda c: {"additional_context": "【E2注入】优先考虑省钱"})
|
|
240
215
|
ag.hooks.register("task_complete", lambda c: completed.append(c.get("result")))
|
|
@@ -242,13 +217,11 @@ def test_reasoning_lifecycle_hooks():
|
|
|
242
217
|
msgs = seen.get("messages", [])
|
|
243
218
|
joined = " ".join(m.get("content", "") for m in msgs)
|
|
244
219
|
assert "【E2注入】优先考虑省钱" in joined, f"pre_reasoning 注入应进入 LLM messages,实得 {msgs}"
|
|
245
|
-
# A3:注入须合并进 user 消息,**不得产生连续两条 user**(模型无关铁律,某些端点会拒连续 user)
|
|
246
220
|
roles = [m.get("role") for m in msgs]
|
|
247
221
|
assert not any(roles[i] == roles[i + 1] == "user" for i in range(len(roles) - 1)), \
|
|
248
222
|
f"注入不应产生连续两条 user 消息(违反模型无关铁律),roles={roles}"
|
|
249
223
|
assert len(completed) == 1, f"task_complete 应触发一次并收到 result,实得 {len(completed)}"
|
|
250
224
|
|
|
251
|
-
# (c) HarnessBlock 结束路径:task_complete 也触发(观测被拦截任务,带 error)
|
|
252
225
|
import types
|
|
253
226
|
async def _boom(self, task, context, session_id, memories):
|
|
254
227
|
raise HarnessBlockedError(types.SimpleNamespace(reason="E2越界"), None)
|
|
@@ -274,32 +247,28 @@ def test_preference_priority_recall():
|
|
|
274
247
|
try:
|
|
275
248
|
from foxclaw import Foxclaw
|
|
276
249
|
os_ = Foxclaw(data_dir=d, shadow_rounds=50)
|
|
277
|
-
ag = os_.office
|
|
250
|
+
ag = os_.office
|
|
278
251
|
ag.router.provider = "stub"
|
|
279
252
|
seen = {}
|
|
280
253
|
def _cap(system, messages, max_tokens, agent_id):
|
|
281
254
|
seen["messages"] = messages; return "(stub)"
|
|
282
255
|
ag.router.complete = _cap
|
|
283
256
|
|
|
284
|
-
# (a) 明确偏好表达 → 存成带 preference tag(shared)
|
|
285
257
|
asyncio.run(ag.run_task("我喜欢简洁直接的回答,不要长篇大论"))
|
|
286
258
|
prefs = ag.hmr.recall_by_tag(agent_id="supervisor", tag="preference", limit=8)
|
|
287
259
|
assert any("简洁" in p.get("content", "") for p in prefs), f"偏好应带 preference tag 存下,实得 {prefs}"
|
|
288
260
|
|
|
289
|
-
# (b) 发**语义完全不相关**的任务 → 偏好仍注入 prompt(纯语义召回此时召不到)
|
|
290
261
|
seen.clear()
|
|
291
262
|
asyncio.run(ag.run_task("解释一下光合作用的原理"))
|
|
292
263
|
joined = " ".join(m.get("content", "") for m in seen.get("messages", []))
|
|
293
264
|
assert "用户偏好" in joined and "简洁" in joined, \
|
|
294
265
|
f"语义不相关任务也应注入偏好(越用越懂你),实得 {seen.get('messages')}"
|
|
295
266
|
|
|
296
|
-
# (c) 无偏好信号的普通任务不误存(保守采集)
|
|
297
267
|
before = len(ag.hmr.recall_by_tag(agent_id="assistant", tag="preference"))
|
|
298
268
|
asyncio.run(ag.run_task("光合作用发生在植物的哪个部位"))
|
|
299
269
|
after = len(ag.hmr.recall_by_tag(agent_id="assistant", tag="preference"))
|
|
300
270
|
assert after == before, "无偏好信号的任务不应新增偏好"
|
|
301
271
|
|
|
302
|
-
# (d) 关采集开关 → 偏好句也不存(可关)
|
|
303
272
|
os.environ["FOXCLAW_PREF_CAPTURE"] = "0"
|
|
304
273
|
try:
|
|
305
274
|
asyncio.run(ag.run_task("我喜欢用表格展示数据"))
|
|
@@ -308,16 +277,14 @@ def test_preference_priority_recall():
|
|
|
308
277
|
finally:
|
|
309
278
|
os.environ.pop("FOXCLAW_PREF_CAPTURE", None)
|
|
310
279
|
|
|
311
|
-
# (e) 对话路径(stream_reply)也注入偏好——之前只测了任务路径(深审 F2:claim 必测)
|
|
312
280
|
seen_stream = []
|
|
313
281
|
def _cap_stream(system, messages, *a, **k):
|
|
314
282
|
seen_stream.append(messages); yield "(stub)"
|
|
315
283
|
ag.router.stream = _cap_stream
|
|
316
|
-
list(ag.stream_reply("随便讲点什么"))
|
|
284
|
+
list(ag.stream_reply("随便讲点什么"))
|
|
317
285
|
js = " ".join(m.get("content", "") for m in (seen_stream[0] if seen_stream else []))
|
|
318
286
|
assert "用户偏好" in js and "简洁" in js, f"对话路径(stream_reply)也应注入偏好,实得 {seen_stream}"
|
|
319
287
|
|
|
320
|
-
# (f) 去重:同一偏好存多条,召回只返一条(深审 F1)
|
|
321
288
|
for _ in range(3):
|
|
322
289
|
ag._store_memory("用户偏好:我喜欢简洁直接的回答,不要长篇大论",
|
|
323
290
|
mem_type="preference", tags=["preference"], shared=True)
|
|
@@ -325,7 +292,6 @@ def test_preference_priority_recall():
|
|
|
325
292
|
same = [p for p in got if "简洁直接的回答" in p.get("content", "")]
|
|
326
293
|
assert len(same) == 1, f"重复偏好召回应去重为 1 条,实得 {len(same)}"
|
|
327
294
|
|
|
328
|
-
# (g) 删偏好途径(F3):存唯一偏好 → list 找到(带 id)→ delete → 确认删除
|
|
329
295
|
ag._store_memory("用户偏好:请用英文回复", mem_type="preference", tags=["preference"], shared=True)
|
|
330
296
|
target = [p for p in ag.list_preferences() if "英文" in p.get("content", "")]
|
|
331
297
|
assert target and target[0].get("id"), f"唯一偏好应可列出带 id,实得 {target}"
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agent 规则文件加载测试 —— 全离线。
|
|
3
|
+
|
|
4
|
+
覆盖:
|
|
5
|
+
1. agents/rules/<AGENT_ID>/*.md 在组装系统提示词时被读入(启动即生效,不靠代码常驻)。
|
|
6
|
+
2. 叠加顺序:内核前缀 → 角色职责 → 本 Agent 规则;规则位于最后。
|
|
7
|
+
3. 回退开关 FOXCLAW_AGENT_RULES=0 可整体关闭;无 AGENT_ID / 无目录时不注入。
|
|
8
|
+
4. 规则目录必须落在 foxclaw 包内(随 wheel 分发,不能像仓库根 skills/ 那样漏包)。
|
|
9
|
+
"""
|
|
10
|
+
import os, sys, types
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
os.environ.setdefault("HF_HUB_OFFLINE", "1")
|
|
13
|
+
os.environ.setdefault("TRANSFORMERS_OFFLINE", "1")
|
|
14
|
+
os.environ["FOXCLAW_EMBEDDING_MODEL"] = "foxclaw-nonexistent-fast"
|
|
15
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
16
|
+
|
|
17
|
+
from foxclaw.agents.base import KERNEL_PROMPT
|
|
18
|
+
from foxclaw.agents.mixins.prompt import PromptMixin, RULES_DIR, load_agent_rules
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _fake(role="我是财务角色ROLEXYZ。", agent_id="finance"):
|
|
22
|
+
return types.SimpleNamespace(_role_prompt=role, AGENT_ID=agent_id)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_finance_rules_file_is_loaded():
|
|
26
|
+
text = load_agent_rules("finance")
|
|
27
|
+
assert "Personal Finance Agent Rules v1.0" in text
|
|
28
|
+
assert "不得编造" in text
|
|
29
|
+
assert "储蓄率" in text
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_unknown_agent_has_no_rules():
|
|
33
|
+
assert load_agent_rules("no_such_agent") == ""
|
|
34
|
+
assert load_agent_rules("") == ""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_system_prompt_order_kernel_role_rules():
|
|
38
|
+
out = PromptMixin.system_prompt.fget(_fake())
|
|
39
|
+
assert KERNEL_PROMPT in out
|
|
40
|
+
assert out.index("认知操作系统") < out.index("ROLEXYZ") < out.index("Personal Finance Agent Rules")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_rules_follow_kernel_when_role_is_short():
|
|
44
|
+
out = PromptMixin.system_prompt.fget(_fake(role="ROLEXYZ"))
|
|
45
|
+
assert "本 Agent 专业规则(必须遵守)" in out
|
|
46
|
+
assert "月度结余" in out
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_switch_disables_rules(monkeypatch):
|
|
50
|
+
monkeypatch.setenv("FOXCLAW_AGENT_RULES", "0")
|
|
51
|
+
out = PromptMixin.system_prompt.fget(_fake())
|
|
52
|
+
assert "ROLEXYZ" in out and "Personal Finance Agent Rules" not in out
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_switch_disables_rules_even_without_kernel(monkeypatch):
|
|
56
|
+
monkeypatch.setenv("FOXCLAW_KERNEL_PROMPT", "0")
|
|
57
|
+
monkeypatch.setenv("FOXCLAW_AGENT_RULES", "0")
|
|
58
|
+
monkeypatch.setenv("FOXCLAW_BEHAVIOR", "0")
|
|
59
|
+
assert PromptMixin.system_prompt.fget(_fake(role="ROLEXYZ")) == "ROLEXYZ"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_rules_survive_kernel_off(monkeypatch):
|
|
63
|
+
monkeypatch.setenv("FOXCLAW_KERNEL_PROMPT", "0")
|
|
64
|
+
out = PromptMixin.system_prompt.fget(_fake(role="ROLEXYZ"))
|
|
65
|
+
assert KERNEL_PROMPT not in out
|
|
66
|
+
assert "Personal Finance Agent Rules" in out
|
|
67
|
+
# 行为规范是硬边界,不随内核前缀一起关掉
|
|
68
|
+
assert out.index("[行为规范") < out.index("ROLEXYZ")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_agent_without_id_gets_no_rules():
|
|
72
|
+
fake = types.SimpleNamespace(_role_prompt="ROLEXYZ")
|
|
73
|
+
out = PromptMixin.system_prompt.fget(fake)
|
|
74
|
+
assert "ROLEXYZ" in out and "Personal Finance Agent Rules" not in out
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_rules_dir_lives_inside_package():
|
|
78
|
+
"""规则文件必须在 foxclaw 包内:仓库根的 skills/ 就没被打进 wheel,别重演。"""
|
|
79
|
+
pkg = Path(RULES_DIR).resolve()
|
|
80
|
+
assert pkg.name == "rules" and pkg.parent.name == "agents"
|
|
81
|
+
assert (pkg / "finance" / "RULES.md").is_file()
|
|
@@ -33,7 +33,6 @@ def check(cond, label):
|
|
|
33
33
|
print(f" ✗ {label}")
|
|
34
34
|
|
|
35
35
|
|
|
36
|
-
# ═══════════════ [1]-[3] P2-2 compose pending ═══════════════
|
|
37
36
|
from foxclaw.skills import SkillRegistry, SkillSpec, SkillAdapter
|
|
38
37
|
from foxclaw.skills.pipeline import compose, plan, AHSPipeline
|
|
39
38
|
|
|
@@ -58,7 +57,7 @@ def _make_pending_registry(counter: dict):
|
|
|
58
57
|
reg.register(SkillSpec(
|
|
59
58
|
id="send_email", name="发邮件", description="发邮件",
|
|
60
59
|
kind="local", triggers=["发送", "邮件"], side_effect="send",
|
|
61
|
-
destructive=True,
|
|
60
|
+
destructive=True,
|
|
62
61
|
handler=lambda to="": {"sent": to}, params={"to": "str"}))
|
|
63
62
|
return reg
|
|
64
63
|
|
|
@@ -92,8 +91,6 @@ def test_compose_pending():
|
|
|
92
91
|
|
|
93
92
|
def test_compose_failed_still_rolls_back():
|
|
94
93
|
print("\n【1b】对照:P2-2 后真失败(not_connected)仍触发回滚")
|
|
95
|
-
# confirmed=True:write/send 都过权限;第二步 handler=None → dispatch not_connected
|
|
96
|
-
# → 真失败 → 回滚第一步(write 副作用,有 compensate)。若误走 pending 则不回滚。
|
|
97
94
|
reg = SkillRegistry()
|
|
98
95
|
rolled = []
|
|
99
96
|
reg.register(SkillSpec(
|
|
@@ -104,7 +101,7 @@ def test_compose_failed_still_rolls_back():
|
|
|
104
101
|
reg.register(SkillSpec(
|
|
105
102
|
id="send_email", name="发邮件", description="发邮件",
|
|
106
103
|
kind="local", triggers=["发送", "邮件"], side_effect="send",
|
|
107
|
-
handler=None,
|
|
104
|
+
handler=None,
|
|
108
105
|
params={"to": "str"}))
|
|
109
106
|
adp = SkillAdapter(reg)
|
|
110
107
|
res = compose(plan("创建日程 然后 发送邮件", reg), reg, adp, confirmed=True)
|
|
@@ -151,7 +148,6 @@ def test_ahs_pipeline_pending():
|
|
|
151
148
|
"composition 字典带 pending_confirmation/completed_steps,未回滚")
|
|
152
149
|
|
|
153
150
|
|
|
154
|
-
# ═══════════════ [4] P2-1 run_async ═══════════════
|
|
155
151
|
def test_run_async():
|
|
156
152
|
print("\n【4】P2-1:run_async 异步入口")
|
|
157
153
|
reg = SkillRegistry()
|
|
@@ -171,24 +167,19 @@ def test_run_async():
|
|
|
171
167
|
pipe = AHSPipeline(reg, SkillAdapter(reg))
|
|
172
168
|
|
|
173
169
|
async def _main():
|
|
174
|
-
# a) 返回 PipelineResult
|
|
175
170
|
r = await pipe.run_async("计算 1+1")
|
|
176
171
|
check(r.task == "计算 1+1" and (r.success or r.is_pending),
|
|
177
172
|
"run_async 返回 PipelineResult")
|
|
178
|
-
# b) 不阻塞事件循环:两个 0.5s 慢任务并发,若串行 ≥1.0s,并发 ≈0.5s
|
|
179
|
-
# (阈值 0.9s:串行/并发区分度大,且留足调度抖动余量——0.3/0.5 的窄距实测会偶抖)
|
|
180
173
|
t0 = time.perf_counter()
|
|
181
174
|
await asyncio.gather(pipe.run_async("慢 1+1"), pipe.run_async("慢 2+2"))
|
|
182
175
|
elapsed = time.perf_counter() - t0
|
|
183
176
|
check(elapsed < 0.9,
|
|
184
177
|
f"并发两个慢 run_async 耗时 {elapsed:.2f}s < 0.9s(未阻塞事件循环)")
|
|
185
178
|
asyncio.run(_main())
|
|
186
|
-
# c) 同步 run 向后兼容
|
|
187
179
|
r_sync = pipe.run("计算 1+1")
|
|
188
180
|
check(r_sync.task == "计算 1+1", "同步 run 仍可用(向后兼容)")
|
|
189
181
|
|
|
190
182
|
|
|
191
|
-
# ═══════════════ [5]-[7] P2-3 idempotency ═══════════════
|
|
192
183
|
from foxclaw.core.idempotency import IdempotencyGuard
|
|
193
184
|
|
|
194
185
|
|
|
@@ -204,7 +195,7 @@ def test_idem_append_log():
|
|
|
204
195
|
print("\n【5】P2-3:追加日志(O(1) 写)+ compact 合并 + 重启回放")
|
|
205
196
|
d = tempfile.mkdtemp(prefix="idem_a_")
|
|
206
197
|
path = os.path.join(d, "idem.json")
|
|
207
|
-
g = IdempotencyGuard(persist_path=path, compact_every=1000)
|
|
198
|
+
g = IdempotencyGuard(persist_path=path, compact_every=1000)
|
|
208
199
|
g.mark("k1"); g.mark("k2"); g.mark("k3")
|
|
209
200
|
log_path = path + ".log"
|
|
210
201
|
check(os.path.exists(log_path), "mark 产生追加日志")
|
|
@@ -213,7 +204,6 @@ def test_idem_append_log():
|
|
|
213
204
|
check(len(lines) == 3, f"3 次 mark = 3 行日志(实为 {len(lines)}),非全量写")
|
|
214
205
|
check(not os.path.exists(path), "未达阈值不写全量快照")
|
|
215
206
|
|
|
216
|
-
# compact 合并:compact_every=5,第 5 次 mark 触发(轮转日志→快照;新 append 开新文件)
|
|
217
207
|
d2 = tempfile.mkdtemp(prefix="idem_b_")
|
|
218
208
|
path2 = os.path.join(d2, "idem.json")
|
|
219
209
|
g2 = IdempotencyGuard(persist_path=path2, compact_every=5)
|
|
@@ -225,13 +215,11 @@ def test_idem_append_log():
|
|
|
225
215
|
with open(path2, encoding="utf-8") as f:
|
|
226
216
|
data = json.load(f)
|
|
227
217
|
check(len(data) == 5, f"快照含全部 5 条(实为 {len(data)})")
|
|
228
|
-
# compact 之后的新 mark 进新日志;重启 = 读快照(5) + 回放新日志(1) = 6
|
|
229
218
|
g2.mark("k5")
|
|
230
219
|
g2b = IdempotencyGuard(persist_path=path2, compact_every=10000)
|
|
231
220
|
check(all(g2b.seen(f"k{i}") for i in range(6)),
|
|
232
221
|
"compact 后继续 mark:重启回放 = 快照5 + 新日志1 = 6 全可见")
|
|
233
222
|
|
|
234
|
-
# 重启回放:g3 不写快照(阈值大),g4 启动 = 读快照(无)+ 回放日志
|
|
235
223
|
d3 = tempfile.mkdtemp(prefix="idem_c_")
|
|
236
224
|
path3 = os.path.join(d3, "idem.json")
|
|
237
225
|
g3 = IdempotencyGuard(persist_path=path3, compact_every=10000)
|
|
@@ -243,7 +231,6 @@ def test_idem_append_log():
|
|
|
243
231
|
|
|
244
232
|
def test_idem_compat_and_eviction():
|
|
245
233
|
print("\n【6】P2-3:旧快照兼容 + 容量淘汰发 delete(重启不复活)")
|
|
246
|
-
# 旧格式:纯全量快照,无 .log → 直接可读
|
|
247
234
|
d = tempfile.mkdtemp(prefix="idem_d_")
|
|
248
235
|
path = os.path.join(d, "idem.json")
|
|
249
236
|
with open(path, "w", encoding="utf-8") as f:
|
|
@@ -252,7 +239,6 @@ def test_idem_compat_and_eviction():
|
|
|
252
239
|
check(g.seen("legacy_k") and g.get_result("legacy_k") == {"x": 1},
|
|
253
240
|
"旧版纯快照格式兼容(无日志,回放为空)")
|
|
254
241
|
|
|
255
|
-
# 容量淘汰必须落 delete:否则重启回放 mark 日志会"复活"被淘汰的 key
|
|
256
242
|
d2 = tempfile.mkdtemp(prefix="idem_e_")
|
|
257
243
|
path2 = os.path.join(d2, "idem.json")
|
|
258
244
|
g2 = IdempotencyGuard(persist_path=path2, max_size=2, compact_every=10000)
|
|
@@ -277,7 +263,7 @@ def test_idem_persist_failure():
|
|
|
277
263
|
return orig_open(*a, **kw)
|
|
278
264
|
builtins.open = _bad_open
|
|
279
265
|
try:
|
|
280
|
-
g.mark("k1")
|
|
266
|
+
g.mark("k1")
|
|
281
267
|
finally:
|
|
282
268
|
builtins.open = orig_open
|
|
283
269
|
check(g.seen("k1"), "持久化失败不影响内存主流程")
|
|
@@ -289,7 +275,6 @@ def test_idem_persist_failure():
|
|
|
289
275
|
"stats() 含新字段(无 persist_path 也一样)")
|
|
290
276
|
|
|
291
277
|
|
|
292
|
-
# ═══════════════ [8]-[9] P2-4 recovery ═══════════════
|
|
293
278
|
from foxclaw.core.recovery import RestartRecovery, RecoveryPlan
|
|
294
279
|
|
|
295
280
|
|
|
@@ -313,7 +298,6 @@ def test_recovery_plan():
|
|
|
313
298
|
"details 逐任务带 action")
|
|
314
299
|
evts = [e for e in spine.events if e[0] == "recovery.plan"]
|
|
315
300
|
check(len(evts) == 3, f"每个分类决定发 recovery.plan 事件(实为 {len(evts)})")
|
|
316
|
-
# scan_interrupted 返回类型不变(兼容)
|
|
317
301
|
check(isinstance(r2.scan_interrupted(), list), "scan_interrupted 仍返回 list(兼容)")
|
|
318
302
|
|
|
319
303
|
|
|
@@ -330,7 +314,6 @@ def test_recovery_details():
|
|
|
330
314
|
check(records["t1"].status == "in_progress" and records["t1"].description == "second",
|
|
331
315
|
"重复 start 保留最后一次")
|
|
332
316
|
|
|
333
|
-
# compact_log:started 很早、finished 刚刚 → 旧逻辑(只看 started)会丢,新逻辑保留
|
|
334
317
|
d2 = tempfile.mkdtemp(prefix="recv_c_")
|
|
335
318
|
log2 = os.path.join(d2, "log.jsonl")
|
|
336
319
|
old = time.time() - 30 * 86400
|
|
@@ -345,20 +328,17 @@ def test_recovery_details():
|
|
|
345
328
|
"compact 用 max(started,finished):started 30 天前但刚完成的记录不丢")
|
|
346
329
|
check(r2._replay()["t_old"].finished > 0, "compact 保留 finished 时间戳")
|
|
347
330
|
|
|
348
|
-
# fsync=False 可用(高频场景开关)
|
|
349
331
|
d3 = tempfile.mkdtemp(prefix="recv_d_")
|
|
350
332
|
log3 = os.path.join(d3, "log.jsonl")
|
|
351
333
|
r3 = RestartRecovery(log3, boot_id="boot1", fsync=False)
|
|
352
334
|
r3.task_started("t1", "x")
|
|
353
335
|
r3.task_finished("t1")
|
|
354
336
|
check(r3._replay()["t1"].status == "done", "fsync=False 正常读写(开关兼容)")
|
|
355
|
-
# 空中断 → 空计划
|
|
356
337
|
r4 = RestartRecovery(os.path.join(d3, "empty.jsonl"), boot_id="b")
|
|
357
338
|
p4 = r4.plan_recovery([])
|
|
358
339
|
check(p4.details == [] and p4.retryable == [], "无中断 → 空计划(不炸)")
|
|
359
340
|
|
|
360
341
|
|
|
361
|
-
# ═══════════════ [10] P3-2 /api/ahs/run ═══════════════
|
|
362
342
|
def test_api_ahs_run():
|
|
363
343
|
print("\n【10】P3-2:/api/ahs/run 走 run_async + 暴露 pending 字段")
|
|
364
344
|
os.environ["FOXCLAW_SHADOW_ROUNDS"] = "6"
|
|
@@ -377,7 +357,6 @@ def test_api_ahs_run():
|
|
|
377
357
|
check("is_pending" in body and "pending_confirmation" in body,
|
|
378
358
|
"响应暴露 is_pending/pending_confirmation 字段")
|
|
379
359
|
check(body.get("success") in (True, False), "success 字段正常返回")
|
|
380
|
-
# resume_state 参数被接受(闭环 P2-2 续跑;空断点现场 → 正常全跑)
|
|
381
360
|
r2 = c.post("/api/ahs/run", json={
|
|
382
361
|
"task": "计算 3*3", "confirmed": True,
|
|
383
362
|
"resume_state": {"completed_steps": [], "resume_at": None}})
|
|
@@ -64,17 +64,15 @@ def _plan_tool():
|
|
|
64
64
|
"required": ["flow"]}}}
|
|
65
65
|
|
|
66
66
|
|
|
67
|
-
# ── 1. 格式转换 ───────────────────────────────────────────────────────────────
|
|
68
67
|
def test_to_anthropic_tools():
|
|
69
68
|
out = ModelRouter._to_anthropic_tools([
|
|
70
69
|
_plan_tool(),
|
|
71
|
-
{"name": "direct", "description": "", "input_schema": {}},
|
|
70
|
+
{"name": "direct", "description": "", "input_schema": {}},
|
|
72
71
|
])
|
|
73
72
|
assert len(out) == 2
|
|
74
73
|
assert out[0]["name"] == "submit_plan"
|
|
75
74
|
assert out[0]["input_schema"]["required"] == ["flow"]
|
|
76
75
|
assert out[1]["name"] == "direct"
|
|
77
|
-
# 非法项(空名/非 dict)跳过
|
|
78
76
|
assert ModelRouter._to_anthropic_tools([{"type": "function", "function": {"name": ""}}]) == []
|
|
79
77
|
assert ModelRouter._to_anthropic_tools(None) == []
|
|
80
78
|
|
|
@@ -88,18 +86,15 @@ def test_to_anthropic_tool_choice():
|
|
|
88
86
|
{"type": "function", "function": {"name": "x"}}) == {"type": "tool", "name": "x"}
|
|
89
87
|
|
|
90
88
|
|
|
91
|
-
# ── 2. supports_tools ──────────────────────────────────────────────────────────
|
|
92
89
|
def test_supports_tools_anthropic():
|
|
93
90
|
r = _router(_fake_client([[]]))
|
|
94
91
|
assert r.is_stub is False
|
|
95
92
|
assert r.supports_tools() is True
|
|
96
|
-
# 无 client(stub)→ 不支持
|
|
97
93
|
r2 = ModelRouter.__new__(ModelRouter)
|
|
98
94
|
r2.provider = "anthropic"; r2._client = None
|
|
99
95
|
assert r2.supports_tools() is False
|
|
100
96
|
|
|
101
97
|
|
|
102
|
-
# ── 3. call_tools anthropic 分支 ──────────────────────────────────────────────
|
|
103
98
|
def test_call_tools_anthropic_tool_use():
|
|
104
99
|
client = _fake_client([[
|
|
105
100
|
_block(type="tool_use", id="toolu_1", name="submit_plan",
|
|
@@ -113,13 +108,11 @@ def test_call_tools_anthropic_tool_use():
|
|
|
113
108
|
"args": {"flow": "sequential",
|
|
114
109
|
"steps": [{"name": "查支出", "agent": "finance"}]},
|
|
115
110
|
"args_parse_failed": False}]
|
|
116
|
-
# 入参:tools 转 anthropic 格式 + tool_choice 转 auto + system 单独传 + 运行时注入
|
|
117
111
|
kw = client.calls[0]
|
|
118
112
|
assert kw["system"].startswith("【运行时信息") and "sys" in kw["system"]
|
|
119
113
|
assert kw["tools"][0]["name"] == "submit_plan"
|
|
120
114
|
assert kw["tool_choice"] == {"type": "auto"}
|
|
121
115
|
assert all(m["role"] != "system" for m in kw["messages"]), "anthropic 不允许 system 角色入 messages"
|
|
122
|
-
# assistant_msg 是 anthropic 原生 blocks
|
|
123
116
|
assert res["assistant_msg"]["role"] == "assistant"
|
|
124
117
|
assert res["assistant_msg"]["content"][0]["type"] == "tool_use"
|
|
125
118
|
|
|
@@ -144,7 +137,6 @@ def test_call_tools_anthropic_tool_choice_none():
|
|
|
144
137
|
assert kw["tools"][0]["name"] == "submit_plan", "历史含 tool_use 时必须仍传 tools"
|
|
145
138
|
|
|
146
139
|
|
|
147
|
-
# ── 4. format_tool_result ─────────────────────────────────────────────────────
|
|
148
140
|
def test_format_tool_result_anthropic():
|
|
149
141
|
r = _router(_fake_client([[]]))
|
|
150
142
|
out = r.format_tool_result({"id": "toolu_9", "name": "web_search"}, "结果")
|
|
@@ -152,7 +144,6 @@ def test_format_tool_result_anthropic():
|
|
|
152
144
|
{"type": "tool_result", "tool_use_id": "toolu_9", "content": "结果"}]}
|
|
153
145
|
|
|
154
146
|
|
|
155
|
-
# ── 5. fail-safe ──────────────────────────────────────────────────────────────
|
|
156
147
|
def test_call_tools_anthropic_failsafe():
|
|
157
148
|
def create(**kw):
|
|
158
149
|
raise RuntimeError("boom")
|
|
@@ -162,19 +153,15 @@ def test_call_tools_anthropic_failsafe():
|
|
|
162
153
|
res = r.call_tools("sys", [{"role": "user", "content": "t"}], [])
|
|
163
154
|
assert res["supported"] is False and res.get("error")
|
|
164
155
|
|
|
165
|
-
# stub(无 client)→ supported=False
|
|
166
156
|
r2 = _router(None)
|
|
167
157
|
res2 = r2.call_tools("sys", [{"role": "user", "content": "t"}], [])
|
|
168
158
|
assert res2["supported"] is False
|
|
169
159
|
|
|
170
160
|
|
|
171
|
-
# ── 6. 回灌闭环(_reply_with_tools 在 anthropic 下)────────────────────────────
|
|
172
161
|
def test_reply_with_tools_anthropic_loop():
|
|
173
162
|
from foxclaw.agents.base import CognitiveAgent
|
|
174
163
|
client = _fake_client([
|
|
175
|
-
# 第1轮:模型要求调 recall_memory
|
|
176
164
|
[_block(type="tool_use", id="c1", name="recall_memory", input={"query": "偏好"})],
|
|
177
|
-
# 第2轮:拿到结果后给最终答案
|
|
178
165
|
[_block(type="text", text="根据记忆,你喜欢灰色。")],
|
|
179
166
|
])
|
|
180
167
|
r = _router(client)
|
|
@@ -187,7 +174,6 @@ def test_reply_with_tools_anthropic_loop():
|
|
|
187
174
|
out = CognitiveAgent._reply_with_tools(
|
|
188
175
|
agent, "我喜欢什么颜色", [{"role": "user", "content": "我喜欢什么颜色"}])
|
|
189
176
|
assert out == "根据记忆,你喜欢灰色。"
|
|
190
|
-
# 第2轮的 messages 必须含 anthropic 格式 assistant(tool_use) + user(tool_result)
|
|
191
177
|
kw2 = client.calls[1]
|
|
192
178
|
has_tool_result = any(
|
|
193
179
|
isinstance(m.get("content"), list)
|
package/python/tests/test_api.py
CHANGED
|
@@ -21,9 +21,8 @@ def show(title, data, limit=None):
|
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
def test_api():
|
|
24
|
-
# 环境变量须在 import foxclaw.api 前设好(模块加载时按环境变量建网关/agent)
|
|
25
24
|
os.environ["FOXCLAW_SHADOW_ROUNDS"] = "6"
|
|
26
|
-
os.environ["FOXCLAW_DATA_DIR"] = tempfile.mkdtemp(prefix="foxclaw_test_")
|
|
25
|
+
os.environ["FOXCLAW_DATA_DIR"] = tempfile.mkdtemp(prefix="foxclaw_test_")
|
|
27
26
|
from foxclaw.api import app
|
|
28
27
|
|
|
29
28
|
with TestClient(app) as c:
|
|
@@ -31,7 +30,6 @@ def test_api():
|
|
|
31
30
|
print("FOXCLAW · API 端到端验证 (ASGI TestClient)")
|
|
32
31
|
print("="*64)
|
|
33
32
|
|
|
34
|
-
# 1. 系统状态
|
|
35
33
|
r = c.get("/api/status")
|
|
36
34
|
print(f"\n[GET /api/status] HTTP {r.status_code}")
|
|
37
35
|
data = r.json()
|
|
@@ -39,7 +37,6 @@ def test_api():
|
|
|
39
37
|
tc = s["thought_chain"]
|
|
40
38
|
print(f" {aid:<12} 节点={tc['total_nodes']:<3} shadow={s['harness']['shadow_mode']}")
|
|
41
39
|
|
|
42
|
-
# 2. 发任务(真实经过 ThoughtChain + Harness)
|
|
43
40
|
print("\n[POST /api/task] 连发 7 个财务任务触发校准")
|
|
44
41
|
for i, t in enumerate(["分析支出","制定预算","评估风险","季度报告","追踪消费","明年规划","同期对比"], 1):
|
|
45
42
|
r = c.post("/api/task", json={"task": t, "agent_id": "finance"})
|
|
@@ -47,11 +44,9 @@ def test_api():
|
|
|
47
44
|
err = d.get("error", "ok")
|
|
48
45
|
print(f" [{i}] {t} → HTTP {r.status_code} · {err}")
|
|
49
46
|
|
|
50
|
-
# 3. Harness 状态(校准后)
|
|
51
47
|
r = c.get("/api/harness/finance/status")
|
|
52
48
|
show("GET /api/harness/finance/status", r.json())
|
|
53
49
|
|
|
54
|
-
# 4. 推理链
|
|
55
50
|
r = c.get("/api/thoughtchain/finance/chain")
|
|
56
51
|
nodes = r.json()["nodes"]
|
|
57
52
|
print(f"\n[GET /api/thoughtchain/finance/chain] HTTP {r.status_code} · {len(nodes)} 节点")
|
|
@@ -59,25 +54,40 @@ def test_api():
|
|
|
59
54
|
print(f" {n['type']:<16} {n['harness_status']:<9} "
|
|
60
55
|
f"conf={n['conf']:.2f} Δ={n.get('harness_delta') or 0:.3f}")
|
|
61
56
|
|
|
62
|
-
# 5. 漂移趋势
|
|
63
57
|
r = c.get("/api/thoughtchain/finance/trend?limit=10")
|
|
64
58
|
trend = r.json()["trend"]
|
|
65
59
|
print(f"\n[GET .../trend] {len(trend)} 数据点 · Δ 序列:")
|
|
66
60
|
print(" " + " ".join(f"{t['delta']:.2f}" for t in trend[:10]))
|
|
67
61
|
|
|
68
|
-
# 6. Policy
|
|
69
62
|
r = c.get("/api/harness/finance/policy")
|
|
70
63
|
pol = r.json()
|
|
71
64
|
print(f"\n[GET /api/harness/finance/policy] v{pol['version']} · "
|
|
72
65
|
f"{len(pol.get('rules',[]))} 规则")
|
|
73
66
|
|
|
74
|
-
# 7. 偏好管理端点(F3):GET 列出结构;DELETE 不存在 id 优雅返回 ok=False(不崩)
|
|
75
67
|
r = c.get("/api/preferences")
|
|
76
68
|
assert r.status_code == 200 and "preferences" in r.json(), r.text
|
|
77
69
|
r = c.delete("/api/preferences/nonexistent_id")
|
|
78
70
|
assert r.status_code == 200 and r.json().get("ok") is False, r.text
|
|
79
71
|
print("\n[GET/DELETE /api/preferences] 列出+删除通路 ok")
|
|
80
72
|
|
|
73
|
+
r = c.put("/api/profile", json={"nickname": " snow "})
|
|
74
|
+
assert r.status_code == 200 and r.json().get("nickname") == "snow", r.text
|
|
75
|
+
r = c.get("/api/profile")
|
|
76
|
+
assert r.json().get("nickname") == "snow", r.text
|
|
77
|
+
c.put("/api/profile", json={"nickname": "snow2"})
|
|
78
|
+
r = c.get("/api/preferences")
|
|
79
|
+
nicks = [p["content"] for p in r.json()["preferences"] if "称呼我为" in p["content"]]
|
|
80
|
+
assert nicks == ["用户偏好:称呼我为 snow2"], nicks
|
|
81
|
+
print("\n[GET/PUT /api/profile] 称呼写入/读回/替换 ok")
|
|
82
|
+
|
|
83
|
+
r = c.get("/api/system/info")
|
|
84
|
+
info = r.json()
|
|
85
|
+
assert r.status_code == 200 and "update" in info, r.text
|
|
86
|
+
up = info["update"]
|
|
87
|
+
assert {"enabled", "current", "latest", "update_available"} <= set(up), up
|
|
88
|
+
assert up["enabled"] is False and up["update_available"] is False, up
|
|
89
|
+
print("\n[GET /api/system/info] update 字段存在,默认关闭")
|
|
90
|
+
|
|
81
91
|
print("\n" + "="*64)
|
|
82
92
|
print("✓ API 端到端验证完成 · 所有端点返回真实数据")
|
|
83
93
|
print("="*64)
|
|
@@ -10,12 +10,10 @@ import os
|
|
|
10
10
|
import sys
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
13
|
-
# 本测试要精确控制 FOXCLAW_API_KEY 的有/无;若 .env 带真实 key 会污染判定
|
|
14
|
-
# (见 foxclaw/api.py 中 FOXCLAW_SKIP_DOTENV 的说明),故关掉 .env。
|
|
15
13
|
os.environ["FOXCLAW_SKIP_DOTENV"] = "1"
|
|
16
14
|
|
|
17
15
|
import tempfile
|
|
18
|
-
os.environ["FOXCLAW_DATA_DIR"] = tempfile.mkdtemp(prefix="foxclaw_test_")
|
|
16
|
+
os.environ["FOXCLAW_DATA_DIR"] = tempfile.mkdtemp(prefix="foxclaw_test_")
|
|
19
17
|
from fastapi.testclient import TestClient
|
|
20
18
|
from foxclaw.api import app
|
|
21
19
|
|
|
@@ -44,7 +42,6 @@ def main():
|
|
|
44
42
|
"正确 key → 200")
|
|
45
43
|
|
|
46
44
|
print("\n【3】WebSocket 鉴权(端点内 ?key=,http 中间件管不到 ws)")
|
|
47
|
-
# 无 key → 握手被拒(端点 close 1008)
|
|
48
45
|
rejected = False
|
|
49
46
|
try:
|
|
50
47
|
with c.websocket_connect("/ws/harness"):
|
|
@@ -52,7 +49,6 @@ def main():
|
|
|
52
49
|
except Exception:
|
|
53
50
|
rejected = True
|
|
54
51
|
check(rejected, "WS 无 key → 握手被拒")
|
|
55
|
-
# 带 ?key= → 连上
|
|
56
52
|
connected = False
|
|
57
53
|
try:
|
|
58
54
|
with c.websocket_connect("/ws/harness?key=secret-123"):
|