foxclaw 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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/CHANGELOG.md +57 -0
- package/python/CHANGELOG_CN.md +57 -0
- package/python/README.md +11 -2
- package/python/README_CN.md +32 -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/RELEASE.md +4 -3
- 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 +34 -152
- package/python/foxclaw/agents/base.py +65 -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 +7 -1
- package/python/foxclaw/agents/mixins/preference.py +58 -8
- package/python/foxclaw/agents/mixins/prompt.py +0 -6
- package/python/foxclaw/agents/mixins/skillbox.py +6 -6
- package/python/foxclaw/agents/mixins/tool_exec.py +16 -42
- package/python/foxclaw/api/__init__.py +18 -77
- package/python/foxclaw/api/_state.py +43 -0
- package/python/foxclaw/api/{app.py → bootstrap.py} +5 -32
- 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 +28 -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/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/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 +30 -57
- package/python/foxclaw/core/metrics.py +4 -5
- package/python/foxclaw/core/model_router.py +36 -148
- 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/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 +0 -201
- package/python/frontend/v3/assets/index-CQSBdxk4.js +34 -0
- package/python/frontend/v3/assets/index-D5nIsjms.css +1 -0
- package/python/frontend/v3/index.html +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 +0 -1
- 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 +0 -41
- 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 +6 -3
- package/python/requirements.txt +311 -0
- package/python/tests/conftest.py +0 -22
- package/python/tests/run_all.py +3 -66
- package/python/tests/test_agent_lifecycle.py +4 -38
- 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_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_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_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/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/release_check.py +7 -7
- 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/frontend/v3/assets/index-BD7VPbMV.js +0 -33
- package/python/frontend/v3/assets/index-CAvnYWGH.css +0 -1
|
@@ -13,14 +13,14 @@ sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
|
13
13
|
)
|
|
14
14
|
def test_mcp_server():
|
|
15
15
|
os.environ["FOXCLAW_SHADOW_ROUNDS"] = "6"
|
|
16
|
-
os.environ["FOXCLAW_SKIP_DOTENV"] = "1"
|
|
16
|
+
os.environ["FOXCLAW_SKIP_DOTENV"] = "1"
|
|
17
17
|
os.environ["FOXCLAW_DATA_DIR"] = tempfile.mkdtemp(prefix="foxclaw_mcpsrv_")
|
|
18
|
-
os.environ["FOXCLAW_API_KEY"] = "mcp-test-key"
|
|
18
|
+
os.environ["FOXCLAW_API_KEY"] = "mcp-test-key"
|
|
19
19
|
os.environ["FOXCLAW_MCP_SERVER"] = "1"
|
|
20
20
|
os.environ["FOXCLAW_MCP_AGENTS"] = "1"
|
|
21
|
-
os.environ["FOXCLAW_MCP_RATE_AGENT"] = "1"
|
|
21
|
+
os.environ["FOXCLAW_MCP_RATE_AGENT"] = "1"
|
|
22
22
|
os.environ.setdefault("FOXCLAW_EMBEDDING_MODEL", "foxclaw-nonexistent-fast")
|
|
23
|
-
os.environ["HF_HUB_OFFLINE"] = "1"
|
|
23
|
+
os.environ["HF_HUB_OFFLINE"] = "1"
|
|
24
24
|
os.environ["TRANSFORMERS_OFFLINE"] = "1"
|
|
25
25
|
import uvicorn
|
|
26
26
|
import httpx
|
|
@@ -35,13 +35,12 @@ def test_mcp_server():
|
|
|
35
35
|
daemon=True).start()
|
|
36
36
|
for _ in range(60):
|
|
37
37
|
try:
|
|
38
|
-
urllib.request.urlopen(f"http://127.0.0.1:{PORT}/health", timeout=2); break
|
|
38
|
+
urllib.request.urlopen(f"http://127.0.0.1:{PORT}/health", timeout=2); break
|
|
39
39
|
except urllib.error.HTTPError:
|
|
40
40
|
break
|
|
41
41
|
except Exception:
|
|
42
42
|
time.sleep(1)
|
|
43
43
|
|
|
44
|
-
# [0] 防后门:设了 key,不带/错 key 访问 /api/mcp/ 必须 401(未被鉴权绕过)
|
|
45
44
|
with httpx.Client(timeout=8) as c:
|
|
46
45
|
body = {"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {}}
|
|
47
46
|
hdr = {"Accept": "application/json, text/event-stream"}
|
|
@@ -55,7 +54,6 @@ def test_mcp_server():
|
|
|
55
54
|
async def probe():
|
|
56
55
|
from mcp.client.streamable_http import streamablehttp_client
|
|
57
56
|
from mcp import ClientSession
|
|
58
|
-
# 带正确 key 连接
|
|
59
57
|
async with streamablehttp_client(f"http://127.0.0.1:{PORT}/api/mcp/",
|
|
60
58
|
headers={"X-API-Key": KEY}) as (r, w, _):
|
|
61
59
|
async with ClientSession(r, w) as s:
|
|
@@ -64,7 +62,7 @@ def test_mcp_server():
|
|
|
64
62
|
assert tools == {"knowledge_search", "knowledge_get", "memory_recall", "ask_foxclaw"}, tools
|
|
65
63
|
print("[1] 带key initialize + list_tools(3 只读 + ask_foxclaw) OK")
|
|
66
64
|
|
|
67
|
-
res = await s.call_tool("knowledge_search", {"query": "anything", "top_k": 999999})
|
|
65
|
+
res = await s.call_tool("knowledge_search", {"query": "anything", "top_k": 999999})
|
|
68
66
|
assert res.content is not None
|
|
69
67
|
print("[2] knowledge_search 可调 + top_k 钳制不崩 OK")
|
|
70
68
|
|
|
@@ -82,7 +80,6 @@ def test_mcp_server():
|
|
|
82
80
|
assert "dispatched_to" in txt or "reply" in txt or "error" in txt, txt
|
|
83
81
|
print("[5] ask_foxclaw Agent 调度可调(返回 reply/dispatched_to) OK")
|
|
84
82
|
|
|
85
|
-
# [6] 限流:agent 桶设 1,上面已消耗 → 再调应被限流
|
|
86
83
|
res = await s.call_tool("ask_foxclaw", {"task": "再来一次"})
|
|
87
84
|
txt = res.content[0].text if res.content else ""
|
|
88
85
|
assert "rate limited" in txt, f"限流未生效: {txt}"
|
|
@@ -18,7 +18,7 @@ sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
|
18
18
|
from foxclaw.core.memory_adapter import create_memory_backend
|
|
19
19
|
|
|
20
20
|
QUERY = "quarterly revenue growth and budget forecast analysis"
|
|
21
|
-
N_FINANCE = 55
|
|
21
|
+
N_FINANCE = 55
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
def check(cond, msg):
|
|
@@ -36,16 +36,13 @@ def main():
|
|
|
36
36
|
print(" · HMR 不可用(降级 Mock),跳过 M4 测试")
|
|
37
37
|
print("结果:跳过"); return
|
|
38
38
|
|
|
39
|
-
# office:一条与 query 相关的关键记忆 + 一条无关
|
|
40
39
|
hmr.ingest("office quarterly revenue growth and budget forecast for planning",
|
|
41
40
|
type="concept", agent_id="office", title="office-key")
|
|
42
41
|
hmr.ingest("office meeting room booking schedule for next week",
|
|
43
42
|
type="concept", agent_id="office", title="office-other")
|
|
44
|
-
# finance:大量与 query 强相关记忆(会在全局排序里淹没 office 的记忆)
|
|
45
43
|
for i in range(N_FINANCE):
|
|
46
44
|
hmr.ingest(f"finance quarterly revenue growth and budget forecast record {i}",
|
|
47
45
|
type="concept", agent_id="finance", title=f"fin-{i}")
|
|
48
|
-
# 一条共享记忆
|
|
49
46
|
hmr.ingest("shared team quarterly planning memo", type="concept",
|
|
50
47
|
agent_id="finance", shared=True, title="shared-memo")
|
|
51
48
|
|
|
@@ -6,8 +6,6 @@ import pytest
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
def test_metrics():
|
|
9
|
-
# ISOLATION_GUARD:本用例依赖 import 时固化的模块级单例(网关/限流器/路由配置按 env 在 import 时定型),单进程全量跑会被前序用例污染 →
|
|
10
|
-
# 假红。全量请用 tests/run_all.py(每文件独立子进程)。
|
|
11
9
|
if "foxclaw.api" in sys.modules:
|
|
12
10
|
pytest.skip("需独立进程运行(模块级单例已固化),请用 tests/run_all.py")
|
|
13
11
|
os.environ["FOXCLAW_SHADOW_ROUNDS"] = "6"
|
|
@@ -21,11 +19,9 @@ def test_metrics():
|
|
|
21
19
|
print("=" * 60); print("ops 监控验证"); print("=" * 60)
|
|
22
20
|
|
|
23
21
|
with TestClient(app) as c:
|
|
24
|
-
# 产生一些请求 + 一次任务(触发 LLM 埋点,stub)
|
|
25
22
|
c.get("/api/status"); c.get("/api/ahs/skills"); c.get("/health")
|
|
26
23
|
c.post("/api/task", json={"task": "测试", "agent_id": "supervisor"})
|
|
27
24
|
|
|
28
|
-
# [1] 深度健康:逐子系统(开放路径,探针无需鉴权;不泄露 provider 名)
|
|
29
25
|
hd = c.get("/health/ready")
|
|
30
26
|
assert hd.status_code == 200, hd.status_code
|
|
31
27
|
j = hd.json()
|
|
@@ -33,7 +29,6 @@ def test_metrics():
|
|
|
33
29
|
assert j["components"]["router"] in ("ok", "stub", "down"), "router 不应泄露 provider 名"
|
|
34
30
|
print(f"[1] /health/ready 深度健康({j['status']},{len(j['components'])} 组件,router 不泄露) OK")
|
|
35
31
|
|
|
36
|
-
# [2] JSON metrics:端点延迟分位 + 错误率
|
|
37
32
|
m = c.get("/api/metrics").json()
|
|
38
33
|
assert "uptime_s" in m and m["endpoints"], m
|
|
39
34
|
ep = next(k for k in m["endpoints"] if "status" in k)
|
|
@@ -41,16 +36,13 @@ def test_metrics():
|
|
|
41
36
|
assert st["count"] >= 1 and "p95_ms" in st and "error_rate" in st and "avg_ms" in st, st
|
|
42
37
|
print(f"[2] /api/metrics 端点(p50={st['p50_ms']} p95={st['p95_ms']} err={st['error_rate']}) OK")
|
|
43
38
|
|
|
44
|
-
# [3] LLM 埋点(stub 也计入调用量)
|
|
45
39
|
assert m["llm"], f"LLM 指标应非空: {m['llm']}"
|
|
46
40
|
print(f"[3] LLM 埋点({list(m['llm'])}) OK")
|
|
47
41
|
|
|
48
|
-
# [4] Prometheus 文本格式
|
|
49
42
|
p = c.get("/metrics")
|
|
50
43
|
assert p.status_code == 200 and "foxclaw_requests_total" in p.text and "foxclaw_llm_calls_total" in p.text
|
|
51
44
|
print("[4] /metrics Prometheus 文本 OK")
|
|
52
45
|
|
|
53
|
-
# [5] 未匹配路由(404)不撑爆 key:20 个不同 404 路径 → 只 1 个 "unmatched" key
|
|
54
46
|
for i in range(20):
|
|
55
47
|
c.get(f"/api/does-not-exist/random-{i}")
|
|
56
48
|
m2 = c.get("/api/metrics").json()
|
|
@@ -16,32 +16,25 @@ def test_multi_chain():
|
|
|
16
16
|
|
|
17
17
|
print("=" * 60); print("多步跨 Agent 链式协作验证"); print("=" * 60)
|
|
18
18
|
|
|
19
|
-
# [1] 无前序 → 原样返回(第 1 步不该有前言)
|
|
20
19
|
assert _with_prior("查餐饮支出", []) == "查餐饮支出"
|
|
21
20
|
print("[1] 首步无前序上下文(原样)OK")
|
|
22
21
|
|
|
23
|
-
# [2] 有前序 → 并进任务文本
|
|
24
22
|
t = _with_prior("据此定预算", [{"step": 1, "agent": "finance", "output": "餐饮 1200 元"}])
|
|
25
23
|
assert "据此定预算" in t and "餐饮 1200 元" in t and "第1步(finance)" in t, t
|
|
26
24
|
print("[2] 前序结果并进下一步任务文本 OK")
|
|
27
25
|
|
|
28
|
-
# [3] 步数封顶:只带最近 N 步(防长链把提示词滚爆)
|
|
29
26
|
many = [{"step": i, "agent": "a", "output": f"out{i}"} for i in range(1, 9)]
|
|
30
27
|
t3 = _with_prior("x", many)
|
|
31
28
|
assert "out8" in t3 and "out6" in t3 and "out5" not in t3, t3
|
|
32
29
|
assert t3.count("第") - 1 == PRIOR_MAX_STEPS or t3.count("步(") == PRIOR_MAX_STEPS, t3
|
|
33
30
|
print(f"[3] 只带最近 {PRIOR_MAX_STEPS} 步(更早的不注入)OK")
|
|
34
31
|
|
|
35
|
-
# [4] 单步字符封顶
|
|
36
32
|
t4 = _with_prior("x", [{"step": 1, "agent": "a", "output": "y" * 900}])
|
|
37
33
|
assert "y" * PRIOR_MAX_CHARS in t4 and "y" * (PRIOR_MAX_CHARS + 1) not in t4
|
|
38
34
|
print(f"[4] 单步输出截断到 {PRIOR_MAX_CHARS} 字符 OK")
|
|
39
35
|
|
|
40
|
-
# ── 集成:直接驱动 _dispatch_multi,看子 Agent 真实收到了什么 ──
|
|
41
36
|
from foxclaw import Foxclaw
|
|
42
37
|
|
|
43
|
-
# 注意:Foxclaw 的 data_dir 是**构造参数**(默认 ./foxclaw_data),不读 FOXCLAW_DATA_DIR
|
|
44
|
-
# ——env 是 api.py 读了再传进来的。测试必须显式传临时目录,否则会写进真实用户记忆库。
|
|
45
38
|
os_ = Foxclaw(data_dir=os.environ["FOXCLAW_DATA_DIR"])
|
|
46
39
|
sup = os_.supervisor
|
|
47
40
|
|
|
@@ -58,35 +51,30 @@ def test_multi_chain():
|
|
|
58
51
|
adm = StubAgent("admin", "已按 1200 元安排下周预算")
|
|
59
52
|
subs = {"finance": fin, "admin": adm}
|
|
60
53
|
sched = sup._schedule_for("t", subs) if hasattr(sup, "_schedule_for") else None
|
|
61
|
-
if sched is None:
|
|
54
|
+
if sched is None:
|
|
62
55
|
from foxclaw.core.scheduler import SystemContext
|
|
63
56
|
sched = sup._scheduler.schedule("t", SystemContext(active_agents=0, queue_depth=0, system_load=0.1))
|
|
64
57
|
|
|
65
58
|
routes = [("查本月餐饮支出", "finance"), ("据此安排下周预算", "admin")]
|
|
66
59
|
out = asyncio.run(sup._dispatch_multi("查餐饮并定预算", routes, sched, "sess1", subs, {}, []))
|
|
67
60
|
|
|
68
|
-
# [5] 第 1 步不带前序;第 2 步**收到了**第 1 步的输出 → 链真的接上了
|
|
69
61
|
s1, s2 = fin.seen[0]["task"], adm.seen[0]["task"]
|
|
70
62
|
assert "前序步骤结果" not in s1, f"首步不该有前序前言: {s1}"
|
|
71
63
|
assert "本月餐饮 1200 元" in s2, f"第2步应看到第1步结果,实际: {s2}"
|
|
72
64
|
assert "据此安排下周预算" in s2
|
|
73
65
|
print("[5] 第2步真实收到第1步输出(链式成立)OK")
|
|
74
66
|
|
|
75
|
-
# [6] context 里也有结构化 prior_steps(两手准备)
|
|
76
67
|
ps = adm.seen[0]["context"].get("prior_steps")
|
|
77
68
|
assert ps and ps[0]["agent"] == "finance" and "1200" in ps[0]["output"], ps
|
|
78
69
|
assert fin.seen[0]["context"].get("prior_steps") == [], "首步 prior 应为空"
|
|
79
70
|
print("[6] context.prior_steps 结构化传递 OK")
|
|
80
71
|
|
|
81
|
-
# [7] 展示用 subtask 保留原始文本(不被前序前言污染 UI)
|
|
82
72
|
steps = out["result"]["multi_steps"]
|
|
83
73
|
assert steps[1]["subtask"] == "据此安排下周预算", steps[1]["subtask"]
|
|
84
74
|
assert "前序步骤结果" not in steps[1]["subtask"]
|
|
85
75
|
assert out["result"]["dispatched_to"] == "multi" and len(steps) == 2
|
|
86
76
|
print("[7] UI 展示的 subtask 仍是原始文本(未被污染)OK")
|
|
87
77
|
|
|
88
|
-
# [8] 【长期运行】入链的输出必须截断:prior 会被复制进**每个**后续步骤的 context,
|
|
89
|
-
# 不截断则长链 + 大输出会滚成可观的内存/拷贝开销(渲染本就只用 300 字符)。
|
|
90
78
|
big = StubAgent("finance", "X" * 5000)
|
|
91
79
|
subs2 = {"finance": big, "admin": StubAgent("admin", "ok")}
|
|
92
80
|
asyncio.run(sup._dispatch_multi("t", [("查", "finance"), ("用", "admin")],
|
|
@@ -7,8 +7,6 @@ sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
|
7
7
|
import pytest
|
|
8
8
|
|
|
9
9
|
def test_multimodal():
|
|
10
|
-
# ISOLATION_GUARD:本用例依赖 import 时固化的模块级单例(网关/限流器/路由配置按 env 在 import 时定型),单进程全量跑会被前序用例污染 →
|
|
11
|
-
# 假红。全量请用 tests/run_all.py(每文件独立子进程)。
|
|
12
10
|
if "foxclaw.core.model_router" in sys.modules:
|
|
13
11
|
pytest.skip("需独立进程运行(模块级单例已固化),请用 tests/run_all.py")
|
|
14
12
|
for k in ("ANTHROPIC_API_KEY", "OPENAI_API_KEY", "DEEPSEEK_API_KEY"):
|
|
@@ -24,7 +22,6 @@ def test_multimodal():
|
|
|
24
22
|
IMG = [{"media_type": "image/png", "data": "AAAABBBB"}]
|
|
25
23
|
BASE = [{"role": "user", "content": "这是什么?"}]
|
|
26
24
|
|
|
27
|
-
# [1] anthropic 组装:image/base64 block
|
|
28
25
|
a = ModelRouter._attach_images("anthropic", BASE, IMG)
|
|
29
26
|
blocks = a[-1]["content"]
|
|
30
27
|
assert isinstance(blocks, list) and blocks[0] == {"type": "text", "text": "这是什么?"}, blocks
|
|
@@ -33,26 +30,22 @@ def test_multimodal():
|
|
|
33
30
|
assert BASE[-1]["content"] == "这是什么?", "不得改动原 messages(tier/stub/meter 仍需文本)"
|
|
34
31
|
print("[1] anthropic 多模态 block 组装 OK")
|
|
35
32
|
|
|
36
|
-
# [2] openai 系组装:image_url data URI
|
|
37
33
|
for prov in ("openai", "deepseek", "qwen"):
|
|
38
34
|
o = ModelRouter._attach_images(prov, BASE, IMG)
|
|
39
35
|
blk = o[-1]["content"]
|
|
40
36
|
assert blk[1]["type"] == "image_url" and blk[1]["image_url"]["url"] == "data:image/png;base64,AAAABBBB", (prov, blk)
|
|
41
37
|
print("[2] openai/deepseek/qwen 多模态 image_url 组装 OK")
|
|
42
38
|
|
|
43
|
-
# [3] google/未知 provider:无视觉通道 → 原样(纯文本降级)
|
|
44
39
|
assert ModelRouter._attach_images("google", BASE, IMG) == BASE
|
|
45
40
|
assert ModelRouter._attach_images("whatever", BASE, IMG) == BASE
|
|
46
41
|
print("[3] google/未知 provider 纯文本降级 OK")
|
|
47
42
|
|
|
48
|
-
# [4] 无 user 消息 / 已是多模态 → 不重复组装
|
|
49
43
|
assert ModelRouter._attach_images("anthropic", [{"role": "assistant", "content": "x"}], IMG)[0]["content"] == "x"
|
|
50
44
|
already = [{"role": "user", "content": [{"type": "text", "text": "hi"}]}]
|
|
51
45
|
assert ModelRouter._attach_images("anthropic", already, IMG) == already
|
|
52
46
|
assert ModelRouter._attach_images("anthropic", BASE, None) == BASE
|
|
53
47
|
print("[4] 无 user / 已多模态 / 无图 边界 OK")
|
|
54
48
|
|
|
55
|
-
# [5] _sanitize_images 护栏(数量/大小/类型/非列表)
|
|
56
49
|
from foxclaw.api import _sanitize_images, _MAX_IMAGES, _MAX_IMG_B64
|
|
57
50
|
big = "x" * (_MAX_IMG_B64 + 1)
|
|
58
51
|
many = [{"media_type": "image/png", "data": "a"}] * (_MAX_IMAGES + 3)
|
|
@@ -62,14 +55,12 @@ def test_multimodal():
|
|
|
62
55
|
assert _sanitize_images([{"media_type": "image/jpeg", "data": ""}]) == [], "空数据未拒"
|
|
63
56
|
assert _sanitize_images("notalist") == [] and _sanitize_images([1, "x"]) == []
|
|
64
57
|
ok = _sanitize_images([{"media_type": "image/JPEG", "data": "abc"}])
|
|
65
|
-
assert ok == [{"media_type": "image/jpeg", "data": "abc"}], ok
|
|
66
|
-
# 累计总量上限:每张 8MB,第 3 张会使总量 24MB>20MB → 只留前 2 张
|
|
58
|
+
assert ok == [{"media_type": "image/jpeg", "data": "abc"}], ok
|
|
67
59
|
half = "x" * (8 * 1024 * 1024)
|
|
68
60
|
three = [{"media_type": "image/png", "data": half}] * 3
|
|
69
61
|
assert len(_sanitize_images(three)) == 2, "未按累计总量封顶"
|
|
70
62
|
print("[5] _sanitize_images 护栏(数量/单图/累计总量/类型/归一)OK")
|
|
71
63
|
|
|
72
|
-
# [6] _call_provider 把图片真正带进发给 provider 的 messages(openai 假 client 捕获)
|
|
73
64
|
cap = {}
|
|
74
65
|
def _make_openai():
|
|
75
66
|
def create(model, max_tokens, messages, **kw):
|
|
@@ -82,11 +73,10 @@ def test_multimodal():
|
|
|
82
73
|
r = ModelRouter()
|
|
83
74
|
out, _ = r._call_provider("openai", _make_openai(), "gpt-4o", "sys", BASE, 100, images=IMG)
|
|
84
75
|
assert out == "ok"
|
|
85
|
-
sent = cap["messages"][-1]["content"]
|
|
76
|
+
sent = cap["messages"][-1]["content"]
|
|
86
77
|
assert isinstance(sent, list) and any(b.get("type") == "image_url" for b in sent), sent
|
|
87
78
|
print("[6] _call_provider 把图片带进 provider 请求 OK")
|
|
88
79
|
|
|
89
|
-
# [7] router.stream(openai) 流式路径也带图(假流式 client 捕获)
|
|
90
80
|
capS = {}
|
|
91
81
|
def _make_openai_stream():
|
|
92
82
|
def create(model, max_tokens, messages, stream=False, **kw):
|
|
@@ -97,7 +87,7 @@ def test_multimodal():
|
|
|
97
87
|
return types.SimpleNamespace(chat=types.SimpleNamespace(
|
|
98
88
|
completions=types.SimpleNamespace(create=create)))
|
|
99
89
|
r2 = ModelRouter()
|
|
100
|
-
r2.provider = "openai"; r2._client = _make_openai_stream()
|
|
90
|
+
r2.provider = "openai"; r2._client = _make_openai_stream()
|
|
101
91
|
r2._locked_model = "gpt-4o"
|
|
102
92
|
got = "".join(r2.stream("sys", BASE, images=IMG))
|
|
103
93
|
assert got == "hi", got
|
|
@@ -105,7 +95,6 @@ def test_multimodal():
|
|
|
105
95
|
assert isinstance(sSent, list) and any(b.get("type") == "image_url" for b in sSent), sSent
|
|
106
96
|
print("[7] router.stream 流式多模态带图 OK")
|
|
107
97
|
|
|
108
|
-
# [8] stub 模式:带图不崩,忽略图片(无视觉时优雅降级)
|
|
109
98
|
r3 = ModelRouter(provider="stub")
|
|
110
99
|
assert r3.is_stub
|
|
111
100
|
sc = r3.complete("sys", BASE, images=IMG)
|
|
@@ -114,18 +103,16 @@ def test_multimodal():
|
|
|
114
103
|
assert isinstance(ss, str) and ss, "stub stream 带图应正常返回文本"
|
|
115
104
|
print("[8] stub 带图优雅降级(忽略图片,不崩)OK")
|
|
116
105
|
|
|
117
|
-
# [9] /api/chat/stream 端到端:带图 SSE 正常产出(stub),超大图被 sanitize 掉不崩
|
|
118
106
|
from fastapi.testclient import TestClient
|
|
119
107
|
from foxclaw.api import app
|
|
120
108
|
with TestClient(app) as c:
|
|
121
109
|
body = {"task": "看图说话", "agent_id": "supervisor",
|
|
122
110
|
"images": [{"media_type": "image/png", "data": "iVBORw0KGgo="},
|
|
123
|
-
{"media_type": "image/tiff", "data": "bad"},
|
|
124
|
-
{"media_type": "image/png", "data": "y" * (_MAX_IMG_B64 + 1)}]}
|
|
111
|
+
{"media_type": "image/tiff", "data": "bad"},
|
|
112
|
+
{"media_type": "image/png", "data": "y" * (_MAX_IMG_B64 + 1)}]}
|
|
125
113
|
r = c.post("/api/chat/stream", json=body)
|
|
126
114
|
assert r.status_code == 200, r.status_code
|
|
127
115
|
assert "data:" in r.text, r.text[:200]
|
|
128
|
-
# 无 error 事件(sanitize 把坏图剔掉,剩 1 张合法图走 stub)
|
|
129
116
|
assert '"error"' not in r.text, r.text[:300]
|
|
130
117
|
print("[9] /api/chat/stream 带图端到端(含超大/非法图被剔)OK")
|
|
131
118
|
|
|
@@ -54,7 +54,7 @@ def test_negotiation():
|
|
|
54
54
|
|
|
55
55
|
|
|
56
56
|
def _orch(agents):
|
|
57
|
-
return Orchestrator(_Caps(agents))
|
|
57
|
+
return Orchestrator(_Caps(agents))
|
|
58
58
|
|
|
59
59
|
|
|
60
60
|
def _req(amount=8000):
|
|
@@ -62,7 +62,6 @@ def test_negotiation():
|
|
|
62
62
|
request_type="budget_request", payload={"amount": amount})
|
|
63
63
|
|
|
64
64
|
|
|
65
|
-
# [1] 核心场景:被拒 → 让步 → 通过(原先到 rejected 就结束了)
|
|
66
65
|
gate = _Gate(limit=5000); r = _orch({"finance": gate, "admin": _Requester()})
|
|
67
66
|
req = r.negotiate(_req(8000))
|
|
68
67
|
assert req.status == "approved", (req.status, req.verdict)
|
|
@@ -71,34 +70,29 @@ def test_negotiation():
|
|
|
71
70
|
assert gate.seen[0] == 8000 and gate.seen[-1] <= 5000, gate.seen
|
|
72
71
|
print(f"[1] 被拒→让步→通过({' → '.join(str(int(a)) for a in gate.seen)})OK")
|
|
73
72
|
|
|
74
|
-
# [2] 让步记录可追溯(每轮裁决 + 让到多少)
|
|
75
73
|
n = req.negotiation
|
|
76
74
|
assert n[0]["status"] == "rejected" and "conceded_to" in n[0], n[0]
|
|
77
75
|
assert n[-1]["status"] == "approved", n[-1]
|
|
78
76
|
print(f"[2] 协商日志可追溯({len(n)} 轮,含每轮裁决与让步)OK")
|
|
79
77
|
|
|
80
|
-
# [3] 安全:让无可让 → **维持 rejected**,绝不降级成 needs_confirmation
|
|
81
78
|
gate2 = _Gate(limit=100); r2 = _orch({"finance": gate2, "admin": _Requester()})
|
|
82
79
|
req2 = r2.negotiate(_req(8000))
|
|
83
80
|
assert req2.status == "rejected", f"协商失败必须维持 rejected,不得降级: {req2.status}"
|
|
84
81
|
print("[3] 让无可让 → 维持 rejected(不给点穿守门人的口子)OK")
|
|
85
82
|
|
|
86
|
-
# [4] 有界:轮数不超过 max_rounds(防死循环)
|
|
87
83
|
gate3 = _Gate(limit=1); r3 = _orch({"finance": gate3, "admin": _Requester(floor=0.0)})
|
|
88
84
|
req3 = r3.negotiate(_req(8000), max_rounds=3)
|
|
89
85
|
assert len(gate3.seen) <= 3, f"轮数应有界: {gate3.seen}"
|
|
90
86
|
assert req3.status == "rejected"
|
|
91
87
|
print(f"[4] 有界协商(裁决 {len(gate3.seen)} 次 ≤ max_rounds=3)OK")
|
|
92
88
|
|
|
93
|
-
# [5] 必须真让步:concede 返回原样 payload → 立即停(否则无限重复同一请求)
|
|
94
89
|
class _NoOp:
|
|
95
|
-
def concede(self, req, verdict): return dict(req.payload)
|
|
90
|
+
def concede(self, req, verdict): return dict(req.payload)
|
|
96
91
|
gate4 = _Gate(limit=1); r4 = _orch({"finance": gate4, "admin": _NoOp()})
|
|
97
92
|
r4.negotiate(_req(8000), max_rounds=5)
|
|
98
93
|
assert len(gate4.seen) == 1, f"无实质让步应只裁决一次: {gate4.seen}"
|
|
99
94
|
print("[5] 假让步(payload 无变化)→ 立即停,不重复轰炸守门人 OK")
|
|
100
95
|
|
|
101
|
-
# [6] 不让步的 Agent(默认 concede→None)→ 与协商前逐字等价:只跑一轮
|
|
102
96
|
class _Silent:
|
|
103
97
|
def concede(self, req, verdict): return None
|
|
104
98
|
gate5 = _Gate(limit=1); r5 = _orch({"finance": gate5, "admin": _Silent()})
|
|
@@ -106,7 +100,6 @@ def test_negotiation():
|
|
|
106
100
|
assert len(gate5.seen) == 1 and req5.status == "rejected"
|
|
107
101
|
print("[6] 未实现让步的 Agent → 只跑一轮(零行为变更)OK")
|
|
108
102
|
|
|
109
|
-
# [7] 让步逻辑抛异常 → 停 + 维持原裁决(不因发起方 bug 放行)
|
|
110
103
|
class _Boom:
|
|
111
104
|
def concede(self, req, verdict): raise RuntimeError("concede 崩了")
|
|
112
105
|
gate6 = _Gate(limit=1); r6 = _orch({"finance": gate6, "admin": _Boom()})
|
|
@@ -115,15 +108,13 @@ def test_negotiation():
|
|
|
115
108
|
assert any("concede_error" in x for x in req6.negotiation)
|
|
116
109
|
print("[7] 让步逻辑崩溃 → 维持 rejected + 记录错误 OK")
|
|
117
110
|
|
|
118
|
-
# ── 真 OfficeAgent 的让步策略(行政并入办公,领域逻辑仍归领域 Agent)──
|
|
119
111
|
from foxclaw.agents.agents import OfficeAgent
|
|
120
112
|
adm = OfficeAgent.__new__(OfficeAgent)
|
|
121
113
|
|
|
122
|
-
# [8] OfficeAgent(原 AdminAgent):逐轮降档 → 夹到下限(600)→ 到底认输。全程单调下降、必然终止
|
|
123
114
|
p = adm.concede(_req(1000), "预算不足")
|
|
124
115
|
assert p and p["amount"] == 750 and "降标准" in p["_concession"], p
|
|
125
116
|
seq, cur = [1000], _req(1000).payload
|
|
126
|
-
while True:
|
|
117
|
+
while True:
|
|
127
118
|
nxt = adm.concede(_req(0).__class__(from_agent="admin", to_agent="finance",
|
|
128
119
|
request_type="budget_request", payload=dict(cur)), "拒")
|
|
129
120
|
if nxt is None:
|
|
@@ -134,7 +125,6 @@ def test_negotiation():
|
|
|
134
125
|
assert seq[-1] == 600, f"应走到下限 600(原始 1000 的 60%),实际 {seq}"
|
|
135
126
|
print(f"[8] OfficeAgent 让步链 {seq} → 到下限认输(单调下降·必然终止)OK")
|
|
136
127
|
|
|
137
|
-
# [9] OfficeAgent:非预算请求不插手(回退默认不让步)
|
|
138
128
|
other = CrossAgentRequest(from_agent="admin", to_agent="office",
|
|
139
129
|
request_type="knowledge_request", payload={"q": "x"})
|
|
140
130
|
assert adm.concede(other, "拒") is None
|
|
@@ -23,10 +23,9 @@ async def main():
|
|
|
23
23
|
print("=" * 60)
|
|
24
24
|
print("主管编排 · 全局态势 + 跨 Agent 协调")
|
|
25
25
|
print("=" * 60)
|
|
26
|
-
_data = tempfile.mkdtemp(prefix="foxclaw_orch_")
|
|
26
|
+
_data = tempfile.mkdtemp(prefix="foxclaw_orch_")
|
|
27
27
|
os_ = Foxclaw(data_dir=_data, shadow_rounds=50)
|
|
28
28
|
|
|
29
|
-
# 准备各内核的数据
|
|
30
29
|
fin = os_.finance_runtime
|
|
31
30
|
fin.set_budget("travel", 3000)
|
|
32
31
|
fin.set_balance("default", 5000)
|
|
@@ -38,7 +37,6 @@ async def main():
|
|
|
38
37
|
itin = os_.itinerary_runtime
|
|
39
38
|
itin.import_csv(str(Path(__file__).parent.parent / "examples" / "sample_itinerary.csv"))
|
|
40
39
|
|
|
41
|
-
# 1. 全局态势(驾驶舱视图)
|
|
42
40
|
print("\n【1】主管全局态势(驾驶舱视图)")
|
|
43
41
|
sit = os_.supervisor.global_situation()
|
|
44
42
|
print(f" 财务: {sit['finance']}")
|
|
@@ -47,7 +45,6 @@ async def main():
|
|
|
47
45
|
print(f" 跨域告警 {len(sit['alerts'])} 条:")
|
|
48
46
|
for a in sit["alerts"][:5]:
|
|
49
47
|
print(f" [{a['severity']}] ({a['domain']}) {a['msg']}")
|
|
50
|
-
# "驾驶舱"的意义在于态势**来自各内核的真实数据**,不是空壳。故断言三域计数与导入一致。
|
|
51
48
|
check(sit["finance"]["transactions"] == 13, f"财务态势反映 13 笔导入交易(实为 {sit['finance']['transactions']})")
|
|
52
49
|
check(sit["learning"]["total_items"] == 8, f"学习态势反映 8 个知识点(实为 {sit['learning']['total_items']})")
|
|
53
50
|
check(sit["itinerary"]["waypoints"] == 5, f"行程态势反映 5 个行程点(实为 {sit['itinerary']['waypoints']})")
|
|
@@ -55,7 +52,6 @@ async def main():
|
|
|
55
52
|
check(len(sit["alerts"]) >= 2 and "critical" in sev,
|
|
56
53
|
f"跨域告警被汇总且含 critical(实为 {sorted(sev)})")
|
|
57
54
|
|
|
58
|
-
# 2. 跨 Agent 编排:差旅预算申请(小额,应批准)
|
|
59
55
|
print("\n【2】跨 Agent 编排:行程→财务 差旅预算申请(小额)")
|
|
60
56
|
r1 = os_.supervisor.cross_request(
|
|
61
57
|
from_agent="admin", to_agent="finance", request_type="budget_request",
|
|
@@ -66,7 +62,6 @@ async def main():
|
|
|
66
62
|
check(r1["status"] == "approved",
|
|
67
63
|
f"小额(150)且在预算内 → 直接批准(实为 {r1['status']})")
|
|
68
64
|
|
|
69
|
-
# 3. 跨 Agent 编排:大额差旅(触发守门人确认)
|
|
70
65
|
print("\n【3】跨 Agent 编排:大额差旅(触发守门人)")
|
|
71
66
|
r2 = os_.supervisor.cross_request(
|
|
72
67
|
from_agent="admin", to_agent="finance", request_type="budget_request",
|
|
@@ -76,10 +71,8 @@ async def main():
|
|
|
76
71
|
print(f" 财务裁决: {r2['verdict']}")
|
|
77
72
|
check(r2["status"] == "needs_confirmation",
|
|
78
73
|
f"大额(680>500) → 转人工确认(实为 {r2['status']})")
|
|
79
|
-
# 金额确实改变结论(不是两条路都走同一个分支)
|
|
80
74
|
check(r1["status"] != r2["status"], "金额不同 → 裁决结论确实不同(150 批准 / 680 待确认)")
|
|
81
75
|
|
|
82
|
-
# 4. 跨 Agent 编排:日程占用申请
|
|
83
76
|
print("\n【4】跨 Agent 编排:日程占用申请→行政检查冲突")
|
|
84
77
|
r3 = os_.supervisor.cross_request(
|
|
85
78
|
from_agent="assistant", to_agent="admin", request_type="schedule_request",
|
|
@@ -90,9 +83,6 @@ async def main():
|
|
|
90
83
|
check(r3["status"] == "needs_confirmation",
|
|
91
84
|
f"与既有行程时段冲突 → 转人工确认(实为 {r3['status']})")
|
|
92
85
|
|
|
93
|
-
# 5. 【P1-0 守护】目标 Agent 不存在时必须 fail-closed
|
|
94
|
-
# 这是回归守卫:此前该分支返回 "approved"(默认放行)= fail-open —— 发起方会拿到
|
|
95
|
-
# "已批准",但**没有任何 Agent 真的处理过这个请求**。若哪天有人把它改回去,这里必须红。
|
|
96
86
|
print("\n【5】目标 Agent 不存在 → 必须转人工(不得默认放行)")
|
|
97
87
|
r4 = os_.supervisor.cross_request(
|
|
98
88
|
from_agent="admin", to_agent="不存在的Agent", request_type="budget_request",
|
|
@@ -55,14 +55,12 @@ def test_behavior_and_generic_routing():
|
|
|
55
55
|
os_.learning_runtime.import_csv(str(Path("examples/sample_knowledge.csv")))
|
|
56
56
|
os_.itinerary_runtime.import_csv(str(Path("examples/sample_itinerary.csv")))
|
|
57
57
|
|
|
58
|
-
# 态势通用聚合
|
|
59
58
|
sit = os_.supervisor.global_situation()
|
|
60
59
|
assert set(sit["finance"]) >= {"transactions"} and "total_items" in sit["learning"]
|
|
61
60
|
assert "waypoints" in sit["itinerary"]
|
|
62
61
|
print(f" ✓ 态势聚合: fin={sit['finance']}, learn.items={sit['learning']['total_items']}, "
|
|
63
62
|
f"itin.wp={sit['itinerary']['waypoints']}, alerts={len(sit['alerts'])}")
|
|
64
63
|
|
|
65
|
-
# 裁决行为保持(由目标 Agent 守门人裁决)
|
|
66
64
|
r_small = os_.supervisor.cross_request("admin", "finance", "budget_request",
|
|
67
65
|
{"amount": 150, "category": "travel", "entity": "高铁", "account": "default"}, "小额")
|
|
68
66
|
r_big = os_.supervisor.cross_request("admin", "finance", "budget_request",
|
|
@@ -74,15 +72,11 @@ def test_behavior_and_generic_routing():
|
|
|
74
72
|
assert r_sched["status"] in ("needs_confirmation", "approved"), r_sched
|
|
75
73
|
print(f" ✓ 小额={r_small['status']} 大额={r_big['status']} 日程={r_sched['status']}(委托目标守门人)")
|
|
76
74
|
|
|
77
|
-
# 通用性:路由到不存在的 Agent → 转人工确认,不崩(P1-0)
|
|
78
|
-
# 原断言锁的是 status == "approved" —— 那等于"没有任何 Agent 处理过,却回报批准",
|
|
79
|
-
# 是 fail-open:发起方会据此继续推进(如拿到"预算已批"去下单)。
|
|
80
75
|
r_unknown = os_.supervisor.cross_request("x", "nonexistent", "budget_request", {}, "未知目标")
|
|
81
76
|
assert r_unknown["status"] == "needs_confirmation", r_unknown
|
|
82
77
|
assert "不存在" in r_unknown["verdict"]
|
|
83
78
|
print(f" ✓ 路由到不存在 Agent → {r_unknown['status']}(不再 fail-open 放行,仍不崩)")
|
|
84
79
|
|
|
85
|
-
# 深审修复:守门人裁决崩溃(畸形 payload)必须 fail-safe → 转人工确认,绝不自动放行
|
|
86
80
|
r_bad = os_.supervisor.cross_request("x", "admin", "schedule_request",
|
|
87
81
|
{"start_time": "不是合法时间"}, "畸形时间")
|
|
88
82
|
assert r_bad["status"] == "needs_confirmation", f"裁决崩溃应 fail-safe,实得 {r_bad}"
|
|
@@ -30,7 +30,6 @@ def check(cond, label):
|
|
|
30
30
|
print(f" ✗ {label}")
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
# 测试环境自洽:不读 .env,不打外部 API,不写真实数据目录(在任何 foxclaw import 之前)
|
|
34
33
|
os.environ["FOXCLAW_SKIP_DOTENV"] = "1"
|
|
35
34
|
os.environ.setdefault("FOXCLAW_EMBEDDING_MODEL", "foxclaw-nonexistent-fast")
|
|
36
35
|
os.environ["FOXCLAW_DATA_DIR"] = tempfile.mkdtemp(prefix="foxclaw_b7_")
|
|
@@ -39,7 +38,6 @@ for _k in ("DEEPSEEK_API_KEY", "ANTHROPIC_API_KEY", "OPENAI_API_KEY",
|
|
|
39
38
|
os.environ[_k] = ""
|
|
40
39
|
|
|
41
40
|
|
|
42
|
-
# ═══════════════ [1][2] P3-1 / P3-8 辅助函数行为 ═══════════════
|
|
43
41
|
def test_helpers():
|
|
44
42
|
print("\n【1】P3-1:_constant_time_eq 常量时间比较")
|
|
45
43
|
from foxclaw.api import _constant_time_eq, _is_guarded_path
|
|
@@ -60,7 +58,6 @@ def test_helpers():
|
|
|
60
58
|
check(_is_guarded_path("/docs") is False, "/docs 不受保护")
|
|
61
59
|
|
|
62
60
|
|
|
63
|
-
# ═══════════════ [3] P3-1+P3-8 接线防退化(源码级) ═══════════════
|
|
64
61
|
def test_wiring_source():
|
|
65
62
|
print("\n【3】P3-1+P3-8 接线防退化:中间件/WS 统一走辅助函数(源码级断言)")
|
|
66
63
|
import foxclaw.api as api_mod
|
|
@@ -83,7 +80,6 @@ def test_wiring_source():
|
|
|
83
80
|
check("provided != api_key" not in src_ws, "WS 端点无残留 !=")
|
|
84
81
|
|
|
85
82
|
|
|
86
|
-
# ═══════════════ [4] P3-7 isinstance 异常分类 ═══════════════
|
|
87
83
|
def _run_ability_with(exc):
|
|
88
84
|
"""用抛指定异常的假 HKC 驱动 knowledge_ability 端点,返回 (status, body)。"""
|
|
89
85
|
import foxclaw.api as api_mod
|
|
@@ -94,8 +90,8 @@ def _run_ability_with(exc):
|
|
|
94
90
|
raise exc
|
|
95
91
|
|
|
96
92
|
fake_hkc = SimpleNamespace(available=True, client=_Client())
|
|
97
|
-
old_os = api_mod.
|
|
98
|
-
api_mod.
|
|
93
|
+
old_os = api_mod._state.current_os()
|
|
94
|
+
api_mod._state.set_os_instance(SimpleNamespace(hkc=fake_hkc))
|
|
99
95
|
try:
|
|
100
96
|
try:
|
|
101
97
|
body = asyncio.run(api_mod.knowledge_ability("any_key"))
|
|
@@ -106,7 +102,7 @@ def _run_ability_with(exc):
|
|
|
106
102
|
return e.status_code, {"detail": e.detail}
|
|
107
103
|
raise
|
|
108
104
|
finally:
|
|
109
|
-
api_mod.
|
|
105
|
+
api_mod._state.set_os_instance(old_os)
|
|
110
106
|
|
|
111
107
|
|
|
112
108
|
def test_ability_isinstance():
|
|
@@ -115,12 +111,10 @@ def test_ability_isinstance():
|
|
|
115
111
|
|
|
116
112
|
check(api_mod._HKCInsuffCov is not None, "vendor 下 hkc_sdk.exceptions 可 import")
|
|
117
113
|
|
|
118
|
-
# a) 真 HKCInsufficientCoverage → 证据 gate(sufficient=False,非 404)
|
|
119
114
|
from hkc_sdk.exceptions import HKCInsufficientCoverage
|
|
120
115
|
try:
|
|
121
116
|
raise HKCInsufficientCoverage("覆盖不足测试")
|
|
122
117
|
except HKCInsufficientCoverage as e:
|
|
123
|
-
# 尽量补上端点要读的属性(不同版本构造签名可能不同)
|
|
124
118
|
if not hasattr(e, "missing_skills"):
|
|
125
119
|
e.missing_skills = ["s1"]
|
|
126
120
|
if not hasattr(e, "coverage"):
|
|
@@ -129,7 +123,6 @@ def test_ability_isinstance():
|
|
|
129
123
|
check(status == 200 and body.get("sufficient") is False,
|
|
130
124
|
f"真 HKC 覆盖不足 → 证据 gate(实为 {status}: {str(body)[:80]})")
|
|
131
125
|
|
|
132
|
-
# b) 【精确化核心价值】带 missing_skills 属性的**非 HKC** 异常 → 404,不被误吞
|
|
133
126
|
class _FakeInsuff(Exception):
|
|
134
127
|
def __init__(self):
|
|
135
128
|
super().__init__("鸭子异常")
|
|
@@ -140,7 +133,6 @@ def test_ability_isinstance():
|
|
|
140
133
|
check(status == 404,
|
|
141
134
|
f"isinstance 模式:鸭子异常不再误判为覆盖不足(实为 {status})")
|
|
142
135
|
|
|
143
|
-
# c) 回退:_HKCInsuffCov=None(模拟缺 vendor)时 hasattr 鸭子判断仍工作
|
|
144
136
|
old = api_mod._HKCInsuffCov
|
|
145
137
|
api_mod._HKCInsuffCov = None
|
|
146
138
|
try:
|
|
@@ -153,7 +145,6 @@ def test_ability_isinstance():
|
|
|
153
145
|
api_mod._HKCInsuffCov = old
|
|
154
146
|
|
|
155
147
|
|
|
156
|
-
# ═══════════════ [5][6] P3-5 blocked 短路 + P3-0 system 归因 ═══════════════
|
|
157
148
|
class _MockHMR:
|
|
158
149
|
def __init__(self):
|
|
159
150
|
self.calls = []
|
|
@@ -210,7 +201,6 @@ def test_truth_system_attribution():
|
|
|
210
201
|
check(kw.get("shared") is True, "仍写共享层")
|
|
211
202
|
|
|
212
203
|
|
|
213
|
-
# ═══════════════ [7] P3-6/P4-1 SystemBus exact ═══════════════
|
|
214
204
|
def test_system_bus_exact():
|
|
215
205
|
print("\n【7】P3-6/P4-1:SystemBus exact 参数")
|
|
216
206
|
from foxclaw.core.system_bus import SystemBus
|
|
@@ -252,7 +242,6 @@ def test_system_bus_exact():
|
|
|
252
242
|
f"前缀事件 execution.completed.extra 不触发真值链(沉淀 {len(hmr4.calls)} 次)")
|
|
253
243
|
|
|
254
244
|
|
|
255
|
-
# ═══════════════ [8] P5-3 _transition 终态守卫 ═══════════════
|
|
256
245
|
def test_transition_terminal_guard():
|
|
257
246
|
print("\n【8】P5-3:ProcessManager._transition 终态守卫")
|
|
258
247
|
from foxclaw.kernel.process_manager import ProcessManager
|
|
@@ -261,12 +250,10 @@ def test_transition_terminal_guard():
|
|
|
261
250
|
pm.mark_running(rec.process_id)
|
|
262
251
|
pm.mark_completed(rec.process_id)
|
|
263
252
|
n = pm.active_count()
|
|
264
|
-
# 终态再 _transition(模拟未来新增的直接调用点)→ 幂等忽略
|
|
265
253
|
pm._transition(rec, "failed")
|
|
266
254
|
check(rec.state == "completed", "终态再流转 → 状态不变")
|
|
267
255
|
check(pm.active_count() == n, "终态再流转 → 活跃计数不重复 -1")
|
|
268
256
|
check(rec.state_history.count("completed") == 1, "终态再流转 → 历史不追加")
|
|
269
|
-
# 非终态的非法跳转仍 fail-fast(行为不变)
|
|
270
257
|
rec2 = pm.create("t2")
|
|
271
258
|
rec2.state = "created"
|
|
272
259
|
try:
|
|
@@ -276,7 +263,6 @@ def test_transition_terminal_guard():
|
|
|
276
263
|
check(True, "非终态非法跳转(created→completed)仍抛 ValueError")
|
|
277
264
|
|
|
278
265
|
|
|
279
|
-
# ═══════════════ [9] P5-1 reason_code ═══════════════
|
|
280
266
|
def test_reason_code():
|
|
281
267
|
print("\n【9】P5-1:PermissionDecision.reason_code 分类")
|
|
282
268
|
from foxclaw.skills import SkillSpec
|