foxclaw 0.1.0 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -29
- package/bin/foxclaw.cjs +51 -0
- package/index.js +4 -10
- package/package.json +46 -38
- package/python/.dockerignore +35 -0
- package/python/.env.example +173 -0
- package/python/.gitattributes +46 -0
- package/python/.github/workflows/ci.yml +167 -0
- package/python/.github/workflows/release.yml +183 -0
- package/python/CHANGELOG.md +57 -0
- package/python/CHANGELOG_CN.md +57 -0
- package/python/Dockerfile +64 -0
- package/python/LICENSE +21 -0
- package/python/MANIFEST.in +14 -0
- package/python/README.md +201 -0
- package/python/README_CN.md +336 -0
- package/python/bench/bm25_audit.py +186 -0
- package/python/bench/bm25_regression.py +177 -0
- package/python/bench/bm25_stress.py +155 -0
- package/python/bench/cd_audit.py +215 -0
- package/python/bench/chaos.py +140 -0
- package/python/bench/concurrency.py +103 -0
- package/python/bench/e2e_real_models.py +94 -0
- package/python/bench/llm_retry_test.py +241 -0
- package/python/bench/load_test.py +193 -0
- package/python/bench/vector_ivf_upgrade.py +128 -0
- package/python/docs/CHANNELS.md +195 -0
- package/python/docs/GOVERNANCE.md +164 -0
- package/python/docs/RELEASE.md +110 -0
- package/python/docs/RUN.md +75 -0
- package/python/docs/RUN_CN.md +121 -0
- package/python/docs//351/205/215/347/275/256/346/214/207/345/215/227.md +152 -0
- package/python/evals/__init__.py +0 -0
- package/python/evals/governance/README.md +61 -0
- package/python/evals/governance/__init__.py +0 -0
- package/python/evals/governance/cases.py +115 -0
- package/python/evals/governance/compare_embeddings.py +94 -0
- package/python/evals/governance/convergence.svg +11 -0
- package/python/evals/governance/report.json +371 -0
- package/python/evals/governance/run_evals.py +368 -0
- package/python/examples/governance_middleware_demo.py +72 -0
- package/python/examples/langgraph_governance_demo.py +104 -0
- package/python/examples/sample_itinerary.csv +6 -0
- package/python/examples/sample_knowledge.csv +9 -0
- package/python/examples/sample_transactions.csv +14 -0
- package/python/foxclaw/__init__.py +408 -0
- package/python/foxclaw/agents/__init__.py +12 -0
- package/python/foxclaw/agents/agents.py +35 -0
- package/python/foxclaw/agents/base.py +794 -0
- package/python/foxclaw/agents/impl/__init__.py +1 -0
- package/python/foxclaw/agents/impl/_helpers.py +70 -0
- package/python/foxclaw/agents/impl/finance.py +231 -0
- package/python/foxclaw/agents/impl/office.py +350 -0
- package/python/foxclaw/agents/impl/supervisor.py +592 -0
- package/python/foxclaw/agents/mixins/_context.py +128 -0
- package/python/foxclaw/agents/mixins/augment.py +143 -0
- package/python/foxclaw/agents/mixins/llm.py +67 -0
- package/python/foxclaw/agents/mixins/memory.py +41 -0
- package/python/foxclaw/agents/mixins/preference.py +131 -0
- package/python/foxclaw/agents/mixins/prompt.py +71 -0
- package/python/foxclaw/agents/mixins/skillbox.py +58 -0
- package/python/foxclaw/agents/mixins/tool_exec.py +417 -0
- package/python/foxclaw/api/__init__.py +224 -0
- package/python/foxclaw/api/_state.py +43 -0
- package/python/foxclaw/api/bootstrap.py +96 -0
- package/python/foxclaw/api/middleware.py +294 -0
- package/python/foxclaw/api/routes/ahs.py +404 -0
- package/python/foxclaw/api/routes/attachments.py +116 -0
- package/python/foxclaw/api/routes/browser.py +35 -0
- package/python/foxclaw/api/routes/domain.py +306 -0
- package/python/foxclaw/api/routes/knowledge.py +145 -0
- package/python/foxclaw/api/routes/llm_admin.py +449 -0
- package/python/foxclaw/api/routes/memory.py +105 -0
- package/python/foxclaw/api/routes/orchestration.py +39 -0
- package/python/foxclaw/api/routes/recovery.py +44 -0
- package/python/foxclaw/api/routes/review.py +266 -0
- package/python/foxclaw/api/routes/search.py +33 -0
- package/python/foxclaw/api/routes/system.py +311 -0
- package/python/foxclaw/api/routes/task.py +263 -0
- package/python/foxclaw/api/routes/ws.py +64 -0
- package/python/foxclaw/api/spine.py +52 -0
- package/python/foxclaw/browser/__init__.py +13 -0
- package/python/foxclaw/browser/action/__init__.py +1 -0
- package/python/foxclaw/browser/action/action_executor.py +50 -0
- package/python/foxclaw/browser/action/action_types.py +44 -0
- package/python/foxclaw/browser/action/action_verifier.py +68 -0
- package/python/foxclaw/browser/action/url_policy.py +143 -0
- package/python/foxclaw/browser/adapter/__init__.py +1 -0
- package/python/foxclaw/browser/adapter/base_adapter.py +23 -0
- package/python/foxclaw/browser/adapter/web_adapter.py +46 -0
- package/python/foxclaw/browser/driver/__init__.py +1 -0
- package/python/foxclaw/browser/driver/base_driver.py +61 -0
- package/python/foxclaw/browser/driver/playwright_driver.py +170 -0
- package/python/foxclaw/browser/perception/__init__.py +1 -0
- package/python/foxclaw/browser/perception/dom_extractor.py +42 -0
- package/python/foxclaw/browser/perception/element_indexer.py +56 -0
- package/python/foxclaw/browser/perception/semantic_reducer.py +33 -0
- package/python/foxclaw/browser/perception/snapshot.py +29 -0
- package/python/foxclaw/browser/service.py +144 -0
- package/python/foxclaw/browser/session/__init__.py +1 -0
- package/python/foxclaw/browser/session/session_manager.py +57 -0
- package/python/foxclaw/capability/__init__.py +12 -0
- package/python/foxclaw/capability/embedding_service.py +65 -0
- package/python/foxclaw/channels/__init__.py +138 -0
- package/python/foxclaw/channels/discord.py +175 -0
- package/python/foxclaw/channels/dispatcher.py +179 -0
- package/python/foxclaw/channels/feishu.py +215 -0
- package/python/foxclaw/channels/openclaw_bridge.py +151 -0
- package/python/foxclaw/channels/signing.py +187 -0
- package/python/foxclaw/channels/telegram.py +120 -0
- package/python/foxclaw/channels/weixin.py +215 -0
- package/python/foxclaw/cli.py +183 -0
- package/python/foxclaw/cognitive/__init__.py +4 -0
- package/python/foxclaw/cognitive/reasoner.py +81 -0
- package/python/foxclaw/cognitive/state.py +141 -0
- package/python/foxclaw/core/__init__.py +9 -0
- package/python/foxclaw/core/audit_chain.py +131 -0
- package/python/foxclaw/core/audit_sink.py +100 -0
- package/python/foxclaw/core/compaction.py +265 -0
- package/python/foxclaw/core/config.py +65 -0
- package/python/foxclaw/core/confirm_nonce.py +151 -0
- package/python/foxclaw/core/dispatch.py +27 -0
- package/python/foxclaw/core/envfile.py +48 -0
- package/python/foxclaw/core/failure_policy.py +142 -0
- package/python/foxclaw/core/gateway.py +81 -0
- package/python/foxclaw/core/governance_audit.py +171 -0
- package/python/foxclaw/core/idempotency.py +282 -0
- package/python/foxclaw/core/injection_guard.py +202 -0
- package/python/foxclaw/core/memory_adapter.py +489 -0
- package/python/foxclaw/core/metrics.py +88 -0
- package/python/foxclaw/core/model_router.py +985 -0
- package/python/foxclaw/core/paths.py +103 -0
- package/python/foxclaw/core/recovery.py +349 -0
- package/python/foxclaw/core/scheduler.py +141 -0
- package/python/foxclaw/core/secrets.py +131 -0
- package/python/foxclaw/core/sessions.py +155 -0
- package/python/foxclaw/core/settings.py +414 -0
- package/python/foxclaw/core/shared_write.py +186 -0
- package/python/foxclaw/core/steering_queue.py +154 -0
- package/python/foxclaw/core/system_bus.py +84 -0
- package/python/foxclaw/core/thought_chain.py +316 -0
- package/python/foxclaw/core/token_meter.py +231 -0
- package/python/foxclaw/core/tools.py +164 -0
- package/python/foxclaw/core/tracing.py +375 -0
- package/python/foxclaw/core/truth_pipeline.py +110 -0
- package/python/foxclaw/core/update_check.py +149 -0
- package/python/foxclaw/core/vec_recall_adapter.py +477 -0
- package/python/foxclaw/core/verify.py +173 -0
- package/python/foxclaw/core/web_search.py +218 -0
- package/python/foxclaw/finance/__init__.py +15 -0
- package/python/foxclaw/finance/evidence.py +149 -0
- package/python/foxclaw/finance/fx.py +98 -0
- package/python/foxclaw/finance/graph.py +179 -0
- package/python/foxclaw/finance/models.py +146 -0
- package/python/foxclaw/finance/nlparse.py +515 -0
- package/python/foxclaw/finance/policy.py +301 -0
- package/python/foxclaw/finance/reasoner.py +121 -0
- package/python/foxclaw/finance/reasoning.py +319 -0
- package/python/foxclaw/finance/reconcile.py +175 -0
- package/python/foxclaw/finance/runtime.py +500 -0
- package/python/foxclaw/governance/__init__.py +24 -0
- package/python/foxclaw/governance/middleware.py +176 -0
- package/python/foxclaw/harness/__init__.py +17 -0
- package/python/foxclaw/harness/_base.py +66 -0
- package/python/foxclaw/harness/_policy.py +340 -0
- package/python/foxclaw/harness/drift.py +77 -0
- package/python/foxclaw/harness/event_bus.py +28 -0
- package/python/foxclaw/harness/harness.py +358 -0
- package/python/foxclaw/harness/loop_detection.py +145 -0
- package/python/foxclaw/itinerary/__init__.py +10 -0
- package/python/foxclaw/itinerary/amap_client.py +114 -0
- package/python/foxclaw/itinerary/models.py +80 -0
- package/python/foxclaw/itinerary/reasoner.py +96 -0
- package/python/foxclaw/itinerary/reasoning.py +168 -0
- package/python/foxclaw/itinerary/runtime.py +144 -0
- package/python/foxclaw/kernel/__init__.py +13 -0
- package/python/foxclaw/kernel/capabilities.py +72 -0
- package/python/foxclaw/kernel/policy_gate.py +108 -0
- package/python/foxclaw/kernel/process_manager.py +186 -0
- package/python/foxclaw/kernel/scheduler.py +125 -0
- package/python/foxclaw/kernel/sentinel.py +443 -0
- package/python/foxclaw/kernel/sentinel_client.py +243 -0
- package/python/foxclaw/kernel/spine.py +97 -0
- package/python/foxclaw/knowledge/__init__.py +360 -0
- package/python/foxclaw/knowledge/truth_crystallizer.py +89 -0
- package/python/foxclaw/learning/__init__.py +12 -0
- package/python/foxclaw/learning/knowledge_graph.py +144 -0
- package/python/foxclaw/learning/models.py +93 -0
- package/python/foxclaw/learning/reasoner.py +67 -0
- package/python/foxclaw/learning/runtime.py +351 -0
- package/python/foxclaw/learning/spaced_repetition.py +91 -0
- package/python/foxclaw/mcp_server.py +188 -0
- package/python/foxclaw/orchestration/__init__.py +167 -0
- package/python/foxclaw/pipeline/__init__.py +23 -0
- package/python/foxclaw/pipeline/cognitive_pipeline.py +429 -0
- package/python/foxclaw/pipeline/stage_meta.py +21 -0
- package/python/foxclaw/pipeline/supervisor_stages.py +201 -0
- package/python/foxclaw/planning/__init__.py +17 -0
- package/python/foxclaw/planning/llm_planner.py +232 -0
- package/python/foxclaw/planning/planner.py +94 -0
- package/python/foxclaw/search/__init__.py +24 -0
- package/python/foxclaw/search/agent_port.py +190 -0
- package/python/foxclaw/search/evidence.py +109 -0
- package/python/foxclaw/search/external_search.py +50 -0
- package/python/foxclaw/search/hkc_read_port.py +60 -0
- package/python/foxclaw/search/internal_search.py +71 -0
- package/python/foxclaw/search/models.py +127 -0
- package/python/foxclaw/search/planner.py +44 -0
- package/python/foxclaw/search/retriever.py +39 -0
- package/python/foxclaw/search/runtime.py +127 -0
- package/python/foxclaw/search/strategy.py +44 -0
- package/python/foxclaw/skills/__init__.py +559 -0
- package/python/foxclaw/skills/archive.py +111 -0
- package/python/foxclaw/skills/fetch.py +226 -0
- package/python/foxclaw/skills/hooks_spec.py +69 -0
- package/python/foxclaw/skills/install_cmd.py +216 -0
- package/python/foxclaw/skills/lifecycle.py +252 -0
- package/python/foxclaw/skills/markdown_skill.py +175 -0
- package/python/foxclaw/skills/markdown_store.py +39 -0
- package/python/foxclaw/skills/mcp_client.py +184 -0
- package/python/foxclaw/skills/mcp_store.py +40 -0
- package/python/foxclaw/skills/permission.py +145 -0
- package/python/foxclaw/skills/pipeline.py +354 -0
- package/python/foxclaw/skills/policy.py +74 -0
- package/python/foxclaw/skills/review.py +72 -0
- package/python/foxclaw/skills/store.py +57 -0
- package/python/frontend/assets/core_bg.jpg +0 -0
- package/python/frontend/deck/auth.js +25 -0
- package/python/frontend/deck/chat_model_picker.js +483 -0
- package/python/frontend/deck/domain.js +276 -0
- package/python/frontend/deck/main.js +1806 -0
- package/python/frontend/deck/markdown.js +44 -0
- package/python/frontend/deck/settings.js +307 -0
- package/python/frontend/deck/state.js +5 -0
- package/python/frontend/deck/util.js +4 -0
- package/python/frontend/deck/visuals.js +190 -0
- package/python/frontend/deck.css +855 -0
- package/python/frontend/deck.html +686 -0
- package/python/frontend/deck_v2.html +341 -0
- package/python/frontend/planet/index.html +266 -0
- package/python/frontend/planet/planet.css +579 -0
- package/python/frontend/planet/planet.js +1436 -0
- package/python/frontend/planet/vendor/three.core.min.js +6 -0
- package/python/frontend/planet/vendor/three.module.min.js +6 -0
- package/python/frontend/v3/assets/index-CQSBdxk4.js +34 -0
- package/python/frontend/v3/assets/index-D5nIsjms.css +1 -0
- package/python/frontend/v3/index.html +15 -0
- package/python/frontend/widget_runtime.js +252 -0
- package/python/frontend/widgets.js +639 -0
- package/python/frontend_src/index.html +14 -0
- package/python/frontend_src/package-lock.json +1376 -0
- package/python/frontend_src/package.json +19 -0
- package/python/frontend_src/src/App.vue +58 -0
- package/python/frontend_src/src/api.js +82 -0
- package/python/frontend_src/src/components/ChatDock.vue +365 -0
- package/python/frontend_src/src/components/ComposerMenu.vue +75 -0
- package/python/frontend_src/src/components/DocViewer.vue +162 -0
- package/python/frontend_src/src/components/Inspector.vue +235 -0
- package/python/frontend_src/src/components/LeftRail.vue +415 -0
- package/python/frontend_src/src/components/MenuPopover.vue +128 -0
- package/python/frontend_src/src/components/MessageStream.vue +389 -0
- package/python/frontend_src/src/components/ModelPicker.vue +350 -0
- package/python/frontend_src/src/components/QuickIngest.vue +112 -0
- package/python/frontend_src/src/components/Stage.vue +272 -0
- package/python/frontend_src/src/components/ToolMenu.vue +141 -0
- package/python/frontend_src/src/components/TopBar.vue +118 -0
- package/python/frontend_src/src/components/panels/ActivityPanel.vue +82 -0
- package/python/frontend_src/src/components/panels/AgentsPanel.vue +83 -0
- package/python/frontend_src/src/components/panels/KnowledgePanel.vue +425 -0
- package/python/frontend_src/src/components/panels/MemoryPanel.vue +117 -0
- package/python/frontend_src/src/components/panels/PanelBox.vue +50 -0
- package/python/frontend_src/src/components/panels/ReviewPanel.vue +292 -0
- package/python/frontend_src/src/components/panels/SettingsPanel.vue +358 -0
- package/python/frontend_src/src/components/panels/TasksPanel.vue +73 -0
- package/python/frontend_src/src/components/panels/ToolsPanel.vue +746 -0
- package/python/frontend_src/src/components/panels/UsagePanel.vue +105 -0
- package/python/frontend_src/src/lib/fmt.js +44 -0
- package/python/frontend_src/src/lib/i18n.js +53 -0
- package/python/frontend_src/src/lib/locales/en.js +173 -0
- package/python/frontend_src/src/lib/locales/zh.js +174 -0
- package/python/frontend_src/src/lib/md.js +190 -0
- package/python/frontend_src/src/main.js +9 -0
- package/python/frontend_src/src/stores/app.js +91 -0
- package/python/frontend_src/src/stores/chat.js +450 -0
- package/python/frontend_src/src/stores/model.js +96 -0
- package/python/frontend_src/src/stores/panels.js +289 -0
- package/python/frontend_src/src/stores/profile.js +31 -0
- package/python/frontend_src/src/stores/system.js +25 -0
- package/python/frontend_src/src/styles/base.css +138 -0
- package/python/frontend_src/src/styles/panel.css +117 -0
- package/python/frontend_src/src/styles/tokens.css +78 -0
- package/python/frontend_src/vite.config.js +39 -0
- package/python/pyproject.toml +268 -0
- package/python/requirements.txt +311 -0
- package/python/skills/README.md +123 -0
- package/python/skills/execution-contract/SKILL.md +30 -0
- package/python/skills/general-assistant/SKILL.md +34 -0
- package/python/skills/planner/SKILL.md +33 -0
- package/python/skills/schedule-itinerary/SKILL.md +35 -0
- package/python/skills/skill-composition/SKILL.md +31 -0
- package/python/skills/skill-guard/SKILL.md +67 -0
- package/python/skills/skill-memory/SKILL.md +50 -0
- package/python/skills/skill-router/SKILL.md +35 -0
- package/python/tests/conftest.py +54 -0
- package/python/tests/run_all.py +200 -0
- package/python/tests/test_agent_lifecycle.py +326 -0
- package/python/tests/test_ahs_p2.py +393 -0
- package/python/tests/test_anthropic_tools.py +192 -0
- package/python/tests/test_api.py +97 -0
- package/python/tests/test_api_auth.py +79 -0
- package/python/tests/test_api_host_guard.py +69 -0
- package/python/tests/test_attachments.py +151 -0
- package/python/tests/test_audit_chain.py +116 -0
- package/python/tests/test_audit_sink.py +130 -0
- package/python/tests/test_browser.py +149 -0
- package/python/tests/test_browser_request_guard.py +180 -0
- package/python/tests/test_capabilities.py +152 -0
- package/python/tests/test_channel_signing.py +185 -0
- package/python/tests/test_channel_trust.py +109 -0
- package/python/tests/test_channels.py +174 -0
- package/python/tests/test_cli.py +46 -0
- package/python/tests/test_compaction_toolpair.py +119 -0
- package/python/tests/test_config_layering.py +61 -0
- package/python/tests/test_confirm_nonce.py +186 -0
- package/python/tests/test_context_compaction_wiring.py +124 -0
- package/python/tests/test_dispatch.py +89 -0
- package/python/tests/test_domain_evals.py +334 -0
- package/python/tests/test_domain_import.py +72 -0
- package/python/tests/test_domain_reasoners.py +134 -0
- package/python/tests/test_e2e.py +161 -0
- package/python/tests/test_emb_cache.py +97 -0
- package/python/tests/test_embedding_singleton.py +87 -0
- package/python/tests/test_envfile.py +83 -0
- package/python/tests/test_evals_smoke.py +90 -0
- package/python/tests/test_event_spine.py +174 -0
- package/python/tests/test_evidence_semantic.py +88 -0
- package/python/tests/test_evidence_sse.py +94 -0
- package/python/tests/test_file_ingest.py +135 -0
- package/python/tests/test_finance.py +144 -0
- package/python/tests/test_finance_ledger.py +245 -0
- package/python/tests/test_finance_persist.py +174 -0
- package/python/tests/test_gateway.py +125 -0
- package/python/tests/test_governance.py +421 -0
- package/python/tests/test_governance_audit.py +324 -0
- package/python/tests/test_governance_middleware.py +137 -0
- package/python/tests/test_hkc.py +268 -0
- package/python/tests/test_hkc_index_build_manager.py +306 -0
- package/python/tests/test_hkc_manifest_reuse.py +294 -0
- package/python/tests/test_hkc_search_event_sync.py +482 -0
- package/python/tests/test_import_path_guard.py +155 -0
- package/python/tests/test_injection_guard.py +193 -0
- package/python/tests/test_integration.py +117 -0
- package/python/tests/test_itinerary.py +100 -0
- package/python/tests/test_kee_semantic.py +84 -0
- package/python/tests/test_kernel.py +212 -0
- package/python/tests/test_learning.py +101 -0
- package/python/tests/test_llm_admin.py +145 -0
- package/python/tests/test_llm_planner.py +344 -0
- package/python/tests/test_markdown_skills.py +62 -0
- package/python/tests/test_markdown_skills_openclaw.py +66 -0
- package/python/tests/test_mcp_connectors.py +97 -0
- package/python/tests/test_mcp_guardian_discovery.py +87 -0
- package/python/tests/test_mcp_no_memory.py +43 -0
- package/python/tests/test_mcp_policy_gate.py +76 -0
- package/python/tests/test_mcp_server.py +92 -0
- package/python/tests/test_memory_recall.py +92 -0
- package/python/tests/test_metrics.py +58 -0
- package/python/tests/test_multi_chain.py +90 -0
- package/python/tests/test_multimodal.py +122 -0
- package/python/tests/test_negotiation.py +136 -0
- package/python/tests/test_orchestration.py +109 -0
- package/python/tests/test_orchestration_decoupled.py +103 -0
- package/python/tests/test_p3_p5_hardening.py +324 -0
- package/python/tests/test_parallel_dispatch.py +207 -0
- package/python/tests/test_pipeline.py +492 -0
- package/python/tests/test_plan_visible.py +86 -0
- package/python/tests/test_planner.py +88 -0
- package/python/tests/test_policy_gate.py +108 -0
- package/python/tests/test_profile.py +67 -0
- package/python/tests/test_readme_claims.py +163 -0
- package/python/tests/test_reasoner.py +170 -0
- package/python/tests/test_release_check.py +252 -0
- package/python/tests/test_resume_checkpoint.py +148 -0
- package/python/tests/test_routing.py +85 -0
- package/python/tests/test_scheduler.py +138 -0
- package/python/tests/test_search.py +194 -0
- package/python/tests/test_secrets.py +153 -0
- package/python/tests/test_semantic.py +69 -0
- package/python/tests/test_sessions.py +90 -0
- package/python/tests/test_settings.py +265 -0
- package/python/tests/test_shadow_learning.py +290 -0
- package/python/tests/test_shared_write.py +200 -0
- package/python/tests/test_skill_taint_gate.py +145 -0
- package/python/tests/test_skills.py +117 -0
- package/python/tests/test_skills_fetch.py +282 -0
- package/python/tests/test_stats_and_events.py +162 -0
- package/python/tests/test_streaming.py +55 -0
- package/python/tests/test_supervisor_pipeline.py +176 -0
- package/python/tests/test_supervisor_triage.py +102 -0
- package/python/tests/test_taint.py +112 -0
- package/python/tests/test_task_stream.py +74 -0
- package/python/tests/test_thinking_sse.py +66 -0
- package/python/tests/test_thinking_stream.py +76 -0
- package/python/tests/test_tool_center.py +113 -0
- package/python/tests/test_tool_parallel.py +113 -0
- package/python/tests/test_tool_params.py +99 -0
- package/python/tests/test_tool_use.py +229 -0
- package/python/tests/test_toplevel_concurrency.py +116 -0
- package/python/tests/test_tracing.py +256 -0
- package/python/tests/test_truth_crystallizer.py +125 -0
- package/python/tests/test_truth_flow.py +88 -0
- package/python/tests/test_update_check.py +95 -0
- package/python/tests/test_url_policy.py +53 -0
- package/python/tests/test_verify_evidence.py +212 -0
- package/python/tests/test_version_sync.py +100 -0
- package/python/tests/test_web_trigger.py +82 -0
- package/python/tools/check_faiss_type.py +30 -0
- package/python/tools/check_log.ps1 +38 -0
- package/python/tools/ivf_status.py +72 -0
- package/python/tools/mypy_baseline.txt +13 -0
- package/python/tools/mypy_ratchet.py +143 -0
- package/python/tools/pack.py +173 -0
- package/python/tools/release_check.py +349 -0
- package/python/tools/restart_foxclaw.ps1 +71 -0
- package/python/tools/shared_write_audit.py +190 -0
- package/python/tools/size_baseline.txt +28 -0
- package/python/tools/size_check.py +102 -0
- package/python/tools/strip_comments.py +228 -0
- package/python/tools/verify_env.bat +2 -0
- package/python/tools/verify_env.py +25 -0
- package/python/vendor/HMR_LICENSE +37 -0
- package/python/vendor/HMR_README.md +65 -0
- package/python/vendor/hkc/LICENSE +37 -0
- package/python/vendor/hkc/hkc_ace/__init__.py +0 -0
- package/python/vendor/hkc/hkc_ace/ace.py +457 -0
- package/python/vendor/hkc/hkc_ace/skill_taxonomy.json +465 -0
- package/python/vendor/hkc/hkc_api/__init__.py +0 -0
- package/python/vendor/hkc/hkc_api/container.py +416 -0
- package/python/vendor/hkc/hkc_api/ingest_jobs.py +103 -0
- package/python/vendor/hkc/hkc_api/main.py +157 -0
- package/python/vendor/hkc/hkc_api/routes/__init__.py +0 -0
- package/python/vendor/hkc/hkc_api/routes/ability.py +151 -0
- package/python/vendor/hkc/hkc_api/routes/config.py +51 -0
- package/python/vendor/hkc/hkc_api/routes/knowledge.py +630 -0
- package/python/vendor/hkc/hkc_api/routes/search.py +88 -0
- package/python/vendor/hkc/hkc_api/schemas.py +153 -0
- package/python/vendor/hkc/hkc_api/synthesis.py +179 -0
- package/python/vendor/hkc/hkc_core/__init__.py +0 -0
- package/python/vendor/hkc/hkc_core/graph/__init__.py +0 -0
- package/python/vendor/hkc/hkc_core/graph/base.py +188 -0
- package/python/vendor/hkc/hkc_core/graph/sqlite_store.py +789 -0
- package/python/vendor/hkc/hkc_core/models/__init__.py +0 -0
- package/python/vendor/hkc/hkc_core/models/enums.py +65 -0
- package/python/vendor/hkc/hkc_core/models/ku.py +307 -0
- package/python/vendor/hkc/hkc_core/utils/__init__.py +0 -0
- package/python/vendor/hkc/hkc_core/utils/id_gen.py +113 -0
- package/python/vendor/hkc/hkc_crystallizer/__init__.py +44 -0
- package/python/vendor/hkc/hkc_crystallizer/adapters.py +94 -0
- package/python/vendor/hkc/hkc_crystallizer/candidate.py +145 -0
- package/python/vendor/hkc/hkc_crystallizer/crystallizer.py +179 -0
- package/python/vendor/hkc/hkc_crystallizer/ingress.py +90 -0
- package/python/vendor/hkc/hkc_kde/__init__.py +0 -0
- package/python/vendor/hkc/hkc_kde/chunker.py +197 -0
- package/python/vendor/hkc/hkc_kde/extractor.py +623 -0
- package/python/vendor/hkc/hkc_kde/kde.py +298 -0
- package/python/vendor/hkc/hkc_kde/loaders/__init__.py +0 -0
- package/python/vendor/hkc/hkc_kde/loaders/ebook_loader.py +153 -0
- package/python/vendor/hkc/hkc_kde/loaders/office_loader.py +194 -0
- package/python/vendor/hkc/hkc_kde/loaders/pdf_loader.py +162 -0
- package/python/vendor/hkc/hkc_kde/loaders/text_loader.py +157 -0
- package/python/vendor/hkc/hkc_kde/loaders/web_loader.py +91 -0
- package/python/vendor/hkc/hkc_kde/models.py +74 -0
- package/python/vendor/hkc/hkc_kde/packager.py +329 -0
- package/python/vendor/hkc/hkc_kde/relation_disc.py +151 -0
- package/python/vendor/hkc/hkc_kee/__init__.py +0 -0
- package/python/vendor/hkc/hkc_kee/dedup.py +227 -0
- package/python/vendor/hkc/hkc_kee/fingerprint.py +106 -0
- package/python/vendor/hkc/hkc_kee/kee.py +541 -0
- package/python/vendor/hkc/hkc_kep/__init__.py +0 -0
- package/python/vendor/hkc/hkc_kep/event_bus.py +205 -0
- package/python/vendor/hkc/hkc_sdk/__init__.py +63 -0
- package/python/vendor/hkc/hkc_sdk/base.py +151 -0
- package/python/vendor/hkc/hkc_sdk/direct_client.py +153 -0
- package/python/vendor/hkc/hkc_sdk/exceptions.py +39 -0
- package/python/vendor/hkc/hkc_sdk/http_client.py +166 -0
- package/python/vendor/hkc/hkc_sdk/models.py +171 -0
- package/python/vendor/hkc/hkc_search/__init__.py +0 -0
- package/python/vendor/hkc/hkc_search/bm25.py +325 -0
- package/python/vendor/hkc/hkc_search/embedding_backends.py +374 -0
- package/python/vendor/hkc/hkc_search/graph_search.py +132 -0
- package/python/vendor/hkc/hkc_search/hybrid.py +331 -0
- package/python/vendor/hkc/hkc_search/index_build_manager.py +378 -0
- package/python/vendor/hkc/hkc_search/vector_search.py +484 -0
- package/python/vendor/hmr/__init__.py +0 -0
- package/python/vendor/hmr/core/__init__.py +0 -0
- package/python/vendor/hmr/core/hmr.py +684 -0
- package/python/vendor/hmr/core/models.py +310 -0
- package/python/vendor/hmr/engines/__init__.py +0 -0
- package/python/vendor/hmr/engines/evolution.py +710 -0
- package/python/vendor/hmr/engines/jit_compiler.py +472 -0
- package/python/vendor/hmr/engines/lifecycle.py +264 -0
- package/python/vendor/hmr/engines/policy.py +625 -0
- package/python/vendor/hmr/engines/recall.py +260 -0
- package/python/vendor/hmr/engines/runtime_state.py +158 -0
- package/python/vendor/hmr/engines/scheduler.py +323 -0
- package/python/vendor/hmr/engines/semantic.py +150 -0
- package/python/vendor/hmr/engines/temporal.py +261 -0
- package/python/vendor/hmr/engines/thought_chain.py +635 -0
- package/python/vendor/hmr/graph/__init__.py +0 -0
- package/python/vendor/hmr/graph/cwg.py +338 -0
- package/python/vendor/hmr/graph/memory_graph.py +530 -0
- package/python/vendor/hmr/storage/__init__.py +0 -0
- package/python/vendor/hmr/storage/memory_fs.py +300 -0
- package/python/vendor/hmr/storage/vector_store.py +520 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""发布前校验脚本(tools/release_check.py)的规则测试。
|
|
2
|
+
|
|
3
|
+
锁住三件事:
|
|
4
|
+
1. 四处版本号必须一致 —— 只改一处的发布物会让用户装到对不上的版本;
|
|
5
|
+
2. tag 必须指向本次版本 —— 发布不可撤回,yank 也仍占号;
|
|
6
|
+
3. 产物里不能有 .env / docs/internal / 运行期数据,缺 vendor 只算提示(占位包现状)。
|
|
7
|
+
|
|
8
|
+
脚本放在 tools/ 不在包里,故按文件路径加载;测试只构造最小归档,不跑真构建(秒级)。
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import importlib.util
|
|
14
|
+
import io
|
|
15
|
+
import json
|
|
16
|
+
import shutil
|
|
17
|
+
import sys
|
|
18
|
+
import tarfile
|
|
19
|
+
import zipfile
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
import pytest
|
|
23
|
+
|
|
24
|
+
REPO = Path(__file__).resolve().parents[1]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _load():
|
|
28
|
+
spec = importlib.util.spec_from_file_location(
|
|
29
|
+
"release_check", REPO / "tools" / "release_check.py")
|
|
30
|
+
mod = importlib.util.module_from_spec(spec)
|
|
31
|
+
sys.modules["release_check"] = mod
|
|
32
|
+
spec.loader.exec_module(mod)
|
|
33
|
+
return mod
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
rc = _load()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _wheel(dist: Path, version="0.1.0", name="foxclaw", extra=(),
|
|
40
|
+
description=True) -> Path:
|
|
41
|
+
p = dist / f"{name}-{version}-py3-none-any.whl"
|
|
42
|
+
body = ("\n\n" + "long description body. " * 12) if description else ""
|
|
43
|
+
with zipfile.ZipFile(p, "w") as z:
|
|
44
|
+
z.writestr(f"{name}/__init__.py", "__version__ = '0.1.0'\n")
|
|
45
|
+
z.writestr(f"{name}/cli.py", "def main(): pass\n")
|
|
46
|
+
z.writestr(f"{name}-{version}.dist-info/METADATA",
|
|
47
|
+
f"Name: {name}\nVersion: {version}{body}\n")
|
|
48
|
+
for e in extra:
|
|
49
|
+
z.writestr(e, "x")
|
|
50
|
+
return p
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _sdist(dist: Path, version="0.1.0", name="foxclaw", extra=()) -> Path:
|
|
54
|
+
p = dist / f"{name}-{version}.tar.gz"
|
|
55
|
+
with tarfile.open(p, "w:gz") as t:
|
|
56
|
+
for arc in (f"{name}-{version}/foxclaw/cli.py",
|
|
57
|
+
f"{name}-{version}/PKG-INFO", *extra):
|
|
58
|
+
data = b"x"
|
|
59
|
+
info = tarfile.TarInfo(arc)
|
|
60
|
+
info.size = len(data)
|
|
61
|
+
t.addfile(info, io.BytesIO(data))
|
|
62
|
+
return p
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _tgz(dist: Path, version="0.1.0", name="foxclaw", pkg=None, extra=()) -> Path:
|
|
66
|
+
p = dist / f"{name}-{version}.tgz"
|
|
67
|
+
payload = json.dumps(pkg if pkg is not None else
|
|
68
|
+
{"name": name, "version": version}).encode()
|
|
69
|
+
with tarfile.open(p, "w:gz") as t:
|
|
70
|
+
for arc, data in (("package/package.json", payload),
|
|
71
|
+
("package/index.js", b"export default {}"),
|
|
72
|
+
*[(e, b"x") for e in extra]):
|
|
73
|
+
info = tarfile.TarInfo(arc)
|
|
74
|
+
info.size = len(data)
|
|
75
|
+
t.addfile(info, io.BytesIO(data))
|
|
76
|
+
return p
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _mini_repo(root: Path, version: str, npm_version: str) -> Path:
|
|
80
|
+
"""只含四处版本来源的最小仓库,用来测"漏改一处"。"""
|
|
81
|
+
(root / "foxclaw").mkdir(parents=True)
|
|
82
|
+
(root / "npm").mkdir()
|
|
83
|
+
(root / "pyproject.toml").write_text(
|
|
84
|
+
f'[project]\nname = "foxclaw"\nversion = "{version}"\n', encoding="utf-8")
|
|
85
|
+
(root / "foxclaw" / "__init__.py").write_text(
|
|
86
|
+
f'__version__ = "{version}"\n', encoding="utf-8")
|
|
87
|
+
(root / "npm" / "package.json").write_text(
|
|
88
|
+
json.dumps({"name": "foxclaw", "version": npm_version}), encoding="utf-8")
|
|
89
|
+
(root / "npm" / "index.js").write_text(
|
|
90
|
+
f'export const version = "{npm_version}";\n', encoding="utf-8")
|
|
91
|
+
return root
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_repo_versions_are_consistent():
|
|
95
|
+
"""真仓库当前状态:四处一致,无 error。"""
|
|
96
|
+
assert [i for i in rc.check_versions(REPO) if i.level == "error"] == []
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_single_missed_source_is_an_error(tmp_path):
|
|
100
|
+
root = _mini_repo(tmp_path, "0.2.0", npm_version="0.1.0")
|
|
101
|
+
errs = [i for i in rc.check_versions(root) if i.level == "error"]
|
|
102
|
+
assert errs and any("npm" in i.msg for i in errs), errs
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_unreadable_source_is_an_error(tmp_path):
|
|
106
|
+
root = _mini_repo(tmp_path, "0.1.0", "0.1.0")
|
|
107
|
+
(root / "npm" / "index.js").write_text("// 没有导出\n", encoding="utf-8")
|
|
108
|
+
assert any(i.code == "unreadable" for i in rc.check_versions(root))
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@pytest.mark.parametrize("tag", ["v0.1.0", "0.1.0"])
|
|
112
|
+
def test_tag_matching_version_passes(tag):
|
|
113
|
+
assert rc.check_tag(tag, "0.1.0") == []
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@pytest.mark.parametrize("tag", ["v0.2.0", "v0.1.0-rc1"])
|
|
117
|
+
def test_tag_drift_is_an_error(tag):
|
|
118
|
+
errs = rc.check_tag(tag, "0.1.0")
|
|
119
|
+
assert errs and errs[0].code == "tag_mismatch"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_empty_tag_is_skipped():
|
|
123
|
+
assert rc.check_tag("", "0.1.0") == []
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_repo_declares_readme():
|
|
127
|
+
"""真仓库必须声明正文:0.1.1 在 PyPI 上的项目页就是空白的,这就是回归点。"""
|
|
128
|
+
assert [i for i in rc.check_metadata(REPO) if i.level == "error"] == []
|
|
129
|
+
assert rc.readme_target(REPO).endswith(".md")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_missing_readme_is_error(tmp_path):
|
|
133
|
+
root = _mini_repo(tmp_path, "0.1.0", "0.1.0")
|
|
134
|
+
assert any(i.code == "no_readme" for i in rc.check_metadata(root))
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_short_readme_is_error(tmp_path):
|
|
138
|
+
root = _mini_repo(tmp_path, "0.1.0", "0.1.0")
|
|
139
|
+
(root / "pyproject.toml").write_text(
|
|
140
|
+
'[project]\nname = "foxclaw"\nversion = "0.1.0"\nreadme = "README.md"\n',
|
|
141
|
+
encoding="utf-8")
|
|
142
|
+
(root / "README.md").write_text("短\n", encoding="utf-8")
|
|
143
|
+
assert any(i.code == "readme_too_short" for i in rc.check_metadata(root))
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_readme_pointing_to_missing_file_is_error(tmp_path):
|
|
147
|
+
root = _mini_repo(tmp_path, "0.1.0", "0.1.0")
|
|
148
|
+
(root / "pyproject.toml").write_text(
|
|
149
|
+
'[project]\nname = "foxclaw"\nversion = "0.1.0"\nreadme = "NOPE.md"\n',
|
|
150
|
+
encoding="utf-8")
|
|
151
|
+
assert any(i.code == "readme_missing" for i in rc.check_metadata(root))
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@pytest.mark.parametrize("entry", [
|
|
155
|
+
"foxclaw/.env",
|
|
156
|
+
"foxclaw/.env.local",
|
|
157
|
+
"foxclaw-0.1.0/docs/internal/secret.md",
|
|
158
|
+
"foxclaw/foxclaw_data/sessions.db",
|
|
159
|
+
"foxclaw/old.zip",
|
|
160
|
+
"foxclaw/__pycache__/x.pyc",
|
|
161
|
+
"foxclaw/.venv/lib/x.py",
|
|
162
|
+
])
|
|
163
|
+
def test_forbidden_entries_detected(entry):
|
|
164
|
+
assert rc.forbidden_entry(entry), entry
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@pytest.mark.parametrize("entry", [
|
|
168
|
+
"foxclaw/.env.example",
|
|
169
|
+
"foxclaw/cli.py",
|
|
170
|
+
"foxclaw/vendor/hmr/hmr/__init__.py",
|
|
171
|
+
])
|
|
172
|
+
def test_allowed_entries_pass(entry):
|
|
173
|
+
assert rc.forbidden_entry(entry) is None
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def test_check_dist_passes_with_vendor_warning_only(tmp_path):
|
|
177
|
+
dist = tmp_path / "dist"
|
|
178
|
+
dist.mkdir()
|
|
179
|
+
_wheel(dist)
|
|
180
|
+
_sdist(dist)
|
|
181
|
+
issues = rc.check_dist(dist, "foxclaw", "0.1.0")
|
|
182
|
+
assert [i for i in issues if i.level == "error"] == []
|
|
183
|
+
assert any(i.code == "no_vendor" for i in issues), "缺 vendor 应显式提示"
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def test_check_dist_flags_missing_wheel(tmp_path):
|
|
187
|
+
dist = tmp_path / "dist"
|
|
188
|
+
dist.mkdir()
|
|
189
|
+
_sdist(dist)
|
|
190
|
+
assert any(i.code == "wheel_count" for i in rc.check_dist(dist, "foxclaw", "0.1.0"))
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def test_check_dist_flags_version_drift_in_filename(tmp_path):
|
|
194
|
+
dist = tmp_path / "dist"
|
|
195
|
+
dist.mkdir()
|
|
196
|
+
_wheel(dist, version="0.2.0")
|
|
197
|
+
_sdist(dist, version="0.1.0")
|
|
198
|
+
codes = {i.code for i in rc.check_dist(dist, "foxclaw", "0.1.0")}
|
|
199
|
+
assert "wheel_name" in codes or "meta_mismatch" in codes, codes
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def test_check_dist_flags_secret_inside_archive(tmp_path):
|
|
203
|
+
dist = tmp_path / "dist"
|
|
204
|
+
dist.mkdir()
|
|
205
|
+
_wheel(dist, extra=(".env",))
|
|
206
|
+
_sdist(dist)
|
|
207
|
+
msgs = [i.msg for i in rc.check_dist(dist, "foxclaw", "0.1.0")
|
|
208
|
+
if i.code == "forbidden_entry"]
|
|
209
|
+
assert msgs and any(".env" in m for m in msgs), msgs
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def test_check_dist_flags_empty_long_description(tmp_path):
|
|
213
|
+
dist = tmp_path / "dist"
|
|
214
|
+
dist.mkdir()
|
|
215
|
+
_wheel(dist, description=False)
|
|
216
|
+
_sdist(dist)
|
|
217
|
+
codes = {i.code for i in rc.check_dist(dist, "foxclaw", "0.1.0")}
|
|
218
|
+
assert "no_long_description" in codes, codes
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def test_check_dist_missing_dir_is_error(tmp_path):
|
|
222
|
+
assert any(i.code == "no_dist"
|
|
223
|
+
for i in rc.check_dist(tmp_path / "nope", "foxclaw", "0.1.0"))
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def test_npm_tgz_ok(tmp_path):
|
|
227
|
+
assert rc.check_npm_tgz(_tgz(tmp_path), "foxclaw", "0.1.0") == []
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def test_npm_tgz_name_drift_detected(tmp_path):
|
|
231
|
+
tgz = _tgz(tmp_path, pkg={"name": "foxclaw", "version": "0.2.0"})
|
|
232
|
+
codes = {i.code for i in rc.check_npm_tgz(tgz, "foxclaw", "0.1.0")}
|
|
233
|
+
assert "npm_meta" in codes, codes
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def test_npm_tgz_filename_drift_detected(tmp_path):
|
|
237
|
+
tgz = _tgz(tmp_path, version="0.2.0")
|
|
238
|
+
assert any(i.code == "npm_name" for i in rc.check_npm_tgz(tgz, "foxclaw", "0.1.0"))
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def test_run_end_to_end(tmp_path):
|
|
242
|
+
"""run() 串起来:真仓库 + 构造的 dist/tgz,无 tag 也应通过。"""
|
|
243
|
+
dist = tmp_path / "dist"
|
|
244
|
+
dist.mkdir()
|
|
245
|
+
issues = rc.run(REPO, tag="", dist=dist, npm_tgz=None)
|
|
246
|
+
assert any(i.level == "error" for i in issues), "空 dist 应报错"
|
|
247
|
+
version = rc.collect_versions(REPO)["pyproject.toml:project.version"]
|
|
248
|
+
_wheel(dist, version=version)
|
|
249
|
+
_sdist(dist, version=version)
|
|
250
|
+
tgz = _tgz(dist, version=version)
|
|
251
|
+
assert [i for i in rc.run(REPO, f"v{version}", dist, tgz) if i.level == "error"] == []
|
|
252
|
+
shutil.rmtree(dist)
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""断点续跑(P1):步骤级 checkpoint + 续跑复用已完成步骤。
|
|
2
|
+
|
|
3
|
+
锁住四件事:
|
|
4
|
+
① `RestartRecovery.record_step/steps_of` 能按 task 回放步骤,`compact_log` 不会把断点压实掉;
|
|
5
|
+
② 工具步指纹稳定(同名同参 = 同一步,参数字典序无关),失败/需确认的回注文本不入账;
|
|
6
|
+
③ `_run_tool_calls` 命中续跑缓存时**不再执行**该步,只执行未完成的步并各自记账;
|
|
7
|
+
④ 生命周期钩子下移基类后,任何装着 `_recovery` 的 Agent 都会落 start/finish(领域 Agent 不再漏记)。
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import types
|
|
13
|
+
|
|
14
|
+
import pytest
|
|
15
|
+
|
|
16
|
+
from foxclaw.agents.base import CognitiveAgent
|
|
17
|
+
from foxclaw.agents.mixins import _context, tool_exec
|
|
18
|
+
from foxclaw.core.recovery import RestartRecovery
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class _Rec:
|
|
22
|
+
"""鸭子类型替身:只记调用,不做 IO。"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, rows=None):
|
|
25
|
+
self.steps: list = []
|
|
26
|
+
self.started: list = []
|
|
27
|
+
self.finished: list = []
|
|
28
|
+
self._rows = rows or []
|
|
29
|
+
self.compacted = False
|
|
30
|
+
|
|
31
|
+
def record_step(self, tid, step):
|
|
32
|
+
self.steps.append((tid, step))
|
|
33
|
+
|
|
34
|
+
def steps_of(self, tid, limit=200):
|
|
35
|
+
return list(self._rows)
|
|
36
|
+
|
|
37
|
+
def task_started(self, **kw):
|
|
38
|
+
self.started.append(kw)
|
|
39
|
+
|
|
40
|
+
def task_finished(self, tid, status="done"):
|
|
41
|
+
self.finished.append((tid, status))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_record_and_replay_steps(tmp_path) -> None:
|
|
45
|
+
rec = RestartRecovery(str(tmp_path / "recovery.jsonl"), fsync=False)
|
|
46
|
+
rec.record_step("t1", {"kind": "stage", "name": "perceive"})
|
|
47
|
+
rec.record_step("t2", {"kind": "stage", "name": "recall"})
|
|
48
|
+
rec.record_step("t1", {"kind": "tool", "name": "web_search", "key": "k", "result": "r"})
|
|
49
|
+
steps = rec.steps_of("t1")
|
|
50
|
+
assert [s["name"] for s in steps] == ["perceive", "web_search"]
|
|
51
|
+
assert all("event" not in s and "task_id" not in s for s in steps)
|
|
52
|
+
assert rec.steps_of("nope") == []
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_compact_log_keeps_steps(tmp_path) -> None:
|
|
56
|
+
rec = RestartRecovery(str(tmp_path / "recovery.jsonl"), fsync=False)
|
|
57
|
+
rec.task_started("t1", "任务一")
|
|
58
|
+
rec.record_step("t1", {"kind": "tool", "name": "web_search", "key": "k", "result": "r"})
|
|
59
|
+
rec.task_finished("t1", status="done")
|
|
60
|
+
rec.compact_log(keep_recent_days=7)
|
|
61
|
+
assert len(rec.steps_of("t1")) == 1
|
|
62
|
+
rec.compact_log(keep_recent_days=0)
|
|
63
|
+
assert rec.steps_of("t1") == []
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_step_key_is_argument_order_insensitive() -> None:
|
|
67
|
+
a = tool_exec._step_key("web_search", {"q": "x", "n": 3})
|
|
68
|
+
b = tool_exec._step_key("web_search", {"n": 3, "q": "x"})
|
|
69
|
+
assert a == b
|
|
70
|
+
assert a != tool_exec._step_key("web_search", {"q": "y", "n": 3})
|
|
71
|
+
assert a != tool_exec._step_key("recall_memory", {"q": "x", "n": 3})
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@pytest.mark.parametrize("text,ok", [
|
|
75
|
+
("【联网搜索结果】...", True),
|
|
76
|
+
("【召回的相关记忆】...", True),
|
|
77
|
+
("(工具 web_search 未返回:45s 超时)", False),
|
|
78
|
+
("(该步需要你确认后才能执行)", False),
|
|
79
|
+
("(未知工具 foo)", False),
|
|
80
|
+
("", False),
|
|
81
|
+
])
|
|
82
|
+
def test_step_ok_rejects_failure_texts(text, ok) -> None:
|
|
83
|
+
assert tool_exec._step_ok(text) is ok
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_run_tool_calls_reuses_completed_steps(monkeypatch) -> None:
|
|
87
|
+
executed: list = []
|
|
88
|
+
|
|
89
|
+
def _fake_batch(agent, calls):
|
|
90
|
+
executed.extend(calls)
|
|
91
|
+
return [f"新结果:{c['name']}" for c in calls]
|
|
92
|
+
|
|
93
|
+
monkeypatch.setattr(tool_exec, "_run_tools_batch", _fake_batch)
|
|
94
|
+
rec = _Rec()
|
|
95
|
+
agent = types.SimpleNamespace(_recovery=rec, _session_id="t1")
|
|
96
|
+
done = {"name": "web_search", "args": {"q": "天气"}}
|
|
97
|
+
todo = {"name": "recall_memory", "args": {"q": "偏好"}}
|
|
98
|
+
cache = {tool_exec._step_key("web_search", {"q": "天气"}):
|
|
99
|
+
{"name": "web_search", "result": "【联网搜索结果】晴"}}
|
|
100
|
+
tok = _context._resume_steps.set(cache)
|
|
101
|
+
try:
|
|
102
|
+
out = tool_exec._run_tool_calls(agent, [done, todo])
|
|
103
|
+
finally:
|
|
104
|
+
_context._resume_steps.reset(tok)
|
|
105
|
+
|
|
106
|
+
assert executed == [todo], "已完成的步必须复用结果、绝不重复执行"
|
|
107
|
+
assert "断点续跑" in out[0] and "晴" in out[0]
|
|
108
|
+
assert out[1] == "新结果:recall_memory"
|
|
109
|
+
assert [s[1]["key"] for s in rec.steps] == [tool_exec._step_key("recall_memory", {"q": "偏好"})]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_run_tool_calls_skips_recording_failures(monkeypatch) -> None:
|
|
113
|
+
monkeypatch.setattr(tool_exec, "_run_tools_batch",
|
|
114
|
+
lambda agent, calls: ["(工具 web_search 未返回:45s 超时)"])
|
|
115
|
+
rec = _Rec()
|
|
116
|
+
agent = types.SimpleNamespace(_recovery=rec, _session_id="t1")
|
|
117
|
+
tool_exec._run_tool_calls(agent, [{"name": "web_search", "args": {}}])
|
|
118
|
+
assert rec.steps == [], "失败/超时步不能进 checkpoint(否则续跑会把失败当完成)"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_load_resume_cache_filters_bad_steps() -> None:
|
|
122
|
+
rows = [
|
|
123
|
+
{"kind": "stage", "name": "act"},
|
|
124
|
+
{"kind": "tool", "name": "web_search", "key": "k1", "result": "ok"},
|
|
125
|
+
{"kind": "tool", "name": "web_search", "key": "k2", "result": "x",
|
|
126
|
+
"truncated": True},
|
|
127
|
+
{"kind": "tool", "name": "web_search", "key": "k3", "result": " "},
|
|
128
|
+
{"kind": "tool", "name": "web_search", "result": "no-key"},
|
|
129
|
+
]
|
|
130
|
+
fake = types.SimpleNamespace(_recovery=_Rec(rows))
|
|
131
|
+
assert CognitiveAgent._load_resume_cache(fake, "t1") == {"k1": {"name": "web_search",
|
|
132
|
+
"result": "ok"}}
|
|
133
|
+
assert CognitiveAgent._load_resume_cache(fake, "") == {}
|
|
134
|
+
assert CognitiveAgent._load_resume_cache(types.SimpleNamespace(), "t1") == {}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_lifecycle_hooks_record_for_any_agent() -> None:
|
|
138
|
+
rec = _Rec()
|
|
139
|
+
agent = types.SimpleNamespace(_recovery=rec)
|
|
140
|
+
CognitiveAgent._on_session_started(agent, "t1", "跑个任务", {"channel": "cli"})
|
|
141
|
+
CognitiveAgent._on_task_settled(agent, "t1", "done")
|
|
142
|
+
assert rec.started and rec.started[0]["task_id"] == "t1"
|
|
143
|
+
assert rec.started[0]["channel"] == "cli"
|
|
144
|
+
assert rec.finished == [("t1", "done")]
|
|
145
|
+
|
|
146
|
+
CognitiveAgent._on_session_started(types.SimpleNamespace(), "t1", "x", {})
|
|
147
|
+
CognitiveAgent._on_task_settled(types.SimpleNamespace(), "t1", "done")
|
|
148
|
+
CognitiveAgent._record_stage(types.SimpleNamespace(), "t1", "recall")
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""任务感知模型路由 + fallback 链验证(全离线,monkeypatch 不联网)。"""
|
|
2
|
+
import os, sys
|
|
3
|
+
import pytest
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@pytest.mark.xfail(
|
|
9
|
+
reason="pre-existing 失败:本地 stub 无外部 API 时,fallback 链全模型降级后断言 '全部模型失败' 不一致;需真实 provider 配置(fast/smart 多档)才能过。",
|
|
10
|
+
strict=False,
|
|
11
|
+
)
|
|
12
|
+
def test_routing():
|
|
13
|
+
for k in ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "DEEPSEEK_API_KEY", "GOOGLE_API_KEY",
|
|
14
|
+
"DASHSCOPE_API_KEY", "FOXCLAW_MODEL", "FOXCLAW_MODEL_FAST", "FOXCLAW_MODEL_SMART",
|
|
15
|
+
"FOXCLAW_PROVIDER"]:
|
|
16
|
+
os.environ.pop(k, None)
|
|
17
|
+
|
|
18
|
+
from foxclaw.core.model_router import ModelRouter, _route_tier, _tier_model
|
|
19
|
+
|
|
20
|
+
print("=" * 60); print("任务感知模型路由 + fallback 链验证"); print("=" * 60)
|
|
21
|
+
|
|
22
|
+
assert _route_tier("supervisor", [{"role": "user", "content": "x"}]) == "fast"
|
|
23
|
+
assert _route_tier("finance", [{"role": "user", "content": "帮我做深度财务分析报告"}]) == "smart"
|
|
24
|
+
assert _route_tier("assistant", [{"role": "user", "content": "你好"}]) == "fast"
|
|
25
|
+
assert _tier_model("deepseek", "fast") == "deepseek-chat"
|
|
26
|
+
assert _tier_model("deepseek", "smart") == "deepseek-reasoner"
|
|
27
|
+
print("[1] 任务→档 + 档→模型 OK")
|
|
28
|
+
|
|
29
|
+
os.environ["FOXCLAW_PROVIDER"] = "deepseek"; os.environ["DEEPSEEK_API_KEY"] = "x"
|
|
30
|
+
r = ModelRouter()
|
|
31
|
+
assert r.provider == "deepseek"
|
|
32
|
+
assert r._model_for("deepseek", "supervisor", [{"role": "user", "content": "x"}]) == "deepseek-chat"
|
|
33
|
+
assert r._model_for("deepseek", "finance", [{"role": "user", "content": "深度分析报告方案评估"}]) == "deepseek-reasoner"
|
|
34
|
+
print("[2] 按任务/Agent 选档(supervisor→chat,复杂→reasoner) OK")
|
|
35
|
+
|
|
36
|
+
os.environ["OPENAI_API_KEY"] = "y"
|
|
37
|
+
r2 = ModelRouter(); calls = []
|
|
38
|
+
def _fake(p, c, m, s, msgs, mt):
|
|
39
|
+
calls.append(p)
|
|
40
|
+
if p == "deepseek":
|
|
41
|
+
raise RuntimeError("deepseek down")
|
|
42
|
+
return f"OK-{p}", None
|
|
43
|
+
r2._call_provider = _fake
|
|
44
|
+
out = r2.complete("sys", [{"role": "user", "content": "hi"}], agent_id="assistant")
|
|
45
|
+
assert "OK-openai" in out and calls == ["deepseek", "openai"], (out, calls)
|
|
46
|
+
assert "回退到 openai" in r2.fallback_reason
|
|
47
|
+
print("[3] fallback 链(deepseek 失败→openai) OK")
|
|
48
|
+
|
|
49
|
+
r3 = ModelRouter(provider="deepseek", model="fixed-x")
|
|
50
|
+
assert r3._model_for("deepseek", "supervisor", [{"role": "user", "content": "x"}]) == "fixed-x"
|
|
51
|
+
print("[4] 锁定模型不分档(向后兼容) OK")
|
|
52
|
+
|
|
53
|
+
def _allfail(*a):
|
|
54
|
+
raise RuntimeError("boom")
|
|
55
|
+
r2._call_provider = _allfail
|
|
56
|
+
out = r2.complete("sys", [{"role": "user", "content": "hi"}])
|
|
57
|
+
assert "STUB" in out or "全部模型失败" in out, out
|
|
58
|
+
assert "已完成处理" not in out, f"故障被伪装成成功: {out}"
|
|
59
|
+
assert r2.is_degraded() is True, "全部失败必须置 degraded 标志(上游据此禁写长期记忆)"
|
|
60
|
+
print("[5] 全部失败 → 显式失败(无假成功文案) + degraded OK")
|
|
61
|
+
|
|
62
|
+
r4 = ModelRouter()
|
|
63
|
+
r4.switch("deepseek", "deepseek-reasoner")
|
|
64
|
+
assert r4._model_for("deepseek", "supervisor", [{"role": "user", "content": "简单"}]) == "deepseek-reasoner"
|
|
65
|
+
r4.switch("deepseek")
|
|
66
|
+
assert r4._model_for("deepseek", "supervisor", [{"role": "user", "content": "简单"}]) == "deepseek-chat"
|
|
67
|
+
print("[6] switch 尊重显式模型 + 清锁恢复分档 OK")
|
|
68
|
+
|
|
69
|
+
r5 = ModelRouter(); seq = []
|
|
70
|
+
def _fk(p, c, m, s, msgs, mt):
|
|
71
|
+
seq.append(p)
|
|
72
|
+
if p == "deepseek":
|
|
73
|
+
raise RuntimeError("down")
|
|
74
|
+
return f"OK-{p}", None
|
|
75
|
+
r5._call_provider = _fk
|
|
76
|
+
r5.complete("s", [{"role": "user", "content": "hi"}]); seq.clear()
|
|
77
|
+
r5.complete("s", [{"role": "user", "content": "hi"}])
|
|
78
|
+
assert seq == ["openai"], f"断路器失效: {seq}"
|
|
79
|
+
print("[7] 断路器跳过宕机 provider OK")
|
|
80
|
+
|
|
81
|
+
print("\n任务感知模型路由验证全部通过")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
if __name__ == "__main__":
|
|
85
|
+
test_routing()
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""
|
|
2
|
+
微内核调度器(v2 ① Step1:Scheduler v0)测试 —— 全离线,线程级。
|
|
3
|
+
|
|
4
|
+
覆盖:
|
|
5
|
+
1. 无上限(默认):准入直通,行为不变;并发不受限(Barrier 确定性达峰值 N)
|
|
6
|
+
2. 有上限:并发配额生效(peak_running 不超过 max_concurrency = 背压)
|
|
7
|
+
3. 生命周期事件:每任务 emit task.lifecycle.{scheduled,running,completed};失败 → failed
|
|
8
|
+
(【P5-2】回退路径原用 started,与 ProcessManager 的 running 不一致,已统一为 running)
|
|
9
|
+
4. 记账 + 结果透传 + 异常透传
|
|
10
|
+
"""
|
|
11
|
+
import sys, threading, time
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
14
|
+
|
|
15
|
+
from foxclaw.kernel import Scheduler
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class _CapSpine:
|
|
19
|
+
def __init__(self): self.events = []; self._lk = threading.Lock()
|
|
20
|
+
def publish(self, topic, payload=None, source="", **kw):
|
|
21
|
+
with self._lk: self.events.append(topic)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _run_n_concurrent(sched, n, work):
|
|
25
|
+
threads = [threading.Thread(target=lambda: sched.run(work, task="t")) for _ in range(n)]
|
|
26
|
+
for t in threads: t.start()
|
|
27
|
+
for t in threads: t.join()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_unlimited_reaches_peak():
|
|
31
|
+
print("\n【1】无上限(默认):并发不受限,行为直通")
|
|
32
|
+
sched = Scheduler()
|
|
33
|
+
N = 6
|
|
34
|
+
barrier = threading.Barrier(N)
|
|
35
|
+
def work():
|
|
36
|
+
barrier.wait(timeout=5)
|
|
37
|
+
time.sleep(0.01)
|
|
38
|
+
return "ok"
|
|
39
|
+
_run_n_concurrent(sched, N, work)
|
|
40
|
+
st = sched.stats()
|
|
41
|
+
assert st["max_concurrency"] is None
|
|
42
|
+
assert st["peak_running"] == N, f"无上限应达峰值 {N},实得 {st['peak_running']}"
|
|
43
|
+
assert st["completed"] == N and st["submitted"] == N
|
|
44
|
+
print(f" ✓ 无上限:peak={st['peak_running']}=N,completed={st['completed']}")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_concurrency_cap():
|
|
48
|
+
print("\n【2】有上限:并发配额生效(背压)")
|
|
49
|
+
sched = Scheduler(max_concurrency=2)
|
|
50
|
+
def work():
|
|
51
|
+
time.sleep(0.02)
|
|
52
|
+
return "ok"
|
|
53
|
+
_run_n_concurrent(sched, 8, work)
|
|
54
|
+
st = sched.stats()
|
|
55
|
+
assert st["peak_running"] <= 2, f"并发峰值不得超上限 2,实得 {st['peak_running']}"
|
|
56
|
+
assert st["peak_running"] == 2, "8 任务/上限 2 应打满到 2"
|
|
57
|
+
assert st["completed"] == 8
|
|
58
|
+
print(f" ✓ 上限 2:peak={st['peak_running']}(不超上限),completed={st['completed']}")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_lifecycle_events_and_accounting():
|
|
62
|
+
print("\n【3】生命周期事件 + 记账 + 失败传播")
|
|
63
|
+
spine = _CapSpine()
|
|
64
|
+
sched = Scheduler(spine=spine)
|
|
65
|
+
r = sched.run(lambda: 42, agent_id="finance", task="算账")
|
|
66
|
+
assert r == 42
|
|
67
|
+
assert spine.events == ["task.lifecycle.scheduled", "task.lifecycle.running",
|
|
68
|
+
"task.lifecycle.completed"], spine.events
|
|
69
|
+
spine.events.clear()
|
|
70
|
+
raised = False
|
|
71
|
+
try:
|
|
72
|
+
sched.run(lambda: (_ for _ in ()).throw(RuntimeError("boom")), task="崩")
|
|
73
|
+
except RuntimeError:
|
|
74
|
+
raised = True
|
|
75
|
+
assert raised, "异常应透传"
|
|
76
|
+
assert spine.events == ["task.lifecycle.scheduled", "task.lifecycle.running",
|
|
77
|
+
"task.lifecycle.failed"], spine.events
|
|
78
|
+
st = sched.stats()
|
|
79
|
+
assert st["submitted"] == 2 and st["completed"] == 1 and st["failed"] == 1
|
|
80
|
+
assert st["running"] == 0, "跑完 running 归零"
|
|
81
|
+
print(f" ✓ 成功→completed事件;失败→failed事件+异常透传;记账 {st}")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_pm_delegation_unified_lifecycle():
|
|
85
|
+
print("\n【4】Step3:有 ProcessManager → 委托它发唯一 task.lifecycle.<state>(不双套词汇)")
|
|
86
|
+
from foxclaw.kernel import EventSpine, ProcessManager
|
|
87
|
+
from foxclaw.core.system_bus import SystemBus
|
|
88
|
+
spine = EventSpine()
|
|
89
|
+
seen = []
|
|
90
|
+
spine.project(lambda e: seen.append(e.topic))
|
|
91
|
+
pm = ProcessManager(bus=SystemBus(spine=spine))
|
|
92
|
+
sched = Scheduler(spine=spine, process_manager=pm)
|
|
93
|
+
r = sched.run(lambda: 7, agent_id="finance", task="t")
|
|
94
|
+
assert r == 7
|
|
95
|
+
life = [t for t in seen if t.startswith("task.lifecycle")]
|
|
96
|
+
assert life == ["task.lifecycle.scheduled", "task.lifecycle.running",
|
|
97
|
+
"task.lifecycle.completed"], f"应由 pm 发唯一词汇,实得 {life}"
|
|
98
|
+
assert pm.stats()["by_state"].get("completed") == 1
|
|
99
|
+
assert sched.stats()["completed"] == 1
|
|
100
|
+
print(" ✓ pm 发 task.lifecycle.{scheduled,running,completed};顶层任务纳入进程;记账双在")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_priority_admission():
|
|
104
|
+
print("\n【5】Step5:满载后释放 → 高优先排队者先获空位(不抢占运行中)")
|
|
105
|
+
sched = Scheduler(max_concurrency=1)
|
|
106
|
+
order = []
|
|
107
|
+
running = threading.Event(); release = threading.Event()
|
|
108
|
+
def blocker():
|
|
109
|
+
sched.run(lambda: (running.set(), release.wait(5))[-1], priority=0, task="blocker")
|
|
110
|
+
tb = threading.Thread(target=blocker); tb.start()
|
|
111
|
+
assert running.wait(5), "blocker 应占住唯一槽"
|
|
112
|
+
def worker(p, name):
|
|
113
|
+
sched.run(lambda: order.append(name), priority=p, task=name)
|
|
114
|
+
tlo = threading.Thread(target=worker, args=(0, "lo"))
|
|
115
|
+
thi = threading.Thread(target=worker, args=(10, "hi"))
|
|
116
|
+
tlo.start(); time.sleep(0.05); thi.start(); time.sleep(0.15)
|
|
117
|
+
release.set()
|
|
118
|
+
for t in (tb, tlo, thi): t.join(5)
|
|
119
|
+
assert order == ["hi", "lo"], f"高优先应先获空位,实得 {order}"
|
|
120
|
+
print(f" ✓ 释放后:高优先先获空位 → {order}(准入优先级,非抢占运行中)")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def main():
|
|
124
|
+
print("=" * 60)
|
|
125
|
+
print("微内核调度器 · Scheduler(v2 ① Step1/3/5)")
|
|
126
|
+
print("=" * 60)
|
|
127
|
+
test_unlimited_reaches_peak()
|
|
128
|
+
test_concurrency_cap()
|
|
129
|
+
test_lifecycle_events_and_accounting()
|
|
130
|
+
test_pm_delegation_unified_lifecycle()
|
|
131
|
+
test_priority_admission()
|
|
132
|
+
print("\n" + "=" * 60)
|
|
133
|
+
print("✓ Step1 验证完成:准入配额(背压)+ 生命周期事件 + 记账 + 透传")
|
|
134
|
+
print("=" * 60)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
if __name__ == "__main__":
|
|
138
|
+
main()
|