foxclaw 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +1 -8
- package/package.json +1 -1
- package/python/.env.example +8 -0
- package/python/.github/workflows/ci.yml +47 -2
- package/python/.github/workflows/release.yml +5 -0
- package/python/CHANGELOG.md +100 -0
- package/python/CHANGELOG_CN.md +100 -0
- package/python/README.md +17 -2
- package/python/README_CN.md +38 -20
- package/python/bench/bm25_audit.py +1 -14
- package/python/bench/bm25_regression.py +0 -1
- package/python/bench/bm25_stress.py +0 -4
- package/python/bench/cd_audit.py +0 -23
- package/python/bench/chaos.py +6 -8
- package/python/bench/e2e_real_models.py +0 -1
- package/python/bench/llm_retry_test.py +2 -9
- package/python/bench/load_test.py +5 -8
- package/python/bench/vector_ivf_upgrade.py +1 -7
- package/python/docs/CONTENT_GUARD.md +189 -0
- package/python/docs/CONTENT_GUARD_CN.md +189 -0
- package/python/docs/RELEASE.md +78 -37
- package/python/docs/RUN_CN.md +2 -2
- package/python/evals/governance/cases.py +0 -37
- package/python/evals/governance/run_evals.py +5 -37
- package/python/examples/governance_middleware_demo.py +1 -6
- package/python/examples/langgraph_governance_demo.py +7 -11
- package/python/foxclaw/__init__.py +74 -152
- package/python/foxclaw/agents/base.py +82 -136
- package/python/foxclaw/agents/impl/_helpers.py +2 -10
- package/python/foxclaw/agents/impl/finance.py +4 -21
- package/python/foxclaw/agents/impl/office.py +7 -24
- package/python/foxclaw/agents/impl/supervisor.py +12 -79
- package/python/foxclaw/agents/mixins/_context.py +0 -23
- package/python/foxclaw/agents/mixins/augment.py +10 -16
- package/python/foxclaw/agents/mixins/llm.py +7 -6
- package/python/foxclaw/agents/mixins/memory.py +23 -3
- package/python/foxclaw/agents/mixins/preference.py +58 -8
- package/python/foxclaw/agents/mixins/prompt.py +59 -12
- package/python/foxclaw/agents/mixins/skillbox.py +6 -6
- package/python/foxclaw/agents/mixins/tool_exec.py +16 -42
- package/python/foxclaw/agents/rules/finance/RULES.md +131 -0
- package/python/foxclaw/api/__init__.py +18 -77
- package/python/foxclaw/api/_state.py +43 -0
- package/python/foxclaw/api/bootstrap.py +114 -0
- package/python/foxclaw/api/middleware.py +5 -28
- package/python/foxclaw/api/routes/ahs.py +60 -73
- package/python/foxclaw/api/routes/attachments.py +5 -6
- package/python/foxclaw/api/routes/browser.py +7 -11
- package/python/foxclaw/api/routes/domain.py +18 -26
- package/python/foxclaw/api/routes/knowledge.py +3 -21
- package/python/foxclaw/api/routes/llm_admin.py +21 -33
- package/python/foxclaw/api/routes/memory.py +50 -12
- package/python/foxclaw/api/routes/orchestration.py +3 -5
- package/python/foxclaw/api/routes/recovery.py +4 -3
- package/python/foxclaw/api/routes/review.py +2 -11
- package/python/foxclaw/api/routes/search.py +3 -7
- package/python/foxclaw/api/routes/system.py +33 -49
- package/python/foxclaw/api/routes/task.py +31 -49
- package/python/foxclaw/api/routes/ws.py +2 -6
- package/python/foxclaw/api/spine.py +1 -4
- package/python/foxclaw/behavior/__init__.py +20 -0
- package/python/foxclaw/behavior/behavior.yaml +62 -0
- package/python/foxclaw/behavior/spec.py +287 -0
- package/python/foxclaw/browser/action/action_executor.py +1 -5
- package/python/foxclaw/browser/action/action_types.py +0 -1
- package/python/foxclaw/browser/action/url_policy.py +1 -5
- package/python/foxclaw/browser/adapter/web_adapter.py +1 -1
- package/python/foxclaw/browser/driver/playwright_driver.py +20 -16
- package/python/foxclaw/browser/perception/dom_extractor.py +0 -1
- package/python/foxclaw/browser/perception/element_indexer.py +1 -2
- package/python/foxclaw/browser/service.py +7 -11
- package/python/foxclaw/capability/embedding_service.py +4 -6
- package/python/foxclaw/channels/__init__.py +9 -11
- package/python/foxclaw/channels/discord.py +4 -13
- package/python/foxclaw/channels/dispatcher.py +2 -27
- package/python/foxclaw/channels/feishu.py +10 -20
- package/python/foxclaw/channels/openclaw_bridge.py +4 -9
- package/python/foxclaw/channels/signing.py +9 -9
- package/python/foxclaw/channels/telegram.py +3 -4
- package/python/foxclaw/channels/weixin.py +7 -17
- package/python/foxclaw/cli.py +42 -13
- package/python/foxclaw/cognitive/reasoner.py +9 -10
- package/python/foxclaw/cognitive/state.py +18 -24
- package/python/foxclaw/core/audit_chain.py +1 -3
- package/python/foxclaw/core/audit_sink.py +3 -3
- package/python/foxclaw/core/compaction.py +8 -26
- package/python/foxclaw/core/config.py +2 -2
- package/python/foxclaw/core/confirm_nonce.py +6 -12
- package/python/foxclaw/core/content_guard.py +341 -0
- package/python/foxclaw/core/envfile.py +2 -2
- package/python/foxclaw/core/failure_policy.py +17 -26
- package/python/foxclaw/core/gateway.py +6 -10
- package/python/foxclaw/core/governance_audit.py +3 -7
- package/python/foxclaw/core/idempotency.py +15 -22
- package/python/foxclaw/core/injection_guard.py +10 -29
- package/python/foxclaw/core/memory_adapter.py +53 -60
- package/python/foxclaw/core/metrics.py +4 -5
- package/python/foxclaw/core/model_router.py +74 -150
- package/python/foxclaw/core/paths.py +1 -1
- package/python/foxclaw/core/recovery.py +21 -34
- package/python/foxclaw/core/scheduler.py +23 -26
- package/python/foxclaw/core/secrets.py +0 -3
- package/python/foxclaw/core/sensitive_write.py +283 -0
- package/python/foxclaw/core/sessions.py +4 -6
- package/python/foxclaw/core/settings.py +16 -32
- package/python/foxclaw/core/shared_write.py +11 -23
- package/python/foxclaw/core/steering_queue.py +12 -11
- package/python/foxclaw/core/system_bus.py +1 -1
- package/python/foxclaw/core/thought_chain.py +30 -32
- package/python/foxclaw/core/token_meter.py +9 -13
- package/python/foxclaw/core/tools.py +4 -12
- package/python/foxclaw/core/tracing.py +25 -40
- package/python/foxclaw/core/truth_pipeline.py +5 -9
- package/python/foxclaw/core/update_check.py +149 -0
- package/python/foxclaw/core/vec_recall_adapter.py +11 -37
- package/python/foxclaw/core/verify.py +6 -14
- package/python/foxclaw/core/web_search.py +4 -28
- package/python/foxclaw/finance/evidence.py +14 -18
- package/python/foxclaw/finance/fx.py +1 -3
- package/python/foxclaw/finance/graph.py +9 -18
- package/python/foxclaw/finance/models.py +20 -24
- package/python/foxclaw/finance/nlparse.py +12 -72
- package/python/foxclaw/finance/policy.py +8 -36
- package/python/foxclaw/finance/reasoner.py +3 -11
- package/python/foxclaw/finance/reasoning.py +13 -34
- package/python/foxclaw/finance/reconcile.py +5 -10
- package/python/foxclaw/finance/runtime.py +19 -46
- package/python/foxclaw/governance/middleware.py +13 -21
- package/python/foxclaw/harness/_base.py +2 -8
- package/python/foxclaw/harness/_policy.py +19 -38
- package/python/foxclaw/harness/drift.py +1 -13
- package/python/foxclaw/harness/event_bus.py +2 -5
- package/python/foxclaw/harness/harness.py +9 -97
- package/python/foxclaw/harness/loop_detection.py +11 -18
- package/python/foxclaw/itinerary/amap_client.py +2 -5
- package/python/foxclaw/itinerary/models.py +7 -7
- package/python/foxclaw/itinerary/reasoner.py +1 -6
- package/python/foxclaw/itinerary/reasoning.py +15 -21
- package/python/foxclaw/itinerary/runtime.py +8 -12
- package/python/foxclaw/kernel/capabilities.py +5 -6
- package/python/foxclaw/kernel/policy_gate.py +4 -10
- package/python/foxclaw/kernel/process_manager.py +17 -36
- package/python/foxclaw/kernel/scheduler.py +3 -9
- package/python/foxclaw/kernel/sentinel.py +13 -23
- package/python/foxclaw/kernel/sentinel_client.py +12 -15
- package/python/foxclaw/kernel/spine.py +9 -9
- package/python/foxclaw/knowledge/__init__.py +4 -20
- package/python/foxclaw/knowledge/truth_crystallizer.py +5 -8
- package/python/foxclaw/learning/knowledge_graph.py +4 -9
- package/python/foxclaw/learning/models.py +13 -13
- package/python/foxclaw/learning/reasoner.py +1 -4
- package/python/foxclaw/learning/runtime.py +19 -31
- package/python/foxclaw/learning/spaced_repetition.py +2 -7
- package/python/foxclaw/mcp_server.py +2 -3
- package/python/foxclaw/orchestration/__init__.py +16 -33
- package/python/foxclaw/pipeline/cognitive_pipeline.py +45 -97
- package/python/foxclaw/pipeline/stage_meta.py +21 -0
- package/python/foxclaw/pipeline/supervisor_stages.py +13 -42
- package/python/foxclaw/planning/llm_planner.py +3 -3
- package/python/foxclaw/planning/planner.py +6 -11
- package/python/foxclaw/search/agent_port.py +7 -11
- package/python/foxclaw/search/evidence.py +0 -4
- package/python/foxclaw/search/internal_search.py +3 -5
- package/python/foxclaw/search/models.py +11 -13
- package/python/foxclaw/search/planner.py +3 -3
- package/python/foxclaw/search/retriever.py +1 -1
- package/python/foxclaw/search/runtime.py +1 -8
- package/python/foxclaw/skills/__init__.py +46 -100
- package/python/foxclaw/skills/archive.py +111 -0
- package/python/foxclaw/skills/fetch.py +226 -0
- package/python/foxclaw/skills/hooks_spec.py +21 -25
- package/python/foxclaw/skills/install_cmd.py +216 -0
- package/python/foxclaw/skills/lifecycle.py +16 -42
- package/python/foxclaw/skills/markdown_skill.py +10 -9
- package/python/foxclaw/skills/mcp_client.py +5 -11
- package/python/foxclaw/skills/mcp_store.py +0 -3
- package/python/foxclaw/skills/permission.py +13 -48
- package/python/foxclaw/skills/pipeline.py +21 -53
- package/python/foxclaw/skills/policy.py +4 -10
- package/python/foxclaw/skills/review.py +3 -3
- package/python/frontend/deck/auth.js +0 -7
- package/python/frontend/deck/chat_model_picker.js +0 -54
- package/python/frontend/deck/domain.js +0 -20
- package/python/frontend/deck/main.js +0 -144
- package/python/frontend/deck/markdown.js +0 -4
- package/python/frontend/deck/settings.js +1 -18
- package/python/frontend/deck/state.js +0 -1
- package/python/frontend/deck/util.js +0 -2
- package/python/frontend/deck/visuals.js +0 -22
- package/python/frontend/planet/planet.js +18 -201
- package/python/frontend/v3/assets/index-JKfHlb-a.css +1 -0
- package/python/frontend/v3/assets/index-tN90dpUH.js +34 -0
- package/python/frontend/v3/index.html +3 -3
- package/python/frontend/widget_runtime.js +0 -43
- package/python/frontend/widgets.js +0 -57
- package/python/frontend_src/index.html +1 -1
- package/python/frontend_src/package-lock.json +2 -2
- package/python/frontend_src/package.json +1 -1
- package/python/frontend_src/src/App.vue +3 -7
- package/python/frontend_src/src/api.js +0 -7
- package/python/frontend_src/src/components/ChatDock.vue +27 -39
- package/python/frontend_src/src/components/DocViewer.vue +0 -9
- package/python/frontend_src/src/components/Inspector.vue +0 -6
- package/python/frontend_src/src/components/LeftRail.vue +60 -39
- package/python/frontend_src/src/components/MenuPopover.vue +9 -11
- package/python/frontend_src/src/components/MessageStream.vue +18 -30
- package/python/frontend_src/src/components/ModelPicker.vue +0 -5
- package/python/frontend_src/src/components/QuickIngest.vue +0 -6
- package/python/frontend_src/src/components/Stage.vue +71 -25
- package/python/frontend_src/src/components/ToolMenu.vue +0 -3
- package/python/frontend_src/src/components/TopBar.vue +38 -10
- package/python/frontend_src/src/components/panels/ActivityPanel.vue +0 -2
- package/python/frontend_src/src/components/panels/AgentsPanel.vue +2 -5
- package/python/frontend_src/src/components/panels/KnowledgePanel.vue +0 -16
- package/python/frontend_src/src/components/panels/MemoryPanel.vue +42 -3
- package/python/frontend_src/src/components/panels/PanelBox.vue +6 -7
- package/python/frontend_src/src/components/panels/ReviewPanel.vue +0 -16
- package/python/frontend_src/src/components/panels/SettingsPanel.vue +142 -38
- package/python/frontend_src/src/components/panels/TasksPanel.vue +0 -1
- package/python/frontend_src/src/components/panels/ToolsPanel.vue +125 -19
- package/python/frontend_src/src/lib/fmt.js +0 -4
- package/python/frontend_src/src/lib/i18n.js +53 -0
- package/python/frontend_src/src/lib/locales/en.js +173 -0
- package/python/frontend_src/src/lib/locales/zh.js +174 -0
- package/python/frontend_src/src/lib/md.js +0 -14
- package/python/frontend_src/src/main.js +2 -0
- package/python/frontend_src/src/stores/app.js +17 -32
- package/python/frontend_src/src/stores/chat.js +0 -34
- package/python/frontend_src/src/stores/model.js +0 -8
- package/python/frontend_src/src/stores/panels.js +16 -44
- package/python/frontend_src/src/stores/profile.js +31 -0
- package/python/frontend_src/src/stores/system.js +25 -0
- package/python/frontend_src/vite.config.js +0 -7
- package/python/pyproject.toml +17 -3
- package/python/requirements.txt +311 -0
- package/python/tests/conftest.py +0 -22
- package/python/tests/run_all.py +4 -66
- package/python/tests/test_agent_lifecycle.py +4 -38
- package/python/tests/test_agent_rules.py +81 -0
- package/python/tests/test_ahs_p2.py +4 -25
- package/python/tests/test_anthropic_tools.py +1 -15
- package/python/tests/test_api.py +19 -9
- package/python/tests/test_api_auth.py +1 -5
- package/python/tests/test_attachments.py +9 -25
- package/python/tests/test_audit_chain.py +2 -7
- package/python/tests/test_audit_sink.py +0 -6
- package/python/tests/test_behavior_spec.py +198 -0
- package/python/tests/test_browser.py +2 -6
- package/python/tests/test_browser_request_guard.py +0 -4
- package/python/tests/test_capabilities.py +0 -6
- package/python/tests/test_channel_signing.py +2 -9
- package/python/tests/test_channels.py +1 -11
- package/python/tests/test_config_layering.py +0 -4
- package/python/tests/test_confirm_nonce.py +1 -12
- package/python/tests/test_conflict_dismiss.py +143 -0
- package/python/tests/test_content_guard.py +177 -0
- package/python/tests/test_context_compaction_wiring.py +1 -7
- package/python/tests/test_dispatch.py +0 -6
- package/python/tests/test_domain_evals.py +11 -80
- package/python/tests/test_domain_import.py +0 -6
- package/python/tests/test_domain_reasoners.py +0 -14
- package/python/tests/test_e2e.py +0 -17
- package/python/tests/test_emb_cache.py +7 -17
- package/python/tests/test_embedding_singleton.py +1 -3
- package/python/tests/test_evals_smoke.py +0 -24
- package/python/tests/test_event_spine.py +10 -18
- package/python/tests/test_evidence_semantic.py +1 -14
- package/python/tests/test_evidence_sse.py +3 -6
- package/python/tests/test_file_ingest.py +0 -11
- package/python/tests/test_finance.py +0 -9
- package/python/tests/test_finance_ledger.py +5 -12
- package/python/tests/test_finance_persist.py +8 -12
- package/python/tests/test_gateway.py +3 -22
- package/python/tests/test_governance.py +5 -52
- package/python/tests/test_governance_audit.py +4 -39
- package/python/tests/test_governance_middleware.py +1 -7
- package/python/tests/test_hkc.py +6 -24
- package/python/tests/test_hkc_index_build_manager.py +0 -33
- package/python/tests/test_hkc_manifest_reuse.py +4 -27
- package/python/tests/test_hkc_search_event_sync.py +3 -54
- package/python/tests/test_import_path_guard.py +0 -6
- package/python/tests/test_injection_guard.py +18 -36
- package/python/tests/test_integration.py +5 -29
- package/python/tests/test_itinerary.py +0 -3
- package/python/tests/test_kee_semantic.py +0 -5
- package/python/tests/test_kernel.py +1 -11
- package/python/tests/test_learning.py +3 -12
- package/python/tests/test_llm_admin.py +4 -19
- package/python/tests/test_llm_planner.py +1 -23
- package/python/tests/test_markdown_skills.py +3 -14
- package/python/tests/test_markdown_skills_openclaw.py +4 -9
- package/python/tests/test_mcp_connectors.py +3 -15
- package/python/tests/test_mcp_guardian_discovery.py +2 -6
- package/python/tests/test_mcp_no_memory.py +3 -3
- package/python/tests/test_mcp_policy_gate.py +0 -1
- package/python/tests/test_mcp_server.py +6 -9
- package/python/tests/test_memory_recall.py +1 -4
- package/python/tests/test_metrics.py +0 -8
- package/python/tests/test_multi_chain.py +1 -13
- package/python/tests/test_multimodal.py +5 -18
- package/python/tests/test_negotiation.py +3 -13
- package/python/tests/test_orchestration.py +1 -11
- package/python/tests/test_orchestration_decoupled.py +0 -6
- package/python/tests/test_p3_p5_hardening.py +3 -17
- package/python/tests/test_parallel_dispatch.py +4 -12
- package/python/tests/test_pipeline.py +17 -58
- package/python/tests/test_plan_visible.py +4 -4
- package/python/tests/test_planner.py +0 -7
- package/python/tests/test_policy_gate.py +2 -3
- package/python/tests/test_profile.py +67 -0
- package/python/tests/test_readme_claims.py +163 -0
- package/python/tests/test_reasoner.py +2 -17
- package/python/tests/test_release_check.py +2 -13
- package/python/tests/test_resume_checkpoint.py +7 -15
- package/python/tests/test_routing.py +2 -12
- package/python/tests/test_scheduler.py +7 -10
- package/python/tests/test_search.py +1 -11
- package/python/tests/test_secrets.py +4 -10
- package/python/tests/test_semantic.py +1 -10
- package/python/tests/test_sensitive_write.py +181 -0
- package/python/tests/test_sessions.py +3 -4
- package/python/tests/test_settings.py +2 -18
- package/python/tests/test_shadow_learning.py +3 -43
- package/python/tests/test_shared_write.py +0 -7
- package/python/tests/test_skill_taint_gate.py +1 -10
- package/python/tests/test_skills.py +2 -18
- package/python/tests/test_skills_fetch.py +282 -0
- package/python/tests/test_stats_and_events.py +2 -5
- package/python/tests/test_streaming.py +2 -7
- package/python/tests/test_supervisor_pipeline.py +4 -5
- package/python/tests/test_supervisor_triage.py +4 -10
- package/python/tests/test_taint.py +2 -7
- package/python/tests/test_task_stream.py +1 -12
- package/python/tests/test_thinking_sse.py +0 -1
- package/python/tests/test_tool_center.py +1 -17
- package/python/tests/test_tool_parallel.py +2 -2
- package/python/tests/test_tool_params.py +1 -5
- package/python/tests/test_tool_use.py +0 -13
- package/python/tests/test_toplevel_concurrency.py +3 -4
- package/python/tests/test_tracing.py +7 -41
- package/python/tests/test_truth_crystallizer.py +4 -6
- package/python/tests/test_truth_flow.py +2 -7
- package/python/tests/test_update_check.py +95 -0
- package/python/tests/test_verify_evidence.py +8 -21
- package/python/tests/test_version_sync.py +35 -6
- package/python/tests/test_web_trigger.py +0 -17
- package/python/tools/build_content_lexicon.py +218 -0
- package/python/tools/bundle_web.py +98 -0
- package/python/tools/check_faiss_type.py +0 -1
- package/python/tools/check_log.ps1 +0 -8
- package/python/tools/mypy_baseline.txt +12 -4
- package/python/tools/mypy_ratchet.py +52 -6
- package/python/tools/pack.py +0 -5
- package/python/tools/recheck_conflicts.py +156 -0
- package/python/tools/release_check.py +24 -15
- package/python/tools/restart_foxclaw.ps1 +5 -8
- package/python/tools/shared_write_audit.py +1 -15
- package/python/tools/size_check.py +1 -1
- package/python/tools/strip_comments.py +228 -0
- package/python/vendor/hkc/hkc_api/routes/ability.py +31 -0
- package/python/vendor/hkc/hkc_api/schemas.py +11 -0
- package/python/vendor/hkc/hkc_kee/kee.py +124 -3
- package/python/vendor/hkc/hkc_sdk/base.py +22 -0
- package/python/vendor/hkc/hkc_sdk/direct_client.py +10 -0
- package/python/vendor/hkc/hkc_sdk/http_client.py +19 -0
- package/python/foxclaw/api/app.py +0 -123
- package/python/frontend/v3/assets/index-BD7VPbMV.js +0 -33
- package/python/frontend/v3/assets/index-CAvnYWGH.css +0 -1
|
@@ -15,6 +15,7 @@ import inspect
|
|
|
15
15
|
import os
|
|
16
16
|
from dataclasses import dataclass, field
|
|
17
17
|
from typing import Any, Callable
|
|
18
|
+
from .stage_meta import named_stage
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
@dataclass
|
|
@@ -27,32 +28,27 @@ class PipelineContext:
|
|
|
27
28
|
反对的是"跨任务/并发共享可变状态"(改一半被读到不一致);ctx 不属于该反模式,可变安全。
|
|
28
29
|
并发隔离由 test_pipeline.py::test_concurrent_ctx_isolation 锁定(24 并发任务零串)。
|
|
29
30
|
(会话指针 _session_id/_parent_node_id 另经 contextvar 隔离,见 base.py。)"""
|
|
30
|
-
agent: Any
|
|
31
|
+
agent: Any
|
|
31
32
|
task: str
|
|
32
33
|
context: dict = field(default_factory=dict)
|
|
33
34
|
session_id: str = ""
|
|
34
|
-
|
|
35
|
-
perceived: dict = field(default_factory=dict) # perceive stage 的输入理解
|
|
35
|
+
perceived: dict = field(default_factory=dict)
|
|
36
36
|
memories: list = field(default_factory=list)
|
|
37
|
-
plan: dict = field(default_factory=dict)
|
|
38
|
-
route: dict = field(default_factory=dict)
|
|
37
|
+
plan: dict = field(default_factory=dict)
|
|
38
|
+
route: dict = field(default_factory=dict)
|
|
39
39
|
result: Any = None
|
|
40
40
|
extra: dict = field(default_factory=dict)
|
|
41
|
-
verified: dict = field(default_factory=dict)
|
|
42
|
-
reflect: dict = field(default_factory=dict)
|
|
43
|
-
trace: list = field(default_factory=list)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
plan_obj: Any = None # plan stage 的 planner 拆解结果(多步计划)
|
|
53
|
-
step_routes: list = field(default_factory=list) # [(子任务文本, 目标 Agent)]
|
|
54
|
-
multi_agent: bool = False # 是否跨多 Agent(顺序链式 / 并行 fork-join)
|
|
55
|
-
dispatch_result: dict = field(default_factory=dict) # dispatch stage 的原始分发返回
|
|
41
|
+
verified: dict = field(default_factory=dict)
|
|
42
|
+
reflect: dict = field(default_factory=dict)
|
|
43
|
+
trace: list = field(default_factory=list)
|
|
44
|
+
stop_event: Any = None
|
|
45
|
+
interrupted: bool = False
|
|
46
|
+
sched: Any = None
|
|
47
|
+
target: str = ""
|
|
48
|
+
plan_obj: Any = None
|
|
49
|
+
step_routes: list = field(default_factory=list)
|
|
50
|
+
multi_agent: bool = False
|
|
51
|
+
dispatch_result: dict = field(default_factory=dict)
|
|
56
52
|
|
|
57
53
|
|
|
58
54
|
def _emit_stage(ctx: "PipelineContext", stage_name: str) -> None:
|
|
@@ -95,16 +91,10 @@ class CognitivePipeline:
|
|
|
95
91
|
回调异常被吞(fail-safe):流式反馈绝不拖垮任务执行。"""
|
|
96
92
|
from foxclaw.core.tracing import span as _span
|
|
97
93
|
for stage in self.stages:
|
|
98
|
-
# 协作式中断点(P3):stage **边界**检查 stop_event。opt-in——默认 stop_event=None 恒不触发,
|
|
99
|
-
# 与改前逐字一致。置位时优雅停:标记 interrupted、保留已完成 stage 的 ctx 产物、不跑后续 stage。
|
|
100
|
-
# 粒度=stage 边界:一般无法中止已在执行的单个 stage;但**流式路径(run_task_stream,D1)**下
|
|
101
|
-
# act 的 LLM 生成经 `_act_stream` 的**同一** stop_event 串到 `router.stream`,可 **token 级**
|
|
102
|
-
# 即时停 → 断连时二者协同:停后续 stage(此处)+ 停 act 生成(base._call_llm),省 token。
|
|
103
94
|
if _stop_requested(ctx):
|
|
104
95
|
ctx.interrupted = True
|
|
105
96
|
break
|
|
106
97
|
name = getattr(stage, "stage_name", getattr(stage, "__name__", "stage"))
|
|
107
|
-
# 单一 stage 循环 → 一处埋点覆盖全部 stage(链路里能看到 perceive→…→reflect 各自耗时)
|
|
108
98
|
with _span(f"stage.{name}", agent=getattr(ctx.agent, "AGENT_ID", "")):
|
|
109
99
|
r = stage(ctx)
|
|
110
100
|
if inspect.isawaitable(r):
|
|
@@ -113,15 +103,13 @@ class CognitivePipeline:
|
|
|
113
103
|
_emit_stage(ctx, name)
|
|
114
104
|
if on_stage is not None:
|
|
115
105
|
try:
|
|
116
|
-
on_stage(name, ctx)
|
|
106
|
+
on_stage(name, ctx)
|
|
117
107
|
except Exception:
|
|
118
|
-
pass
|
|
108
|
+
pass
|
|
119
109
|
return ctx
|
|
120
110
|
|
|
121
111
|
|
|
122
|
-
|
|
123
|
-
# stage 是操作 ctx 的可调用;命名 stage_* 便于事件与轨迹标注。
|
|
124
|
-
|
|
112
|
+
@named_stage("perceive")
|
|
125
113
|
def stage_perceive(ctx: PipelineContext) -> None:
|
|
126
114
|
"""感知:把原始输入框定为结构化感知(Step 5)。当前记录轻量输入特征;
|
|
127
115
|
这里是未来"意图识别/输入解析"的落点。也是逐 stage 事件链的起点。"""
|
|
@@ -129,14 +117,13 @@ def stage_perceive(ctx: PipelineContext) -> None:
|
|
|
129
117
|
"task_len": len(ctx.task or ""),
|
|
130
118
|
"has_context": bool(ctx.context),
|
|
131
119
|
}
|
|
132
|
-
stage_perceive.stage_name = "perceive"
|
|
133
120
|
|
|
134
121
|
|
|
122
|
+
@named_stage("recall")
|
|
135
123
|
async def stage_recall(ctx: PipelineContext) -> None:
|
|
136
124
|
"""召回相关记忆(等价 P1-1 模板的 _recall_memory(_recall_query, top_k))。"""
|
|
137
125
|
a = ctx.agent
|
|
138
126
|
ctx.memories = a._recall_memory(a._recall_query(ctx.task), top_k=a._recall_topk)
|
|
139
|
-
stage_recall.stage_name = "recall"
|
|
140
127
|
|
|
141
128
|
|
|
142
129
|
def _plan_text(plan: dict) -> str:
|
|
@@ -152,7 +139,7 @@ def _record_plan_node(ctx: PipelineContext) -> None:
|
|
|
152
139
|
计划节点不参与准入裁决,也不被 act 读取(见下方「informational」边界说明)。
|
|
153
140
|
"""
|
|
154
141
|
agent = getattr(ctx, "agent", None)
|
|
155
|
-
think = getattr(agent, "_think", None)
|
|
142
|
+
think = getattr(agent, "_think", None)
|
|
156
143
|
if think is None:
|
|
157
144
|
return
|
|
158
145
|
try:
|
|
@@ -162,6 +149,7 @@ def _record_plan_node(ctx: PipelineContext) -> None:
|
|
|
162
149
|
pass
|
|
163
150
|
|
|
164
151
|
|
|
152
|
+
@named_stage("plan")
|
|
165
153
|
def stage_plan(ctx: PipelineContext) -> None:
|
|
166
154
|
"""结构规划:用 **P1-2 统一规划引擎** 分析任务结构(是否多步/并行),记录到 ctx.plan。
|
|
167
155
|
|
|
@@ -187,28 +175,10 @@ def stage_plan(ctx: PipelineContext) -> None:
|
|
|
187
175
|
steps, flow = [task], "single"
|
|
188
176
|
ctx.plan = {"steps": steps, "flow": flow, "multi_step": len(steps) > 1}
|
|
189
177
|
_record_plan_node(ctx)
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
# stage_plan 写 ctx.plan 供观测/审计,但**没有任何 _execute 读取它**,故不改变执行
|
|
193
|
-
# (act 整段交 LLM/skill)。让 act 真按 plan 多步执行是**行为改变**,须 opt-in + 每步
|
|
194
|
-
# confirm 挂起 + fail-safe(见 V2_ROADMAP「Skill 接入认知管线」),不在此顺手做。
|
|
195
|
-
# (B1 自审:曾在此加 `stage_plan.informational=True` 属性,但无任何消费者=又一个死标记,
|
|
196
|
-
# 已删——注释足以说明,不造没人读的假结构化标记。)
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
# 默认自动派发的门槛(A·Step5,2026-07-16:从"默认关"改为"默认开但严格受限")。
|
|
200
|
-
# 背景:此前 route/dispatch 双双默认关 → 工具中心/MCP 连接器/技能台**在正常对话里根本够不着**
|
|
201
|
-
# (「帮我算 3+5」助手只会用 LLM 硬答,计算器技能不会被调用)。整套工具生态形同虚设。
|
|
202
|
-
# 放量策略:只自动派发**只读**技能、且要求**高置信**;有副作用的(write/send/pay/destructive)
|
|
203
|
-
# 一律不自动选中,仍须显式 use_skill —— 自动执行绝不越过"会改变世界"的那条线。
|
|
204
|
-
# 阈值取 0.6 —— 这是**量出来的**,不是拍的。router 实测分值只有三档:
|
|
205
|
-
# 0.600 强匹配(触发词直接命中,如「帮我计算…」→calculator)
|
|
206
|
-
# 0.480 弱匹配(如「查下去公司的路线」→amap_route、「发邮件给张三」→gmail_send)
|
|
207
|
-
# 0.000 无匹配(闲聊/写代码)
|
|
208
|
-
# 故 (0.48, 0.60] 区间内的任何值都等价于"只要强匹配";再高(如 0.75)则**一个都选不中**、
|
|
209
|
-
# 功能形同虚设。0.6 已是最保守的可用值 —— "比 0.6 更保守"是伪命题。
|
|
178
|
+
|
|
179
|
+
|
|
210
180
|
AUTO_ROUTE_THRESHOLD = 0.6
|
|
211
|
-
AUTO_ROUTE_READ_ONLY = True
|
|
181
|
+
AUTO_ROUTE_READ_ONLY = True
|
|
212
182
|
|
|
213
183
|
|
|
214
184
|
def _is_read_only(skill) -> bool:
|
|
@@ -242,6 +212,7 @@ def _route_policy(agent, context: dict) -> dict:
|
|
|
242
212
|
"read_only": bool(p.get("read_only", AUTO_ROUTE_READ_ONLY))}
|
|
243
213
|
|
|
244
214
|
|
|
215
|
+
@named_stage("route")
|
|
245
216
|
def stage_route(ctx: PipelineContext) -> None:
|
|
246
217
|
"""路由决策:记录本任务由谁处理到 ctx.route(target=self 或 skill)。
|
|
247
218
|
|
|
@@ -260,33 +231,27 @@ def stage_route(ctx: PipelineContext) -> None:
|
|
|
260
231
|
return
|
|
261
232
|
reg = getattr(ctx.agent, "skill_registry", None)
|
|
262
233
|
if reg is None:
|
|
263
|
-
return
|
|
234
|
+
return
|
|
264
235
|
try:
|
|
265
236
|
from ..skills.pipeline import route as skill_route
|
|
266
237
|
r = skill_route(ctx.task, reg)
|
|
267
238
|
except Exception:
|
|
268
|
-
return
|
|
239
|
+
return
|
|
269
240
|
if r.primary and r.confidence >= policy["threshold"]:
|
|
270
241
|
sk = reg.get(r.primary)
|
|
271
|
-
# 只读闸:有副作用的技能(write/send/pay/destructive)**绝不自动选中**——
|
|
272
|
-
# 自动执行不得越过"会改变世界"的线;要用仍可显式 use_skill(那时用户是知情的)。
|
|
273
242
|
if policy["read_only"] and sk is not None and not _is_read_only(sk):
|
|
274
|
-
return
|
|
275
|
-
# 外泄闸:问的是**用户自己的数据**(账本/行程/学习)→ 绝不自动发给**外部**技能。
|
|
276
|
-
# 与 needs_search 共用同一判定(单一真源)。否则「查一下最新的本月支出」会把私人
|
|
277
|
-
# 财务问题自动送去外部搜索——那正是 needs_search 已挡住、却从技能这条旁路漏回来的 bug。
|
|
243
|
+
return
|
|
278
244
|
if sk is not None and getattr(sk, "external", False):
|
|
279
245
|
try:
|
|
280
246
|
from ..core.web_search import is_personal_query
|
|
281
247
|
if is_personal_query(ctx.task):
|
|
282
|
-
return
|
|
248
|
+
return
|
|
283
249
|
except Exception:
|
|
284
|
-
pass
|
|
250
|
+
pass
|
|
285
251
|
ctx.route = {"target": "skill", "skill_id": r.primary,
|
|
286
252
|
"confidence": round(float(r.confidence), 3),
|
|
287
253
|
"reason": f"router 选中 skill「{r.primary}」"
|
|
288
254
|
f"(置信 {r.confidence:.2f}≥{policy['threshold']})"}
|
|
289
|
-
stage_route.stage_name = "route"
|
|
290
255
|
|
|
291
256
|
|
|
292
257
|
def _skill_dispatch_enabled(agent, context: dict) -> bool:
|
|
@@ -339,21 +304,21 @@ async def _act_via_skill(ctx: "PipelineContext"):
|
|
|
339
304
|
reg = getattr(agent, "skill_registry", None)
|
|
340
305
|
adp = getattr(agent, "skill_adapter", None)
|
|
341
306
|
if reg is None or adp is None:
|
|
342
|
-
return None
|
|
307
|
+
return None
|
|
343
308
|
sid = (ctx.route or {}).get("skill_id")
|
|
344
|
-
|
|
309
|
+
if not isinstance(sid, str) or not sid:
|
|
310
|
+
return None
|
|
311
|
+
skill = reg.get(sid)
|
|
345
312
|
if skill is None:
|
|
346
313
|
return None
|
|
347
314
|
from ..skills.pipeline import _build_args
|
|
348
315
|
from ..skills import set_loop_session, reset_loop_session
|
|
349
316
|
args = _build_args(skill, ctx.task, {})
|
|
350
317
|
confirmed = _confirmed_for(ctx.context, sid)
|
|
351
|
-
token = set_loop_session(getattr(ctx, "session_id", None))
|
|
318
|
+
token = set_loop_session(getattr(ctx, "session_id", None))
|
|
352
319
|
try:
|
|
353
320
|
res = await asyncio.to_thread(adp.invoke, skill, args, confirmed)
|
|
354
321
|
except Exception as e:
|
|
355
|
-
# invoke 理应自兜底(catches _dispatch 异常);万一权限评估等意外抛错 → **fail-closed**:
|
|
356
|
-
# 结构化 error 回传,**绝不回落 LLM**(路由到发邮件却让 LLM 代答=幻觉/危险),也不崩整个任务。
|
|
357
322
|
return ({"skill": sid, "status": "error", "message": f"skill dispatch 异常: {e}",
|
|
358
323
|
"pending": False}, {"via": "skill", "skill_id": sid, "skill_status": "error"})
|
|
359
324
|
finally:
|
|
@@ -361,12 +326,11 @@ async def _act_via_skill(ctx: "PipelineContext"):
|
|
|
361
326
|
extra = {"via": "skill", "skill_id": sid, "skill_status": res.status}
|
|
362
327
|
if res.success:
|
|
363
328
|
return ({"skill": sid, "output": res.output, "status": res.status}, extra)
|
|
364
|
-
# 非成功:无副作用已发生(权限门+confirmed=False保证)。结构化回传,绝不当完成、绝不回落 LLM
|
|
365
|
-
# (路由到发邮件却让 LLM 代答=危险/幻觉);needs_confirmation 待用户确认后重跑(带 _confirmed)。
|
|
366
329
|
return ({"skill": sid, "status": res.status, "message": res.message,
|
|
367
330
|
"pending": res.status == "needs_confirmation"}, extra)
|
|
368
331
|
|
|
369
332
|
|
|
333
|
+
@named_stage("act")
|
|
370
334
|
async def stage_act(ctx: PipelineContext) -> None:
|
|
371
335
|
"""领域执行步骤。默认:领域 `_execute`(→ LLM),等价 P1-1 模板。
|
|
372
336
|
|
|
@@ -381,9 +345,9 @@ async def stage_act(ctx: PipelineContext) -> None:
|
|
|
381
345
|
return
|
|
382
346
|
ctx.result, ctx.extra = await ctx.agent._execute(
|
|
383
347
|
ctx.task, ctx.context, ctx.session_id, ctx.memories)
|
|
384
|
-
stage_act.stage_name = "act"
|
|
385
348
|
|
|
386
349
|
|
|
350
|
+
@named_stage("verify")
|
|
387
351
|
def stage_verify(ctx: PipelineContext) -> None:
|
|
388
352
|
"""把本次执行的 Harness 校验**结果**显式化到 ctx.verified(Step 2)。
|
|
389
353
|
|
|
@@ -399,19 +363,15 @@ def stage_verify(ctx: PipelineContext) -> None:
|
|
|
399
363
|
except Exception:
|
|
400
364
|
nodes = []
|
|
401
365
|
warns = sum(1 for n in nodes if getattr(n, "harness_status", "") == "WARN")
|
|
402
|
-
deltas = [n.harness_delta for n in nodes
|
|
366
|
+
deltas = [n.harness_delta for n in nodes
|
|
403
367
|
if getattr(n, "harness_delta", None) is not None]
|
|
404
368
|
ctx.verified = {
|
|
405
|
-
"passed": True,
|
|
369
|
+
"passed": True,
|
|
406
370
|
"nodes": len(nodes),
|
|
407
371
|
"warns": warns,
|
|
408
|
-
"max_delta": round(max(deltas), 3) if deltas else 0.0,
|
|
409
|
-
# last_delta = 最近一次推理的漂移(本次尝试的最终状态)。reflect 用它作重试信号——
|
|
410
|
-
# 深审修复:max_delta 是 max over 累积历史(单调不减),重试追加节点后仍是旧峰值,
|
|
411
|
-
# 会让重试永远到上限、无法因"重试改善"提前退出;last_delta 反映当前尝试,可正确早退。
|
|
372
|
+
"max_delta": round(max(deltas), 3) if deltas else 0.0,
|
|
412
373
|
"last_delta": round(deltas[-1], 3) if deltas else 0.0,
|
|
413
374
|
}
|
|
414
|
-
stage_verify.stage_name = "verify"
|
|
415
375
|
|
|
416
376
|
|
|
417
377
|
def _reflect_policy(agent, context: dict) -> dict:
|
|
@@ -426,10 +386,11 @@ def _reflect_policy(agent, context: dict) -> dict:
|
|
|
426
386
|
if p is True:
|
|
427
387
|
p = {}
|
|
428
388
|
return {"enabled": True,
|
|
429
|
-
"max_retries": int(p.get("max_retries", 1)),
|
|
389
|
+
"max_retries": int(p.get("max_retries", 1)),
|
|
430
390
|
"drift_threshold": float(p.get("drift_threshold", 0.5))}
|
|
431
391
|
|
|
432
392
|
|
|
393
|
+
@named_stage("reflect")
|
|
433
394
|
async def stage_reflect(ctx: PipelineContext) -> None:
|
|
434
395
|
"""反思:读 verify 信号(max_delta),决定接受还是**有上限地**重试(Step 3)。
|
|
435
396
|
|
|
@@ -440,42 +401,29 @@ async def stage_reflect(ctx: PipelineContext) -> None:
|
|
|
440
401
|
policy = _reflect_policy(ctx.agent, ctx.context)
|
|
441
402
|
decision, retries = "accept", 0
|
|
442
403
|
if policy["enabled"]:
|
|
443
|
-
# 用 last_delta(当前尝试的最终漂移)作重试信号,而非 max_delta(累积峰值,单调不减)——
|
|
444
|
-
# 否则重试改善了也无法早退。重试若把漂移降到阈值内,下一轮 last_delta 达标即停止。
|
|
445
404
|
while (ctx.verified.get("last_delta", 0.0) > policy["drift_threshold"]
|
|
446
405
|
and retries < policy["max_retries"]):
|
|
447
|
-
# 协作式中断点(P3):每次重试都是一次 `_execute`(LLM 调用)——这里是本管线里最接近
|
|
448
|
-
# 范式"agentic loop 每轮检查中断"的地方。置位则停在下一轮 LLM 调用**之前**(省 token),
|
|
449
|
-
# 保留已有最好结果。opt-in:stop_event=None 恒不触发,重试行为逐字不变。
|
|
450
406
|
if _stop_requested(ctx):
|
|
451
|
-
# 中断:decision 统一记 "interrupted",已发生的重试次数由 retries 字段承载。
|
|
452
|
-
# (B2 审核 F1:此前分 interrupted/retry_interrupted 两值,其中 retries==0 一支仅在
|
|
453
|
-
# 外部线程于"pre-reflect 边界检查"与"首个 while 检查"之间置位才可达——单线程测试无法
|
|
454
|
-
# 稳定覆盖。合并为单值消除该未覆盖分支,信息量不减: retries 已表达重试了几次。)
|
|
455
407
|
ctx.interrupted = True
|
|
456
408
|
decision = "interrupted"
|
|
457
409
|
break
|
|
458
|
-
prev_result, prev_extra = ctx.result, ctx.extra
|
|
410
|
+
prev_result, prev_extra = ctx.result, ctx.extra
|
|
459
411
|
try:
|
|
460
412
|
retries += 1
|
|
461
413
|
decision = "retry"
|
|
462
414
|
ctx.result, ctx.extra = await ctx.agent._execute(
|
|
463
415
|
ctx.task, ctx.context, ctx.session_id, ctx.memories)
|
|
464
|
-
stage_verify(ctx)
|
|
416
|
+
stage_verify(ctx)
|
|
465
417
|
except Exception:
|
|
466
|
-
# 重试出错(含重跑被 BLOCK)→ 回滚到上一次好结果,停止重试(不让重试变差)
|
|
467
418
|
ctx.result, ctx.extra = prev_result, prev_extra
|
|
468
419
|
decision = "retry_reverted"
|
|
469
420
|
break
|
|
470
|
-
# 重试后 last_delta 已达标 → 视为改善接受
|
|
471
421
|
if decision == "retry" and ctx.verified.get("last_delta", 0.0) <= policy["drift_threshold"]:
|
|
472
422
|
decision = "retry_accepted"
|
|
473
423
|
ctx.reflect = {"decision": decision, "retries": retries,
|
|
474
424
|
"final_last_delta": ctx.verified.get("last_delta", 0.0),
|
|
475
425
|
"final_max_delta": ctx.verified.get("max_delta", 0.0)}
|
|
476
|
-
stage_reflect.stage_name = "reflect"
|
|
477
426
|
|
|
478
427
|
|
|
479
|
-
# 默认管线的 stage 序列(完整八阶段之七;persist=run_task 里的 emit + 领域 _store_memory)。
|
|
480
428
|
DEFAULT_STAGES = [stage_perceive, stage_recall, stage_plan, stage_route,
|
|
481
429
|
stage_act, stage_verify, stage_reflect]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""stage 的命名元数据。
|
|
2
|
+
|
|
3
|
+
pipeline 的两个模块都按「函数 + stage_name 属性」标注阶段,读侧用 getattr 兜底
|
|
4
|
+
(见 cognitive_pipeline 里取 name 的那处)。属性赋值 `fn.stage_name = ...` 在类型
|
|
5
|
+
检查里不成立 —— 函数对象的类型没有这个属性;setattr 是运行期真做的同一件事,
|
|
6
|
+
而且让「名字」与「定义」写在同一处,不会漏标。
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Callable, TypeVar
|
|
11
|
+
|
|
12
|
+
F = TypeVar("F", bound=Callable[..., object])
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def named_stage(name: str) -> Callable[[F], F]:
|
|
16
|
+
"""给 stage 可调用挂 stage_name(stage 事件与轨迹标注用这个键)。"""
|
|
17
|
+
def deco(fn: F) -> F:
|
|
18
|
+
vars(fn)["stage_name"] = name
|
|
19
|
+
return fn
|
|
20
|
+
|
|
21
|
+
return deco
|
|
@@ -16,11 +16,12 @@ hooks / 流式(run_task_stream)/ stop_event 中断 / 统一 `_emit_execution` /
|
|
|
16
16
|
import asyncio
|
|
17
17
|
from datetime import datetime
|
|
18
18
|
|
|
19
|
+
from ..core.dispatch import dispatch_failed
|
|
19
20
|
from .cognitive_pipeline import PipelineContext, stage_recall # noqa: F401 (recall 复用默认 stage)
|
|
20
|
-
from
|
|
21
|
+
from .stage_meta import named_stage
|
|
21
22
|
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
@named_stage("meta_check")
|
|
24
25
|
async def sv_meta_check(ctx: PipelineContext) -> None:
|
|
25
26
|
agent = ctx.agent
|
|
26
27
|
if not agent._is_meta(ctx.task):
|
|
@@ -30,7 +31,6 @@ async def sv_meta_check(ctx: PipelineContext) -> None:
|
|
|
30
31
|
node_type="decision", conf=0.92,
|
|
31
32
|
)
|
|
32
33
|
subagents = getattr(agent, "_subagents", {})
|
|
33
|
-
# 拓扑 5 → 3:助手/行政已下放为指令技能,不再出现在团队介绍里。
|
|
34
34
|
sub_names = {
|
|
35
35
|
"finance": "财务 Agent(理财、收支、预算、风控)",
|
|
36
36
|
"office": "办公学习 Agent(学习、知识管理、研究、写作、日程与行程)",
|
|
@@ -57,11 +57,10 @@ async def sv_meta_check(ctx: PipelineContext) -> None:
|
|
|
57
57
|
)
|
|
58
58
|
ctx.result = {"dispatched_to": "supervisor", "sub_result": reply,
|
|
59
59
|
"self_answered": True}
|
|
60
|
-
ctx.extra["_meta_answered"] = True
|
|
61
|
-
sv_meta_check.stage_name = "meta_check"
|
|
60
|
+
ctx.extra["_meta_answered"] = True
|
|
62
61
|
|
|
63
62
|
|
|
64
|
-
|
|
63
|
+
@named_stage("schedule")
|
|
65
64
|
async def sv_schedule(ctx: PipelineContext) -> None:
|
|
66
65
|
if ctx.extra.get("_meta_answered"):
|
|
67
66
|
return
|
|
@@ -73,10 +72,9 @@ async def sv_schedule(ctx: PipelineContext) -> None:
|
|
|
73
72
|
+ (f" · 抢占({ctx.sched.preemption_reason})" if ctx.sched.preemptible else "")),
|
|
74
73
|
node_type="decision", conf=0.9,
|
|
75
74
|
)
|
|
76
|
-
sv_schedule.stage_name = "schedule"
|
|
77
75
|
|
|
78
76
|
|
|
79
|
-
|
|
77
|
+
@named_stage("route")
|
|
80
78
|
async def sv_route(ctx: PipelineContext) -> None:
|
|
81
79
|
if ctx.extra.get("_meta_answered"):
|
|
82
80
|
return
|
|
@@ -84,11 +82,6 @@ async def sv_route(ctx: PipelineContext) -> None:
|
|
|
84
82
|
target = agent._route(ctx.task, ctx.sched)
|
|
85
83
|
ctx.target = target
|
|
86
84
|
ctx.route = {"target": target, "reason": "主管关键词+调度意图分诊"}
|
|
87
|
-
# 无关键词命中 = 正面对话/泛化问题 → 主管**直接自答一次**,省掉"LLM 分诊 + 派发"的
|
|
88
|
-
# 第二次串行 LLM 调用(每次约 5s 网络往返,正面对话不值当)。只有命中领域关键词
|
|
89
|
-
# (财务/行政/办公/…)才派发给对应子 Agent。stub 下保持原路径(零额外调用、离线测试确定)。
|
|
90
|
-
# 权衡:少数本该派给领域 Agent 的"无关键词"任务会由主管自答——用户明确选速度优先。
|
|
91
|
-
# (本 stage 只做**决策**置标记;自答的 LLM 调用在 dispatch stage 执行。)
|
|
92
85
|
if not agent._has_keyword_hit(ctx.task) and not agent.router.is_stub:
|
|
93
86
|
agent._think(
|
|
94
87
|
content="无关键词命中 → 主管直接自答(省掉二次分诊派发,正面对话提速)",
|
|
@@ -100,16 +93,13 @@ async def sv_route(ctx: PipelineContext) -> None:
|
|
|
100
93
|
node_type="decision",
|
|
101
94
|
conf=0.9,
|
|
102
95
|
)
|
|
103
|
-
sv_route.stage_name = "route"
|
|
104
96
|
|
|
105
97
|
|
|
106
|
-
|
|
98
|
+
@named_stage("plan")
|
|
107
99
|
async def sv_plan(ctx: PipelineContext) -> None:
|
|
108
100
|
if ctx.extra.get("_meta_answered") or ctx.extra.get("_self_answer"):
|
|
109
101
|
return
|
|
110
102
|
agent = ctx.agent
|
|
111
|
-
# P0-1:LLM 规划含同步网络调用(call_tools),放线程池避免阻塞事件循环;
|
|
112
|
-
# 规则拆解路径是纯 CPU,放线程池开销可忽略、行为不变。
|
|
113
103
|
plan_obj, step_routes = await asyncio.to_thread(agent._plan_task, ctx.task, ctx.sched)
|
|
114
104
|
if plan_obj is not None:
|
|
115
105
|
routes_desc = " → ".join(f"{name}[{ag}]" for name, ag in step_routes)
|
|
@@ -117,23 +107,20 @@ async def sv_plan(ctx: PipelineContext) -> None:
|
|
|
117
107
|
content=f"planner 判定为多步任务({len(plan_obj.tasks)}步):{routes_desc}",
|
|
118
108
|
node_type="decision", conf=0.85,
|
|
119
109
|
)
|
|
120
|
-
# 若子任务跨不同 Agent,标记为多 Agent 顺序执行
|
|
121
110
|
distinct_agents = {ag for _, ag in step_routes}
|
|
122
111
|
ctx.multi_agent = len(distinct_agents) > 1
|
|
123
112
|
else:
|
|
124
113
|
ctx.multi_agent = False
|
|
125
114
|
ctx.plan_obj = plan_obj
|
|
126
115
|
ctx.step_routes = step_routes or []
|
|
127
|
-
sv_plan.stage_name = "plan"
|
|
128
116
|
|
|
129
117
|
|
|
130
|
-
|
|
118
|
+
@named_stage("dispatch")
|
|
131
119
|
async def sv_dispatch(ctx: PipelineContext) -> None:
|
|
132
120
|
agent = ctx.agent
|
|
133
121
|
if ctx.extra.get("_meta_answered"):
|
|
134
122
|
return
|
|
135
123
|
|
|
136
|
-
# 主管自答(route stage 的决策在此执行)
|
|
137
124
|
if ctx.extra.get("_self_answer"):
|
|
138
125
|
_msgs = [{"role": "user", "content": (
|
|
139
126
|
f"你是 Foxclaw 的主管 Agent(Supervisor),统筹整个智能体团队。"
|
|
@@ -141,9 +128,6 @@ async def sv_dispatch(ctx: PipelineContext) -> None:
|
|
|
141
128
|
f"问题作答,不要只做自我介绍、不要答非所问。若确属需要专门领域深办的任务,可"
|
|
142
129
|
f"简要引导用户去对应能力,但仍先给出有用的直接回应。"
|
|
143
130
|
)}]
|
|
144
|
-
# 联网增强:新闻/实时类(_web_augment 内 needs_search 命中)走百度搜索注入真实结果
|
|
145
|
-
# 再作答,避免主管凭空编造(系统 prompt 明令严禁模拟搜索)。纯闲聊 needs_search=False,
|
|
146
|
-
# 不搜、不减速。阻塞 I/O 放线程池(与 base._execute 同法),不堵 async 事件循环。
|
|
147
131
|
_msgs = await asyncio.to_thread(agent._web_augment, ctx.task, _msgs)
|
|
148
132
|
reply = await agent._call_llm(_msgs)
|
|
149
133
|
agent._store_memory(
|
|
@@ -157,7 +141,6 @@ async def sv_dispatch(ctx: PipelineContext) -> None:
|
|
|
157
141
|
|
|
158
142
|
subagents = getattr(agent, "_subagents", {})
|
|
159
143
|
|
|
160
|
-
# 多 Agent 执行:并行计划(无依赖)→ fork-join 并发;否则顺序执行后汇聚
|
|
161
144
|
if ctx.multi_agent and ctx.step_routes:
|
|
162
145
|
if getattr(ctx.plan_obj, "flow_type", "sequential") == "parallel":
|
|
163
146
|
agent._think(
|
|
@@ -173,58 +156,46 @@ async def sv_dispatch(ctx: PipelineContext) -> None:
|
|
|
173
156
|
subagents, ctx.context, ctx.memories)
|
|
174
157
|
return
|
|
175
158
|
|
|
176
|
-
# 单 Agent 分发
|
|
177
159
|
if ctx.target in subagents:
|
|
178
160
|
ctx.dispatch_result = await agent._dispatch_single(
|
|
179
161
|
ctx.task, ctx.target, ctx.sched, ctx.session_id,
|
|
180
162
|
subagents, ctx.context, ctx.memories, ctx.plan_obj)
|
|
181
163
|
return
|
|
182
164
|
|
|
183
|
-
# 子 Agent 不可用(未注册)→ 主管自处理
|
|
184
165
|
ctx.dispatch_result = await agent._dispatch_fallback(ctx.task, ctx.context)
|
|
185
|
-
sv_dispatch.stage_name = "dispatch"
|
|
186
166
|
|
|
187
167
|
|
|
188
|
-
|
|
168
|
+
@named_stage("replan")
|
|
189
169
|
async def sv_replan(ctx: PipelineContext) -> None:
|
|
190
170
|
if ctx.extra.get("_meta_answered") or ctx.extra.get("_self_answer"):
|
|
191
171
|
return
|
|
192
172
|
agent = ctx.agent
|
|
193
173
|
dr = ctx.dispatch_result or {}
|
|
194
174
|
if not dispatch_failed(dr):
|
|
195
|
-
return
|
|
175
|
+
return
|
|
196
176
|
if not agent._replan_enabled():
|
|
197
|
-
return
|
|
177
|
+
return
|
|
198
178
|
subagents = getattr(agent, "_subagents", {})
|
|
199
179
|
max_rounds = int((ctx.context or {}).get("replan_max_rounds", 2))
|
|
200
180
|
ctx.dispatch_result = await agent._replan_and_redo(
|
|
201
181
|
ctx.task, ctx.sched, ctx.session_id, subagents, ctx.context, ctx.memories,
|
|
202
182
|
dr, max_rounds=max_rounds)
|
|
203
|
-
sv_replan.stage_name = "replan"
|
|
204
183
|
|
|
205
184
|
|
|
206
|
-
|
|
185
|
+
@named_stage("aggregate")
|
|
207
186
|
async def sv_aggregate(ctx: PipelineContext) -> None:
|
|
208
|
-
# 清除内部短路标记,不外泄到返回体(它们会经 **ctx.extra 透传进 resp)
|
|
209
187
|
ctx.extra.pop("_meta_answered", None)
|
|
210
188
|
ctx.extra.pop("_self_answer", None)
|
|
211
189
|
dr = ctx.dispatch_result or {}
|
|
212
190
|
if not dr:
|
|
213
|
-
return
|
|
191
|
+
return
|
|
214
192
|
ctx.result = dr.get("result", ctx.result)
|
|
215
|
-
# 白名单透传(与原主管返回体逐字一致的顶层键);session_id/agent/thought_summary
|
|
216
|
-
# 由 base 模板统一组装,不从 dispatch_result 取。
|
|
217
193
|
for k in ("schedule", "plan", "process_id"):
|
|
218
194
|
if k in dr:
|
|
219
195
|
ctx.extra[k] = dr[k]
|
|
220
|
-
# 「未完成 ⇒ 不作为已完成真值沉淀」:任一子任务被 Harness 拦截 → 提升为顶层
|
|
221
|
-
# status=blocked(与 base 模板的 skill_status 提升同一机制),执行事件 blocked=True,
|
|
222
|
-
# 真值链不沉淀(呼应 P3-5)。成功/自答路径不带此键,行为逐字不变。
|
|
223
196
|
if isinstance(ctx.result, dict) and ctx.result.get("blocked"):
|
|
224
197
|
ctx.extra["skill_status"] = "blocked"
|
|
225
|
-
sv_aggregate.stage_name = "aggregate"
|
|
226
198
|
|
|
227
199
|
|
|
228
|
-
# 主管 pipeline 的 stage 序列(recall 复用默认实现:与旧 `self._recall_memory(task)` 逐字等价)
|
|
229
200
|
SUPERVISOR_STAGES = [stage_recall, sv_meta_check, sv_schedule, sv_route,
|
|
230
201
|
sv_plan, sv_dispatch, sv_replan, sv_aggregate]
|
|
@@ -152,9 +152,9 @@ def _invoke_plan_tool(router, system: str, user_content: str, agent_catalog: dic
|
|
|
152
152
|
return None
|
|
153
153
|
calls = res.get("tool_calls") or []
|
|
154
154
|
if not calls:
|
|
155
|
-
return None
|
|
155
|
+
return None
|
|
156
156
|
if calls[0].get("args_parse_failed"):
|
|
157
|
-
return None
|
|
157
|
+
return None
|
|
158
158
|
args = calls[0].get("args") or {}
|
|
159
159
|
steps = args.get("steps")
|
|
160
160
|
if not isinstance(steps, list):
|
|
@@ -168,7 +168,7 @@ def _invoke_plan_tool(router, system: str, user_content: str, agent_catalog: dic
|
|
|
168
168
|
continue
|
|
169
169
|
agent = str(s.get("agent") or "office").strip()
|
|
170
170
|
if agent not in agent_catalog:
|
|
171
|
-
agent = "office"
|
|
171
|
+
agent = "office"
|
|
172
172
|
dep = s.get("depends_on") or []
|
|
173
173
|
if not isinstance(dep, list):
|
|
174
174
|
dep = []
|
|
@@ -11,14 +11,11 @@
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
import re
|
|
13
13
|
from dataclasses import dataclass, field
|
|
14
|
+
from typing import Optional
|
|
14
15
|
|
|
15
|
-
# ── 单一真源:否定词 + 顺序/并行/列表 标记 ──────────────────────
|
|
16
16
|
NEGATIONS = ["不", "别", "无需", "不要", "取消", "甭", "don't", "no need", "without"]
|
|
17
|
-
# 顺序拆分信号(明确的顺序连接词;不用"和/并"等高风险常用字,避免误拆名词短语)
|
|
18
17
|
SPLIT_MARKERS = ["然后", "接着", "之后", "再", "最后", ";", ";"]
|
|
19
|
-
# 并行信号:出现这些"同时性"连接词 → 子任务彼此无依赖,可并行 fork
|
|
20
18
|
PARALLEL_TRIGGERS = ["同时", "分别", "并行", "一并"]
|
|
21
|
-
# 列表分隔符(仅在检出并行触发词时才当拆分点,避免误拆普通名词短语)
|
|
22
19
|
LIST_SEPS = ["、", ",", ",", ";", ";"]
|
|
23
20
|
|
|
24
21
|
|
|
@@ -55,15 +52,15 @@ class ScoredCandidate:
|
|
|
55
52
|
|
|
56
53
|
@dataclass
|
|
57
54
|
class TriggerRoute:
|
|
58
|
-
primary: str = None
|
|
55
|
+
primary: Optional[str] = None
|
|
59
56
|
confidence: float = 0.0
|
|
60
|
-
candidates: list = field(default_factory=list)
|
|
61
|
-
fallback: str = None
|
|
57
|
+
candidates: list = field(default_factory=list)
|
|
58
|
+
fallback: Optional[str] = None
|
|
62
59
|
needs_composition: bool = False
|
|
63
60
|
reason: str = ""
|
|
64
61
|
|
|
65
62
|
|
|
66
|
-
def trigger_route(task: str, candidates: list, fallback_id: str = None,
|
|
63
|
+
def trigger_route(task: str, candidates: list, fallback_id: str | None = None,
|
|
67
64
|
min_conf: float = 0.15, near_gap: float = 0.1) -> TriggerRoute:
|
|
68
65
|
"""触发词路由三步打分:否定过滤后的触发命中(0.8)+ 描述领域匹配(0.2),确定性排序。
|
|
69
66
|
这是 AHS 原 route 的算法,现作为三处共用的通用引擎。"""
|
|
@@ -75,17 +72,15 @@ def trigger_route(task: str, candidates: list, fallback_id: str = None,
|
|
|
75
72
|
triggers = c.triggers or []
|
|
76
73
|
if not triggers:
|
|
77
74
|
scored.append((c.id, 0.0)); continue
|
|
78
|
-
# 第1步:否定过滤后的触发命中数 → 命中即给强基础分,不被触发词总数稀释
|
|
79
75
|
hits = keyword_hits(task, triggers, negation=True)
|
|
80
76
|
hit_score = 0.0 if hits == 0 else min(0.6 + 0.15 * (hits - 1), 1.0)
|
|
81
|
-
# 第2步:领域匹配(描述里的词出现在任务中,粗略语义近似)
|
|
82
77
|
desc_words = [w for w in re.split(r"[\s,,、]", c.description or "") if len(w) >= 2]
|
|
83
78
|
domain_hit = sum(1 for w in desc_words if w in task)
|
|
84
79
|
domain_score = min(domain_hit / max(len(desc_words), 1), 1.0)
|
|
85
80
|
score = 0.8 * hit_score + 0.2 * domain_score
|
|
86
81
|
scored.append((c.id, round(score, 3)))
|
|
87
82
|
|
|
88
|
-
scored.sort(key=lambda x: (-x[1], x[0]))
|
|
83
|
+
scored.sort(key=lambda x: (-x[1], x[0]))
|
|
89
84
|
top_id, top_score = scored[0]
|
|
90
85
|
cands = [(i, s) for i, s in scored if s > 0]
|
|
91
86
|
|