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,794 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CognitiveAgent — 认知增强 Agent 基类
|
|
3
|
+
内建 HMR + ThoughtChain + Harness,不需要外部配置
|
|
4
|
+
每个 Agent 实例拥有:
|
|
5
|
+
- 独立的 HMR Workspace(记忆隔离)
|
|
6
|
+
- 独立的 ThoughtChain(推理轨迹)
|
|
7
|
+
- 独立的 Harness 实例(策略边界)
|
|
8
|
+
- 共享的 Anthropic LLM 接口
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import threading
|
|
12
|
+
import uuid
|
|
13
|
+
from typing import TYPE_CHECKING, Any
|
|
14
|
+
|
|
15
|
+
from ..core.thought_chain import ThoughtChain, ThoughtNode
|
|
16
|
+
from ..harness.harness import Harness, HarnessResult
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from ..core.recovery import RestartRecovery
|
|
20
|
+
from ..core.system_bus import SystemBus
|
|
21
|
+
from ..kernel import ProcessManager
|
|
22
|
+
from ..skills.lifecycle import HookManager
|
|
23
|
+
from ..skills.markdown_skill import MarkdownSkillRegistry
|
|
24
|
+
|
|
25
|
+
_THINK_LOCK = threading.RLock()
|
|
26
|
+
|
|
27
|
+
_GATE_UNSET = object()
|
|
28
|
+
|
|
29
|
+
from .mixins._context import (
|
|
30
|
+
_agent_ctx_get,
|
|
31
|
+
_agent_ctx_set,
|
|
32
|
+
_taint_get,
|
|
33
|
+
_taint_raise,
|
|
34
|
+
_taint_reset,
|
|
35
|
+
_untrusted_set,
|
|
36
|
+
_evidence_reset,
|
|
37
|
+
_evidence_take,
|
|
38
|
+
_suppress_memory,
|
|
39
|
+
_act_stream,
|
|
40
|
+
_reasoning_inject,
|
|
41
|
+
_resume_steps,
|
|
42
|
+
HarnessBlockedError,
|
|
43
|
+
)
|
|
44
|
+
from .mixins.augment import AugmentMixin
|
|
45
|
+
from .mixins.llm import LlmMixin
|
|
46
|
+
from .mixins.memory import MemoryMixin
|
|
47
|
+
from .mixins.skillbox import SkillBoxMixin
|
|
48
|
+
from .mixins.preference import PreferenceMixin
|
|
49
|
+
from .mixins.prompt import PromptMixin, KERNEL_PROMPT
|
|
50
|
+
from .mixins.tool_exec import ToolExecMixin
|
|
51
|
+
|
|
52
|
+
__all__ = [
|
|
53
|
+
"CognitiveAgent", "HarnessBlockedError", "KERNEL_PROMPT",
|
|
54
|
+
"_suppress_memory", "_act_stream", "_taint_get", "_taint_raise", "_taint_reset",
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _plan_payload(ctx) -> dict | None:
|
|
59
|
+
"""从管线 ctx 提取计划负载:{steps, flow, multi_step};没有计划 → None。
|
|
60
|
+
|
|
61
|
+
领域管线把计划写 `ctx.plan`(pipeline.stage_plan),主管管线写 `ctx.plan_obj`
|
|
62
|
+
(pipeline.supervisor_stages.sv_plan),两种形态都兼容 → 两条链路都能回显计划。
|
|
63
|
+
纯只读、不抛异常(坏了就当没有计划,不拖垮任务)。
|
|
64
|
+
"""
|
|
65
|
+
for attr in ("plan", "plan_obj"):
|
|
66
|
+
p = getattr(ctx, attr, None)
|
|
67
|
+
if p is None:
|
|
68
|
+
continue
|
|
69
|
+
try:
|
|
70
|
+
steps = p.get("steps") if isinstance(p, dict) else getattr(p, "steps", None)
|
|
71
|
+
if not steps:
|
|
72
|
+
continue
|
|
73
|
+
steps = [str(s) for s in steps]
|
|
74
|
+
flow = (p.get("flow", "single") if isinstance(p, dict)
|
|
75
|
+
else getattr(p, "flow", "single")) or "single"
|
|
76
|
+
multi = (p.get("multi_step") if isinstance(p, dict)
|
|
77
|
+
else getattr(p, "multi_step", False))
|
|
78
|
+
return {"steps": steps, "flow": flow,
|
|
79
|
+
"multi_step": bool(multi) or len(steps) > 1}
|
|
80
|
+
except Exception:
|
|
81
|
+
continue
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class CognitiveAgent(AugmentMixin, LlmMixin, MemoryMixin,
|
|
86
|
+
PreferenceMixin, PromptMixin, ToolExecMixin, SkillBoxMixin):
|
|
87
|
+
"""
|
|
88
|
+
所有 Foxclaw Agent 的基类
|
|
89
|
+
子类只需实现:_role_prompt、allowed_tools、run_task
|
|
90
|
+
(system_prompt 由基类统一 = KERNEL_PROMPT 内核前缀 + 子类 _role_prompt 角色职责)
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
AGENT_ID: str = "base"
|
|
94
|
+
AGENT_NAME: str = "Base Agent"
|
|
95
|
+
ALLOWED_TOOL_CATEGORIES: set[str] = {"read"}
|
|
96
|
+
|
|
97
|
+
md_skills: "MarkdownSkillRegistry"
|
|
98
|
+
hooks: "HookManager"
|
|
99
|
+
_system_bus: "SystemBus"
|
|
100
|
+
_process_manager: "ProcessManager"
|
|
101
|
+
_recovery: "RestartRecovery"
|
|
102
|
+
|
|
103
|
+
def __init__(
|
|
104
|
+
self,
|
|
105
|
+
hmr,
|
|
106
|
+
thought_chain: ThoughtChain,
|
|
107
|
+
client=None,
|
|
108
|
+
data_dir: str = "./foxclaw_data",
|
|
109
|
+
shadow_rounds: int = 20,
|
|
110
|
+
router=None,
|
|
111
|
+
spine=None,
|
|
112
|
+
caps=None,
|
|
113
|
+
):
|
|
114
|
+
self.hmr = hmr
|
|
115
|
+
self.thought_chain = thought_chain
|
|
116
|
+
self.client = client
|
|
117
|
+
if router is None:
|
|
118
|
+
from ..core.model_router import ModelRouter
|
|
119
|
+
router = ModelRouter()
|
|
120
|
+
self.router = router
|
|
121
|
+
self.data_dir = data_dir
|
|
122
|
+
|
|
123
|
+
self.harness = Harness(
|
|
124
|
+
agent_id=self.AGENT_ID,
|
|
125
|
+
thought_chain=thought_chain,
|
|
126
|
+
policy_path=f"{data_dir}/policy_{self.AGENT_ID}.yaml",
|
|
127
|
+
shadow_rounds=shadow_rounds,
|
|
128
|
+
spine=spine,
|
|
129
|
+
)
|
|
130
|
+
self.harness.block_on_semantic = not getattr(self, "IS_ROUTER", False)
|
|
131
|
+
|
|
132
|
+
self.workspace = hmr.get_workspace(self.AGENT_ID)
|
|
133
|
+
|
|
134
|
+
self._ctx_token = uuid.uuid4().hex
|
|
135
|
+
|
|
136
|
+
self._caps = caps
|
|
137
|
+
|
|
138
|
+
def search_internal(self, query: str, top_k: int = 5):
|
|
139
|
+
"""Agent 反向调用内部搜索(HMR记忆 + HKC知识 + Agent态势),只查内部、不联网。
|
|
140
|
+
这是「串连 Agent」双向连通的 Agent→搜索 方向。经注册中枢惰性解析 search_runtime。"""
|
|
141
|
+
caps = getattr(self, "_caps", None)
|
|
142
|
+
rt = caps.get("search_runtime") if caps is not None else None
|
|
143
|
+
if rt is None:
|
|
144
|
+
return None
|
|
145
|
+
return rt.search(query, agent_id=self.AGENT_ID, internal_only=True)
|
|
146
|
+
|
|
147
|
+
def available_abilities(self, keys: list | None = None) -> list:
|
|
148
|
+
"""【HKC 第2档 Step2·能力自省】证据驱动地查 HKC 的 ACE 能力:哪些的知识覆盖已达标
|
|
149
|
+
(可编译 sufficient=True)、哪些还差(sufficient=False + missing_skills)。
|
|
150
|
+
|
|
151
|
+
**只读、informational——不改任何执行**。HKC 未启用/未 vendor → []。coverage 是确定性
|
|
152
|
+
(无 LLM),但对每个能力算一遍图覆盖,故**按需调用**(非热路径)。注:HKC 知识是全局共享
|
|
153
|
+
的(非按 Agent 分区),故各 Agent 看到的能力覆盖一致——这是"当前知识能支撑哪些能力"的
|
|
154
|
+
系统级自省,挂在 Agent 上是"我能取用哪些能力"的语义。"""
|
|
155
|
+
caps = getattr(self, "_caps", None)
|
|
156
|
+
hkc = caps.get("hkc") if caps is not None else None
|
|
157
|
+
if hkc is None or not getattr(hkc, "available", False):
|
|
158
|
+
return []
|
|
159
|
+
client = hkc.client
|
|
160
|
+
try:
|
|
161
|
+
all_keys = list(keys) if keys else list(client.list_abilities())
|
|
162
|
+
except Exception:
|
|
163
|
+
return []
|
|
164
|
+
out = []
|
|
165
|
+
for key in all_keys:
|
|
166
|
+
try:
|
|
167
|
+
ab = client.compile_ability(key)
|
|
168
|
+
out.append({"ability_key": ab.ability_key, "display_name": ab.display_name,
|
|
169
|
+
"domain": ab.domain, "sufficient": True, "coverage": ab.coverage})
|
|
170
|
+
except Exception as e:
|
|
171
|
+
if hasattr(e, "missing_skills"):
|
|
172
|
+
out.append({"ability_key": key, "sufficient": False,
|
|
173
|
+
"coverage": getattr(e, "coverage", {}),
|
|
174
|
+
"missing_skills": getattr(e, "missing_skills", [])})
|
|
175
|
+
return out
|
|
176
|
+
|
|
177
|
+
@property
|
|
178
|
+
def _session_id(self):
|
|
179
|
+
return _agent_ctx_get(self._ctx_token).get("session_id")
|
|
180
|
+
|
|
181
|
+
@_session_id.setter
|
|
182
|
+
def _session_id(self, value):
|
|
183
|
+
_agent_ctx_set(self._ctx_token, session_id=value)
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def _parent_node_id(self):
|
|
187
|
+
return _agent_ctx_get(self._ctx_token).get("parent_node_id")
|
|
188
|
+
|
|
189
|
+
@_parent_node_id.setter
|
|
190
|
+
def _parent_node_id(self, value):
|
|
191
|
+
_agent_ctx_set(self._ctx_token, parent_node_id=value)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def start_session(self, goal: str, entities: list[str] | None = None) -> str:
|
|
195
|
+
"""开始一个新任务会话,注册目标基线"""
|
|
196
|
+
self._session_id = f"sess_{uuid.uuid4().hex[:10]}"
|
|
197
|
+
self._parent_node_id = None
|
|
198
|
+
_taint_reset(self)
|
|
199
|
+
_evidence_reset(self)
|
|
200
|
+
|
|
201
|
+
self.harness.register_goal(
|
|
202
|
+
goal_text=goal,
|
|
203
|
+
allowed_tool_categories=self.ALLOWED_TOOL_CATEGORIES,
|
|
204
|
+
baseline_entities=set(entities or []),
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
goal_node = ThoughtNode.create(
|
|
208
|
+
session_id=self._session_id,
|
|
209
|
+
agent_id=self.AGENT_ID,
|
|
210
|
+
type="goal",
|
|
211
|
+
content=goal,
|
|
212
|
+
conf=1.0,
|
|
213
|
+
entities=entities or [],
|
|
214
|
+
)
|
|
215
|
+
self.thought_chain.add_node(goal_node)
|
|
216
|
+
self.thought_chain.update_harness(goal_node.id, "BASELINE")
|
|
217
|
+
self._parent_node_id = goal_node.id
|
|
218
|
+
|
|
219
|
+
return self._session_id
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _reasoning_result(self, node):
|
|
223
|
+
"""经统一 PolicyGate 做推理准入,返回 HarnessResult(与直接 harness.check 逐字一致)。
|
|
224
|
+
gate 经能力中枢惰性解析并缓存;无 gate(standalone/治理场景)则直接 harness.check。"""
|
|
225
|
+
gate: Any = getattr(self, "_gate_cache", _GATE_UNSET)
|
|
226
|
+
if gate is _GATE_UNSET:
|
|
227
|
+
caps = getattr(self, "_caps", None)
|
|
228
|
+
gate = caps.get("policy_gate") if caps is not None else None
|
|
229
|
+
self._gate_cache = gate
|
|
230
|
+
if gate is not None:
|
|
231
|
+
return gate.check_reasoning(self.harness, node).raw
|
|
232
|
+
return self.harness.check(node)
|
|
233
|
+
|
|
234
|
+
def _think(
|
|
235
|
+
self,
|
|
236
|
+
content: str,
|
|
237
|
+
node_type="reasoning",
|
|
238
|
+
conf: float = 0.9,
|
|
239
|
+
mem_refs: list[str] | None = None,
|
|
240
|
+
tool_name: str | None = None,
|
|
241
|
+
entities: list[str] | None = None,
|
|
242
|
+
metadata: dict | None = None,
|
|
243
|
+
) -> tuple[ThoughtNode, HarnessResult]:
|
|
244
|
+
"""
|
|
245
|
+
记录一个推理步骤,经过 Harness 校验
|
|
246
|
+
如果 BLOCK,抛出 HarnessBlockedError
|
|
247
|
+
"""
|
|
248
|
+
node = ThoughtNode.create(
|
|
249
|
+
session_id=self._session_id or "no_session",
|
|
250
|
+
agent_id=self.AGENT_ID,
|
|
251
|
+
type=node_type,
|
|
252
|
+
content=content,
|
|
253
|
+
conf=conf,
|
|
254
|
+
mem_refs=mem_refs or [],
|
|
255
|
+
parent_ids=[self._parent_node_id] if self._parent_node_id else [],
|
|
256
|
+
tool_name=tool_name,
|
|
257
|
+
entities=entities or [],
|
|
258
|
+
metadata=metadata,
|
|
259
|
+
)
|
|
260
|
+
with _THINK_LOCK:
|
|
261
|
+
self.thought_chain.add_node(node)
|
|
262
|
+
|
|
263
|
+
result = self._reasoning_result(node)
|
|
264
|
+
|
|
265
|
+
if result.is_blocked:
|
|
266
|
+
raise HarnessBlockedError(result, node)
|
|
267
|
+
|
|
268
|
+
if result.is_warned:
|
|
269
|
+
node.conf = max(node.conf - 0.15, 0.1)
|
|
270
|
+
|
|
271
|
+
with _THINK_LOCK:
|
|
272
|
+
self._parent_node_id = node.id
|
|
273
|
+
return node, result
|
|
274
|
+
|
|
275
|
+
@staticmethod
|
|
276
|
+
def _sanitize_history(history, max_turns: int = 12):
|
|
277
|
+
"""清洗前端传来的多轮历史 → 合法 messages:只留 {role∈user/assistant, content:非空str},
|
|
278
|
+
截到最近 max_turns 条。防注入非法角色/超长上下文。"""
|
|
279
|
+
out = []
|
|
280
|
+
for m in (history or [])[-max_turns:]:
|
|
281
|
+
if not isinstance(m, dict):
|
|
282
|
+
continue
|
|
283
|
+
role, content = m.get("role"), m.get("content")
|
|
284
|
+
if role in ("user", "assistant") and isinstance(content, str) and content.strip():
|
|
285
|
+
out.append({"role": role, "content": content})
|
|
286
|
+
return out
|
|
287
|
+
|
|
288
|
+
def _compact_context(self, messages: list, stage: str = "") -> list:
|
|
289
|
+
"""LLM 调用前的上下文预算闸:超阈值则摘要/截断,任何失败都原样返回。
|
|
290
|
+
|
|
291
|
+
历史:`core/compaction.py` 的 Compactor 早已实现(含 tool_calls 原子组切分、
|
|
292
|
+
LLM 摘要→部分摘要→截断三级降级),但**没有任何生产调用点**,实际生效的
|
|
293
|
+
只有 `_sanitize_history` 的「按条数砍到 12 条」——长对话、多工具轮会
|
|
294
|
+
直接把 provider 上下文撑爆。这里补上真实接线。
|
|
295
|
+
|
|
296
|
+
预算由 FOXCLAW_CONTEXT_MAX_TOKENS 控制(默认 6000);置 0 可整体关闭。
|
|
297
|
+
"""
|
|
298
|
+
import os as _os
|
|
299
|
+
try:
|
|
300
|
+
limit = int(_os.environ.get("FOXCLAW_CONTEXT_MAX_TOKENS", "6000") or 6000)
|
|
301
|
+
except Exception:
|
|
302
|
+
limit = 6000
|
|
303
|
+
if limit <= 0:
|
|
304
|
+
return messages
|
|
305
|
+
try:
|
|
306
|
+
from ..core.compaction import Compactor
|
|
307
|
+
compactor = Compactor(router=getattr(self, "router", None), max_tokens=limit)
|
|
308
|
+
compacted, res = compactor.maybe_compact(messages)
|
|
309
|
+
if res is not None:
|
|
310
|
+
try:
|
|
311
|
+
self._think(
|
|
312
|
+
content=(f"上下文压缩[{stage or 'llm'}] {res.method}: "
|
|
313
|
+
f"{res.original_count}→{res.compacted_count} 条, "
|
|
314
|
+
f"{res.original_tokens}→{res.compacted_tokens} token"
|
|
315
|
+
+ (f"; {res.note}" if res.note else "")),
|
|
316
|
+
node_type="reflection", conf=0.7)
|
|
317
|
+
except Exception:
|
|
318
|
+
pass
|
|
319
|
+
return compacted
|
|
320
|
+
except Exception:
|
|
321
|
+
return messages
|
|
322
|
+
|
|
323
|
+
@staticmethod
|
|
324
|
+
def _attachments_context(attachments) -> str:
|
|
325
|
+
"""把聊天附件拼成一段给 LLM 的上下文:文本类贴正文、其它降级为「文件名 + 元数据」提示。
|
|
326
|
+
|
|
327
|
+
附件正文已在路由层 _sanitize_attachments 清洗(限长/限量);这里再按**不可信数据**隔离:
|
|
328
|
+
附件可能是别人转发的文件,与用户当面对话不是同一信任级别,而手写中文定界符可被正文
|
|
329
|
+
伪造。统一走 wrap_untrusted(随机 nonce 边界 + 破界 defang + 注入特征告警)。"""
|
|
330
|
+
if not attachments:
|
|
331
|
+
return ""
|
|
332
|
+
from ..core.injection_guard import wrap_untrusted
|
|
333
|
+
parts = []
|
|
334
|
+
for a in attachments:
|
|
335
|
+
if not isinstance(a, dict):
|
|
336
|
+
continue
|
|
337
|
+
name = a.get("name") or "文件"
|
|
338
|
+
text = (a.get("text") or "").strip()
|
|
339
|
+
if text:
|
|
340
|
+
block = wrap_untrusted(text, source="attachment")
|
|
341
|
+
parts.append(f"【附件「{name}」内容如下】\n{block}\n【附件「{name}」结束】")
|
|
342
|
+
else:
|
|
343
|
+
mime = a.get("mime") or "未知类型"
|
|
344
|
+
size = a.get("size") or 0
|
|
345
|
+
parts.append(f"【附件「{name}」({mime}, {size} 字节)】用户上传了此文件,"
|
|
346
|
+
f"但无法提取为可读文本;若需要其内容,请说明并提示用户改用文本/PDF 等可读格式。")
|
|
347
|
+
return "\n\n" + "\n\n".join(parts) if parts else ""
|
|
348
|
+
|
|
349
|
+
def stream_reply(self, task: str, stop_event=None, images: list | None = None, history: list | None = None,
|
|
350
|
+
attachments: list | None = None, on_thinking=None, on_evidence=None):
|
|
351
|
+
"""流式直答路径(方案A):对话/问答/分析,不做多步编排(那走 run_task)。同步生成器,
|
|
352
|
+
由端点在线程池迭代成 SSE。stop_event 置位(客户端断连)则提前终止,省 token。
|
|
353
|
+
images(可选)= 多模态图片,随问题发给视觉模型(模型无关,非视觉模型 router 层降级)。
|
|
354
|
+
history(可选)= 前端带来的最近几轮对话 → 拼在当前问题前,让多轮连贯。
|
|
355
|
+
attachments(可选)= 聊天附件 [{name,size,mime,url,text}]:text 为提取正文,拼进问题末尾
|
|
356
|
+
供 LLM 阅读;text 为空的(二进制/图片)降级为「文件名 + 元数据」提示。
|
|
357
|
+
on_thinking(可选)= 推理增量回调。模型的原生 reasoning 与正文内嵌 <think> 块都经此
|
|
358
|
+
出去,由端点作为独立 SSE `thinking` 事件送前端折叠显示;正文 (yield) 仍走净化。
|
|
359
|
+
on_evidence(可选)= 执行证据回调。工具轮的每步核验结论(verified/unverified/failed)
|
|
360
|
+
+ 可核对凭据(来源链接、真实存在的文件)在本轮结束时一次性送出,由端点作为
|
|
361
|
+
SSE `evidence` 事件给前端展示 —— 「做成了」这句话从此可以点开看凭什么。
|
|
362
|
+
|
|
363
|
+
两条路径(单一决策源之争,见 TOOL_USE_PLAN.md):
|
|
364
|
+
① FOXCLAW_TOOL_USE=1 且 provider 支持 tools 且无图 → **LLM 自主工具循环**(_reply_with_tools),
|
|
365
|
+
最终答案 chunk 输出;联网/召回由模型自己决策,不再靠关键词表。
|
|
366
|
+
② 否则(默认 / 不支持 tools / 带图)→ **现有关键词增强链 + 真流式**(fallback,行为不变)。"""
|
|
367
|
+
from ..core import tools as _tools
|
|
368
|
+
from ..core import injection_guard as _ig
|
|
369
|
+
from ..core.model_router import _chunk_text
|
|
370
|
+
task = str(task or "")
|
|
371
|
+
_evidence_reset(self)
|
|
372
|
+
if attachments:
|
|
373
|
+
ctx = self._attachments_context(attachments)
|
|
374
|
+
if ctx:
|
|
375
|
+
task = task + ctx
|
|
376
|
+
hist = self._sanitize_history(history)
|
|
377
|
+
messages = hist + [{"role": "user", "content": task}]
|
|
378
|
+
parts = []
|
|
379
|
+
used_tools = False
|
|
380
|
+
guarded_out = False
|
|
381
|
+
|
|
382
|
+
def _emit_thinking(delta: str) -> None:
|
|
383
|
+
"""推理内容的最后一道关:思考也不许带密钥/系统提示原文(与正文同等级净化)。"""
|
|
384
|
+
if not delta or on_thinking is None:
|
|
385
|
+
return
|
|
386
|
+
try:
|
|
387
|
+
if _ig.scan_output(delta):
|
|
388
|
+
on_thinking("(推理内容已被安全护栏过滤)")
|
|
389
|
+
return
|
|
390
|
+
delta = _ig.redact_secrets(delta)
|
|
391
|
+
except Exception:
|
|
392
|
+
pass
|
|
393
|
+
try:
|
|
394
|
+
on_thinking(delta)
|
|
395
|
+
except Exception:
|
|
396
|
+
pass
|
|
397
|
+
|
|
398
|
+
if _tools.tool_use_enabled() and self.router.supports_tools() and not images:
|
|
399
|
+
try:
|
|
400
|
+
tmsgs = self._ability_augment(task, messages)
|
|
401
|
+
tmsgs = self._skill_augment(task, tmsgs)
|
|
402
|
+
tmsgs = self._pref_augment(tmsgs)
|
|
403
|
+
except Exception:
|
|
404
|
+
tmsgs = messages
|
|
405
|
+
_compact = getattr(self, "_compact_context", None)
|
|
406
|
+
if _compact is not None:
|
|
407
|
+
tmsgs = _compact(tmsgs, "tools")
|
|
408
|
+
try:
|
|
409
|
+
final = self._reply_with_tools(task, tmsgs, stop_event=stop_event)
|
|
410
|
+
except Exception:
|
|
411
|
+
final = None
|
|
412
|
+
if final is not None:
|
|
413
|
+
final, _oiss = _ig.guard_output(final, kernel=KERNEL_PROMPT)
|
|
414
|
+
guarded_out = bool(_oiss)
|
|
415
|
+
used_tools = True
|
|
416
|
+
for chunk in _chunk_text(final):
|
|
417
|
+
if stop_event is not None and stop_event.is_set():
|
|
418
|
+
break
|
|
419
|
+
parts.append(chunk)
|
|
420
|
+
yield chunk
|
|
421
|
+
if not used_tools:
|
|
422
|
+
try:
|
|
423
|
+
messages = self._web_augment(task, messages)
|
|
424
|
+
messages = self._ability_augment(task, messages)
|
|
425
|
+
messages = self._skill_augment(task, messages)
|
|
426
|
+
messages = self._pref_augment(messages)
|
|
427
|
+
except Exception:
|
|
428
|
+
messages = hist + [{"role": "user", "content": task}]
|
|
429
|
+
_compact = getattr(self, "_compact_context", None)
|
|
430
|
+
if _compact is not None:
|
|
431
|
+
messages = _compact(messages, "chat")
|
|
432
|
+
_tail = ""
|
|
433
|
+
_WIN = 256
|
|
434
|
+
for chunk in self.router.stream(self.system_prompt, messages,
|
|
435
|
+
agent_id=self.AGENT_ID, stop_event=stop_event,
|
|
436
|
+
images=images,
|
|
437
|
+
on_thinking=_emit_thinking if on_thinking else None):
|
|
438
|
+
_probe = _tail + chunk
|
|
439
|
+
if _ig.scan_output(_probe):
|
|
440
|
+
guarded_out = True
|
|
441
|
+
break
|
|
442
|
+
_tail = _probe[-_WIN:]
|
|
443
|
+
parts.append(chunk)
|
|
444
|
+
yield chunk
|
|
445
|
+
if guarded_out:
|
|
446
|
+
_notice = "\n\n(⚠ 已触发输出安全护栏,中断后续内容。)"
|
|
447
|
+
parts.append(_notice)
|
|
448
|
+
yield _notice
|
|
449
|
+
reply = "".join(parts)
|
|
450
|
+
stopped = stop_event is not None and stop_event.is_set()
|
|
451
|
+
if on_evidence is not None:
|
|
452
|
+
try:
|
|
453
|
+
_evs = _evidence_take(self)
|
|
454
|
+
if _evs:
|
|
455
|
+
on_evidence(_evs)
|
|
456
|
+
except Exception:
|
|
457
|
+
pass
|
|
458
|
+
if reply and not guarded_out and not used_tools and _ig.scan_output(reply, kernel=KERNEL_PROMPT):
|
|
459
|
+
guarded_out = True
|
|
460
|
+
try:
|
|
461
|
+
self._think(content=("流式直答:" + reply)[:500], node_type="reflection", conf=0.8)
|
|
462
|
+
except Exception:
|
|
463
|
+
pass
|
|
464
|
+
try:
|
|
465
|
+
_caps = getattr(self, "_caps", None)
|
|
466
|
+
_spine = _caps.get("event_spine") if _caps is not None else None
|
|
467
|
+
if _spine is not None and reply:
|
|
468
|
+
_spine.publish("chat.completed", {
|
|
469
|
+
"agent_id": self.AGENT_ID,
|
|
470
|
+
"task": (task or "")[:120],
|
|
471
|
+
"reply_chars": len(reply),
|
|
472
|
+
"guarded": bool(guarded_out),
|
|
473
|
+
}, source=self.AGENT_ID)
|
|
474
|
+
except Exception:
|
|
475
|
+
pass
|
|
476
|
+
_degraded = False
|
|
477
|
+
try:
|
|
478
|
+
_degraded = bool(self.router.take_call_degraded())
|
|
479
|
+
except Exception:
|
|
480
|
+
_degraded = False
|
|
481
|
+
if reply and not stopped and not guarded_out and not _degraded:
|
|
482
|
+
try:
|
|
483
|
+
safe_reply = _ig.redact_secrets(reply)
|
|
484
|
+
self._store_memory(f"对话:{task} → {safe_reply}"[:800], mem_type="conversation")
|
|
485
|
+
except Exception:
|
|
486
|
+
pass
|
|
487
|
+
self._maybe_store_preference(task)
|
|
488
|
+
elif _degraded and reply and not stopped:
|
|
489
|
+
try:
|
|
490
|
+
_spine = _caps.get("event_spine") if _caps is not None else None
|
|
491
|
+
if _spine is not None:
|
|
492
|
+
_spine.publish("chat.degraded", {
|
|
493
|
+
"agent_id": self.AGENT_ID,
|
|
494
|
+
"task": (task or "")[:120],
|
|
495
|
+
"reason": getattr(self.router, "degraded_reason", "")[:200],
|
|
496
|
+
}, source=self.AGENT_ID)
|
|
497
|
+
except Exception:
|
|
498
|
+
pass
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
_recall_topk: int = 5
|
|
502
|
+
|
|
503
|
+
def _task_entities(self, task: str) -> list:
|
|
504
|
+
"""本任务的基线实体(用于 Harness 目标基线)。领域 Agent 覆盖。"""
|
|
505
|
+
return []
|
|
506
|
+
|
|
507
|
+
def _recall_query(self, task: str) -> str:
|
|
508
|
+
"""记忆召回查询(领域 Agent 可加前缀,如财务/行程)。默认用原任务。"""
|
|
509
|
+
return task
|
|
510
|
+
|
|
511
|
+
def _on_session_started(self, session_id: str, task: str, context: dict) -> None:
|
|
512
|
+
"""会话已开、管线未跑:落 task_started(崩溃后才认得出"上次跑到一半")。"""
|
|
513
|
+
rec = getattr(self, "_recovery", None)
|
|
514
|
+
if rec is None:
|
|
515
|
+
return
|
|
516
|
+
try:
|
|
517
|
+
rec.task_started(task_id=session_id, description=task,
|
|
518
|
+
session_id=session_id,
|
|
519
|
+
channel=(context or {}).get("channel", ""))
|
|
520
|
+
except Exception:
|
|
521
|
+
pass
|
|
522
|
+
|
|
523
|
+
def _on_task_settled(self, session_id: str, status: str) -> None:
|
|
524
|
+
"""任务终结(done/failed):落 task_finished,把该 task 从"进行中"摘掉。"""
|
|
525
|
+
rec = getattr(self, "_recovery", None)
|
|
526
|
+
if rec is None:
|
|
527
|
+
return
|
|
528
|
+
try:
|
|
529
|
+
rec.task_finished(session_id, status=status)
|
|
530
|
+
except Exception:
|
|
531
|
+
pass
|
|
532
|
+
|
|
533
|
+
def _record_stage(self, session_id: str, stage: str) -> None:
|
|
534
|
+
"""管线 stage 级 checkpoint(fail-safe):断连/崩溃后能回答"跑到哪一步"。"""
|
|
535
|
+
rec = getattr(self, "_recovery", None)
|
|
536
|
+
if rec is None or not session_id:
|
|
537
|
+
return
|
|
538
|
+
try:
|
|
539
|
+
rec.record_step(session_id, {"kind": "stage", "name": stage})
|
|
540
|
+
except Exception:
|
|
541
|
+
pass
|
|
542
|
+
|
|
543
|
+
def _load_resume_cache(self, task_id: str) -> dict:
|
|
544
|
+
"""装载上次运行已完成的工具步骤 → {步骤指纹: {"name", "result"}}。
|
|
545
|
+
|
|
546
|
+
只收「成功 + 未被截断」的步:失败步复用了等于把失败当完成,截断结果复用了等于
|
|
547
|
+
喂给模型半份事实 —— 宁可不复用(重跑),也不给错的上下文。
|
|
548
|
+
"""
|
|
549
|
+
rec = getattr(self, "_recovery", None)
|
|
550
|
+
if rec is None or not task_id:
|
|
551
|
+
return {}
|
|
552
|
+
cache: dict = {}
|
|
553
|
+
try:
|
|
554
|
+
for st in rec.steps_of(task_id):
|
|
555
|
+
if st.get("kind") != "tool" or not st.get("key") or st.get("truncated"):
|
|
556
|
+
continue
|
|
557
|
+
res = st.get("result")
|
|
558
|
+
if isinstance(res, str) and res.strip():
|
|
559
|
+
cache[st["key"]] = {"name": st.get("name", ""), "result": res}
|
|
560
|
+
except Exception:
|
|
561
|
+
return {}
|
|
562
|
+
return cache
|
|
563
|
+
|
|
564
|
+
def _cognitive_pipeline(self):
|
|
565
|
+
"""本 Agent 的认知管线(v2 ②)。默认 recall→act 两个 stage;领域 Agent 可覆盖以
|
|
566
|
+
插入/替换 stage(后续步骤加 verify/reflect/plan/route/perceive)。缓存一份即可
|
|
567
|
+
(stage 无状态,只操作传入的 ctx)。"""
|
|
568
|
+
pipe = getattr(self, "_pipeline_cache", None)
|
|
569
|
+
if pipe is None:
|
|
570
|
+
from ..pipeline import CognitivePipeline, DEFAULT_STAGES
|
|
571
|
+
pipe = self._pipeline_cache = CognitivePipeline(DEFAULT_STAGES)
|
|
572
|
+
return pipe
|
|
573
|
+
|
|
574
|
+
async def run_task(self, task: str, context: dict | None = None, stop_event=None) -> dict:
|
|
575
|
+
"""统一任务生命周期:开会话 → [认知管线:recall → act] → 组装返回。
|
|
576
|
+
Harness BLOCK 统一在此兜底为结构化错误。返回
|
|
577
|
+
{"result",...,"session_id","agent","thought_summary", **extra}。
|
|
578
|
+
v2 ②:执行体由 CognitivePipeline 驱动(Step1 抽壳,行为与 P1-1 模板逐字一致)。
|
|
579
|
+
|
|
580
|
+
stop_event(P3,opt-in,与 stream_reply 同一约定):threading/asyncio.Event,置位则让
|
|
581
|
+
认知管线在下一 stage 边界 / reflect 重试前优雅停止;默认 None → 永不停,行为逐字不变。
|
|
582
|
+
中断时返回体带 `"interrupted": True`,result 为已完成 stage 的部分产物。
|
|
583
|
+
|
|
584
|
+
断点续跑(P1,opt-in):`context={"_resume_task_id": <上次 task_id>}` → 装载上次已完成
|
|
585
|
+
的工具步骤,命中即复用结果不再重复执行;返回体带 `resumed_from` / `resumed_steps`
|
|
586
|
+
说明续了哪一次、复用了多少步。"""
|
|
587
|
+
return await self._run_task_core(task, context, stop_event=stop_event, on_stage=None)
|
|
588
|
+
|
|
589
|
+
async def _run_task_core(self, task: str, context: dict | None = None,
|
|
590
|
+
stop_event=None, on_stage=None) -> dict:
|
|
591
|
+
"""run_task 的执行核心 —— 供同步 `run_task` 与流式 `run_task_stream` **共用**,避免两份
|
|
592
|
+
生命周期逻辑漂移。`on_stage`(D1):逐 stage 进度回调,透传给管线;None=同步路径不回调,
|
|
593
|
+
与改前 run_task 行为逐字一致。"""
|
|
594
|
+
from ..pipeline import PipelineContext
|
|
595
|
+
context = context or {}
|
|
596
|
+
session_id = self.start_session(goal=task, entities=self._task_entities(task))
|
|
597
|
+
_untrusted_set(self, bool(context.get("untrusted_source")))
|
|
598
|
+
self._on_session_started(session_id, task, context)
|
|
599
|
+
_hooks = getattr(self, "hooks", None)
|
|
600
|
+
_inject_parts = []
|
|
601
|
+
if _hooks is not None:
|
|
602
|
+
_hr = _hooks.fire("agent_start", "", {"task": task, "agent_id": self.AGENT_ID})
|
|
603
|
+
if _hr.additional_context:
|
|
604
|
+
_inject_parts.append(_hr.additional_context)
|
|
605
|
+
_pref = self._prefs_context()
|
|
606
|
+
if _pref:
|
|
607
|
+
_inject_parts.append(_pref)
|
|
608
|
+
_inject_tok = _reasoning_inject.set("\n\n".join(_inject_parts)) if _inject_parts else None
|
|
609
|
+
_resume_tid = str(context.get("_resume_task_id") or "").strip()
|
|
610
|
+
_resume_cache = self._load_resume_cache(_resume_tid) if _resume_tid else {}
|
|
611
|
+
_resume_tok = _resume_steps.set(_resume_cache) if _resume_cache else None
|
|
612
|
+
ctx = PipelineContext(agent=self, task=task, context=context,
|
|
613
|
+
session_id=session_id, stop_event=stop_event)
|
|
614
|
+
|
|
615
|
+
def _stage_hook(name, _ctx):
|
|
616
|
+
"""stage 边界:记一条 checkpoint(跑到哪一步),再转发给调用方的流式回调。"""
|
|
617
|
+
self._record_stage(session_id, name)
|
|
618
|
+
if on_stage is not None:
|
|
619
|
+
on_stage(name, _ctx)
|
|
620
|
+
|
|
621
|
+
try:
|
|
622
|
+
await self._cognitive_pipeline().run(ctx, on_stage=_stage_hook)
|
|
623
|
+
resp = {
|
|
624
|
+
"result": ctx.result,
|
|
625
|
+
"session_id": session_id,
|
|
626
|
+
"agent": self.AGENT_ID,
|
|
627
|
+
"thought_summary": self.thought_chain.get_summary(self.AGENT_ID),
|
|
628
|
+
**(ctx.extra or {}),
|
|
629
|
+
}
|
|
630
|
+
_plan = _plan_payload(ctx)
|
|
631
|
+
if _plan:
|
|
632
|
+
resp["plan"] = _plan
|
|
633
|
+
if _resume_cache:
|
|
634
|
+
resp["resumed_from"] = _resume_tid
|
|
635
|
+
resp["resumed_steps"] = len(_resume_cache)
|
|
636
|
+
if ctx.interrupted:
|
|
637
|
+
resp["interrupted"] = True
|
|
638
|
+
skill_status = (ctx.extra or {}).get("skill_status")
|
|
639
|
+
skill_incomplete = bool(skill_status) and skill_status != "done"
|
|
640
|
+
if skill_incomplete:
|
|
641
|
+
resp["status"] = skill_status
|
|
642
|
+
if skill_status == "needs_confirmation":
|
|
643
|
+
resp["pending_action"] = {
|
|
644
|
+
"skill_id": (ctx.extra or {}).get("skill_id"),
|
|
645
|
+
"task": task,
|
|
646
|
+
"message": (ctx.result or {}).get("message", "")
|
|
647
|
+
if isinstance(ctx.result, dict) else "",
|
|
648
|
+
"hint": "确认后带 context={'_confirmed': <skill_id>, ...} 重跑本任务以执行"
|
|
649
|
+
"(绑定到 skill_id 更安全;_confirmed=True 为概括确认)",
|
|
650
|
+
}
|
|
651
|
+
blocked = skill_incomplete or ctx.interrupted
|
|
652
|
+
self._emit_execution(task, ctx.result, blocked=blocked, context=context)
|
|
653
|
+
self._maybe_store_preference(task)
|
|
654
|
+
if _hooks is not None:
|
|
655
|
+
_hooks.fire("agent_end", "", {"task": task, "agent_id": self.AGENT_ID,
|
|
656
|
+
"result": resp.get("result"),
|
|
657
|
+
"interrupted": bool(ctx.interrupted)})
|
|
658
|
+
self._on_task_settled(session_id, "done")
|
|
659
|
+
return resp
|
|
660
|
+
except HarnessBlockedError as e:
|
|
661
|
+
self._emit_execution(
|
|
662
|
+
task, {"error": "harness_blocked", "reason": e.result.reason},
|
|
663
|
+
blocked=True, context=context)
|
|
664
|
+
if _hooks is not None:
|
|
665
|
+
_hooks.fire("agent_end", "", {"task": task, "agent_id": self.AGENT_ID,
|
|
666
|
+
"error": "harness_blocked"})
|
|
667
|
+
self._on_task_settled(session_id, "done")
|
|
668
|
+
return {"error": "harness_blocked", "reason": e.result.reason,
|
|
669
|
+
"session_id": session_id}
|
|
670
|
+
except Exception:
|
|
671
|
+
self._on_task_settled(session_id, "failed")
|
|
672
|
+
raise
|
|
673
|
+
finally:
|
|
674
|
+
if _inject_tok is not None:
|
|
675
|
+
_reasoning_inject.reset(_inject_tok)
|
|
676
|
+
if _resume_tok is not None:
|
|
677
|
+
_resume_steps.reset(_resume_tok)
|
|
678
|
+
|
|
679
|
+
def run_task_stream(self, task: str, context: dict | None = None, stop_event=None):
|
|
680
|
+
"""流式任务执行(D1):**同步 generator**,逐 stage 吐进度 + act 逐 token 出字 + 最终结果。
|
|
681
|
+
产出事件(dict,逐条给 SSE):
|
|
682
|
+
{"stage": name} 每完成一个认知 stage(perceive→…→reflect)
|
|
683
|
+
{"plan": {...}} 紧跟 plan stage:显式计划 {steps, flow, multi_step}(计划可见)
|
|
684
|
+
{"delta": text} act 的 LLM 逐 token(仅纯 LLM 路径;skill dispatch 路径无 token)
|
|
685
|
+
{"result": {...}} 最终 run_task 返回体(含 interrupted/status/pending_action/plan 等)
|
|
686
|
+
{"error": msg} HarnessBlock 之外的异常(HarnessBlock 已被核心兜底进 result)
|
|
687
|
+
|
|
688
|
+
与 `stream_reply` 同构(同步 generator,由 SSE 端点在后台线程迭代),复用共享的
|
|
689
|
+
`_run_task_core`,故生命周期/返回体与非流式 `run_task` **完全一致**——只是多了逐 stage /
|
|
690
|
+
逐 token 的即时反馈。`stop_event`(客户端断连)一处置位、两处生效:停管线 stage(P3)+
|
|
691
|
+
停 act 的 LLM 生成(省 token)。opt-in:不走此方法的 `run_task` 路径完全不受影响。
|
|
692
|
+
|
|
693
|
+
实现:worker 线程跑 `asyncio.run(_run_task_core(..., on_stage=…))`(与 /api/task 一样在
|
|
694
|
+
工作线程内起独立事件循环),stage 回调 + token 回调把事件塞进线程安全 `queue.Queue`,
|
|
695
|
+
本 generator 主体 drain 该队列并 yield。`_act_stream` contextvar 在 worker 内设置,随
|
|
696
|
+
asyncio Task / to_thread 的上下文复制传播到 `_call_llm` → 触发 act 流式。"""
|
|
697
|
+
import queue as _queue, threading as _threading, asyncio as _asyncio
|
|
698
|
+
bridge: "_queue.Queue" = _queue.Queue()
|
|
699
|
+
_SENTINEL = object()
|
|
700
|
+
|
|
701
|
+
def _on_stage(name, _ctx):
|
|
702
|
+
bridge.put({"stage": name})
|
|
703
|
+
if name == "plan":
|
|
704
|
+
_plan = _plan_payload(_ctx)
|
|
705
|
+
if _plan:
|
|
706
|
+
bridge.put({"plan": _plan})
|
|
707
|
+
|
|
708
|
+
def _on_token(chunk):
|
|
709
|
+
bridge.put({"delta": chunk})
|
|
710
|
+
|
|
711
|
+
def _worker():
|
|
712
|
+
token = _act_stream.set({"on_token": _on_token, "stop_event": stop_event})
|
|
713
|
+
try:
|
|
714
|
+
resp = _asyncio.run(self._run_task_core(
|
|
715
|
+
task, context, stop_event=stop_event, on_stage=_on_stage))
|
|
716
|
+
bridge.put({"result": resp})
|
|
717
|
+
except Exception as e:
|
|
718
|
+
bridge.put({"error": str(e)})
|
|
719
|
+
finally:
|
|
720
|
+
_act_stream.reset(token)
|
|
721
|
+
bridge.put(_SENTINEL)
|
|
722
|
+
|
|
723
|
+
_threading.Thread(target=_worker, daemon=True, name="task-stream").start()
|
|
724
|
+
while True:
|
|
725
|
+
item = bridge.get()
|
|
726
|
+
if item is _SENTINEL:
|
|
727
|
+
break
|
|
728
|
+
yield item
|
|
729
|
+
|
|
730
|
+
def _emit_execution(self, task, result, blocked: bool, context: dict):
|
|
731
|
+
"""【P2·全路径触发】把本 Agent 的任务执行发到事件脊柱 → TruthPipeline(共享真值记录)
|
|
732
|
+
+ CognitiveState/WS 可观测。修复"共享真值记录只在主管分发路径产生"的不一致:
|
|
733
|
+
现在直连领域 Agent 的任务也统一走这条真值链。
|
|
734
|
+
|
|
735
|
+
被主管分发/路由调用时(context['_no_emit']=True)跳过——由主管任务级别的统一
|
|
736
|
+
`_emit_execution` 承载(P1-1 后主管也走本模板),避免同一子任务双发。
|
|
737
|
+
经能力中枢解析事件脊柱;无中枢(standalone/治理场景)则静默跳过。"""
|
|
738
|
+
if context.get("_no_emit"):
|
|
739
|
+
return
|
|
740
|
+
caps = getattr(self, "_caps", None)
|
|
741
|
+
spine = caps.get("event_spine") if caps is not None else None
|
|
742
|
+
if spine is None:
|
|
743
|
+
return
|
|
744
|
+
try:
|
|
745
|
+
spine.publish("execution.completed", {
|
|
746
|
+
"agent_id": self.AGENT_ID, "task": task,
|
|
747
|
+
"result": result, "blocked": blocked}, source=self.AGENT_ID)
|
|
748
|
+
except Exception:
|
|
749
|
+
pass
|
|
750
|
+
|
|
751
|
+
async def _execute(self, task: str, context: dict, session_id: str,
|
|
752
|
+
memories: list) -> tuple:
|
|
753
|
+
"""领域执行步骤(召回之后 → 返回之前)。返回 (result, extra_dict)。
|
|
754
|
+
领域 Agent 必须实现。SupervisorAgent 例外:它的 pipeline 不含 act stage
|
|
755
|
+
(路由分发由 supervisor_stages 的 dispatch stage 承担),不会走到本钩子。"""
|
|
756
|
+
raise NotImplementedError(f"{self.AGENT_ID} 未实现 _execute()")
|
|
757
|
+
|
|
758
|
+
def situation(self) -> dict:
|
|
759
|
+
"""本 Agent 的领域态势:声明自己的 domain + 状态 + 告警。默认无领域态势。
|
|
760
|
+
领域 Agent 覆盖。编排层据此**通用聚合**,不需懂任何领域内部 API。
|
|
761
|
+
返回 {} 或 {"domain": str, "status": dict, "alerts": [ {domain,severity,msg} ]}。"""
|
|
762
|
+
return {}
|
|
763
|
+
|
|
764
|
+
def concede(self, req, verdict: str):
|
|
765
|
+
"""【协商闭环】本 Agent 作为**发起方**被拒后,能否让步?
|
|
766
|
+
|
|
767
|
+
返回修订后的 payload(dict)→ 编排层拿去再裁决一轮;返回 None → 让无可让,认输。
|
|
768
|
+
默认 None:**不让步**,行为与协商前逐字一致 —— 所以给所有未实现本钩子的 Agent
|
|
769
|
+
接上协商协议是零风险的(只有实现了 concede 的领域 Agent 才会真的多跑回合)。
|
|
770
|
+
|
|
771
|
+
让步逻辑必须留在**发起方领域**(编排层不懂"改高铁/换酒店"是什么意思)。
|
|
772
|
+
"""
|
|
773
|
+
return None
|
|
774
|
+
|
|
775
|
+
def handle_cross_request(self, req) -> None:
|
|
776
|
+
"""处理跨 Agent 请求,用**本领域守门人**裁决(就地写 req.status / req.verdict)。
|
|
777
|
+
默认:无特殊审批,放行。领域 Agent 覆盖。req 为鸭子类型(读 request_type/payload,
|
|
778
|
+
写 status/verdict),故不与 orchestration 形成 import 依赖。"""
|
|
779
|
+
req.status = "approved"
|
|
780
|
+
req.verdict = "无需特殊审批"
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
def get_status(self) -> dict:
|
|
784
|
+
return {
|
|
785
|
+
"agent_id": self.AGENT_ID,
|
|
786
|
+
"agent_name": self.AGENT_NAME,
|
|
787
|
+
"session_id": self._session_id,
|
|
788
|
+
"harness": {
|
|
789
|
+
"shadow_mode": self.harness.is_shadow_mode,
|
|
790
|
+
"thresholds": self.harness.thresholds,
|
|
791
|
+
"policy": self.harness.get_policy_summary(),
|
|
792
|
+
},
|
|
793
|
+
"thought_chain": self.thought_chain.get_summary(self.AGENT_ID),
|
|
794
|
+
}
|