foxclaw 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +1 -8
- package/package.json +1 -1
- package/python/.env.example +8 -0
- package/python/.github/workflows/ci.yml +47 -2
- package/python/CHANGELOG.md +57 -0
- package/python/CHANGELOG_CN.md +57 -0
- package/python/README.md +11 -2
- package/python/README_CN.md +32 -20
- package/python/bench/bm25_audit.py +1 -14
- package/python/bench/bm25_regression.py +0 -1
- package/python/bench/bm25_stress.py +0 -4
- package/python/bench/cd_audit.py +0 -23
- package/python/bench/chaos.py +6 -8
- package/python/bench/e2e_real_models.py +0 -1
- package/python/bench/llm_retry_test.py +2 -9
- package/python/bench/load_test.py +5 -8
- package/python/bench/vector_ivf_upgrade.py +1 -7
- package/python/docs/RELEASE.md +4 -3
- package/python/docs/RUN_CN.md +2 -2
- package/python/evals/governance/cases.py +0 -37
- package/python/evals/governance/run_evals.py +5 -37
- package/python/examples/governance_middleware_demo.py +1 -6
- package/python/examples/langgraph_governance_demo.py +7 -11
- package/python/foxclaw/__init__.py +34 -152
- package/python/foxclaw/agents/base.py +65 -136
- package/python/foxclaw/agents/impl/_helpers.py +2 -10
- package/python/foxclaw/agents/impl/finance.py +4 -21
- package/python/foxclaw/agents/impl/office.py +7 -24
- package/python/foxclaw/agents/impl/supervisor.py +12 -79
- package/python/foxclaw/agents/mixins/_context.py +0 -23
- package/python/foxclaw/agents/mixins/augment.py +10 -16
- package/python/foxclaw/agents/mixins/llm.py +7 -6
- package/python/foxclaw/agents/mixins/memory.py +7 -1
- package/python/foxclaw/agents/mixins/preference.py +58 -8
- package/python/foxclaw/agents/mixins/prompt.py +0 -6
- package/python/foxclaw/agents/mixins/skillbox.py +6 -6
- package/python/foxclaw/agents/mixins/tool_exec.py +16 -42
- package/python/foxclaw/api/__init__.py +18 -77
- package/python/foxclaw/api/_state.py +43 -0
- package/python/foxclaw/api/{app.py → bootstrap.py} +5 -32
- package/python/foxclaw/api/middleware.py +5 -28
- package/python/foxclaw/api/routes/ahs.py +60 -73
- package/python/foxclaw/api/routes/attachments.py +5 -6
- package/python/foxclaw/api/routes/browser.py +7 -11
- package/python/foxclaw/api/routes/domain.py +18 -26
- package/python/foxclaw/api/routes/knowledge.py +3 -21
- package/python/foxclaw/api/routes/llm_admin.py +21 -33
- package/python/foxclaw/api/routes/memory.py +28 -12
- package/python/foxclaw/api/routes/orchestration.py +3 -5
- package/python/foxclaw/api/routes/recovery.py +4 -3
- package/python/foxclaw/api/routes/review.py +2 -11
- package/python/foxclaw/api/routes/search.py +3 -7
- package/python/foxclaw/api/routes/system.py +33 -49
- package/python/foxclaw/api/routes/task.py +31 -49
- package/python/foxclaw/api/routes/ws.py +2 -6
- package/python/foxclaw/api/spine.py +1 -4
- package/python/foxclaw/browser/action/action_executor.py +1 -5
- package/python/foxclaw/browser/action/action_types.py +0 -1
- package/python/foxclaw/browser/action/url_policy.py +1 -5
- package/python/foxclaw/browser/adapter/web_adapter.py +1 -1
- package/python/foxclaw/browser/driver/playwright_driver.py +20 -16
- package/python/foxclaw/browser/perception/dom_extractor.py +0 -1
- package/python/foxclaw/browser/perception/element_indexer.py +1 -2
- package/python/foxclaw/browser/service.py +7 -11
- package/python/foxclaw/capability/embedding_service.py +4 -6
- package/python/foxclaw/channels/__init__.py +9 -11
- package/python/foxclaw/channels/discord.py +4 -13
- package/python/foxclaw/channels/dispatcher.py +2 -27
- package/python/foxclaw/channels/feishu.py +10 -20
- package/python/foxclaw/channels/openclaw_bridge.py +4 -9
- package/python/foxclaw/channels/signing.py +9 -9
- package/python/foxclaw/channels/telegram.py +3 -4
- package/python/foxclaw/channels/weixin.py +7 -17
- package/python/foxclaw/cli.py +42 -13
- package/python/foxclaw/cognitive/reasoner.py +9 -10
- package/python/foxclaw/cognitive/state.py +18 -24
- package/python/foxclaw/core/audit_chain.py +1 -3
- package/python/foxclaw/core/audit_sink.py +3 -3
- package/python/foxclaw/core/compaction.py +8 -26
- package/python/foxclaw/core/config.py +2 -2
- package/python/foxclaw/core/confirm_nonce.py +6 -12
- package/python/foxclaw/core/envfile.py +2 -2
- package/python/foxclaw/core/failure_policy.py +17 -26
- package/python/foxclaw/core/gateway.py +6 -10
- package/python/foxclaw/core/governance_audit.py +3 -7
- package/python/foxclaw/core/idempotency.py +15 -22
- package/python/foxclaw/core/injection_guard.py +10 -29
- package/python/foxclaw/core/memory_adapter.py +30 -57
- package/python/foxclaw/core/metrics.py +4 -5
- package/python/foxclaw/core/model_router.py +36 -148
- package/python/foxclaw/core/paths.py +1 -1
- package/python/foxclaw/core/recovery.py +21 -34
- package/python/foxclaw/core/scheduler.py +23 -26
- package/python/foxclaw/core/secrets.py +0 -3
- package/python/foxclaw/core/sessions.py +4 -6
- package/python/foxclaw/core/settings.py +16 -32
- package/python/foxclaw/core/shared_write.py +11 -23
- package/python/foxclaw/core/steering_queue.py +12 -11
- package/python/foxclaw/core/system_bus.py +1 -1
- package/python/foxclaw/core/thought_chain.py +30 -32
- package/python/foxclaw/core/token_meter.py +9 -13
- package/python/foxclaw/core/tools.py +4 -12
- package/python/foxclaw/core/tracing.py +25 -40
- package/python/foxclaw/core/truth_pipeline.py +5 -9
- package/python/foxclaw/core/update_check.py +149 -0
- package/python/foxclaw/core/vec_recall_adapter.py +11 -37
- package/python/foxclaw/core/verify.py +6 -14
- package/python/foxclaw/core/web_search.py +4 -28
- package/python/foxclaw/finance/evidence.py +14 -18
- package/python/foxclaw/finance/fx.py +1 -3
- package/python/foxclaw/finance/graph.py +9 -18
- package/python/foxclaw/finance/models.py +20 -24
- package/python/foxclaw/finance/nlparse.py +12 -72
- package/python/foxclaw/finance/policy.py +8 -36
- package/python/foxclaw/finance/reasoner.py +3 -11
- package/python/foxclaw/finance/reasoning.py +13 -34
- package/python/foxclaw/finance/reconcile.py +5 -10
- package/python/foxclaw/finance/runtime.py +19 -46
- package/python/foxclaw/governance/middleware.py +13 -21
- package/python/foxclaw/harness/_base.py +2 -8
- package/python/foxclaw/harness/_policy.py +19 -38
- package/python/foxclaw/harness/drift.py +1 -13
- package/python/foxclaw/harness/event_bus.py +2 -5
- package/python/foxclaw/harness/harness.py +9 -97
- package/python/foxclaw/harness/loop_detection.py +11 -18
- package/python/foxclaw/itinerary/amap_client.py +2 -5
- package/python/foxclaw/itinerary/models.py +7 -7
- package/python/foxclaw/itinerary/reasoner.py +1 -6
- package/python/foxclaw/itinerary/reasoning.py +15 -21
- package/python/foxclaw/itinerary/runtime.py +8 -12
- package/python/foxclaw/kernel/capabilities.py +5 -6
- package/python/foxclaw/kernel/policy_gate.py +4 -10
- package/python/foxclaw/kernel/process_manager.py +17 -36
- package/python/foxclaw/kernel/scheduler.py +3 -9
- package/python/foxclaw/kernel/sentinel.py +13 -23
- package/python/foxclaw/kernel/sentinel_client.py +12 -15
- package/python/foxclaw/kernel/spine.py +9 -9
- package/python/foxclaw/knowledge/__init__.py +4 -20
- package/python/foxclaw/knowledge/truth_crystallizer.py +5 -8
- package/python/foxclaw/learning/knowledge_graph.py +4 -9
- package/python/foxclaw/learning/models.py +13 -13
- package/python/foxclaw/learning/reasoner.py +1 -4
- package/python/foxclaw/learning/runtime.py +19 -31
- package/python/foxclaw/learning/spaced_repetition.py +2 -7
- package/python/foxclaw/mcp_server.py +2 -3
- package/python/foxclaw/orchestration/__init__.py +16 -33
- package/python/foxclaw/pipeline/cognitive_pipeline.py +45 -97
- package/python/foxclaw/pipeline/stage_meta.py +21 -0
- package/python/foxclaw/pipeline/supervisor_stages.py +13 -42
- package/python/foxclaw/planning/llm_planner.py +3 -3
- package/python/foxclaw/planning/planner.py +6 -11
- package/python/foxclaw/search/agent_port.py +7 -11
- package/python/foxclaw/search/evidence.py +0 -4
- package/python/foxclaw/search/internal_search.py +3 -5
- package/python/foxclaw/search/models.py +11 -13
- package/python/foxclaw/search/planner.py +3 -3
- package/python/foxclaw/search/retriever.py +1 -1
- package/python/foxclaw/search/runtime.py +1 -8
- package/python/foxclaw/skills/__init__.py +46 -100
- package/python/foxclaw/skills/archive.py +111 -0
- package/python/foxclaw/skills/fetch.py +226 -0
- package/python/foxclaw/skills/hooks_spec.py +21 -25
- package/python/foxclaw/skills/install_cmd.py +216 -0
- package/python/foxclaw/skills/lifecycle.py +16 -42
- package/python/foxclaw/skills/markdown_skill.py +10 -9
- package/python/foxclaw/skills/mcp_client.py +5 -11
- package/python/foxclaw/skills/mcp_store.py +0 -3
- package/python/foxclaw/skills/permission.py +13 -48
- package/python/foxclaw/skills/pipeline.py +21 -53
- package/python/foxclaw/skills/policy.py +4 -10
- package/python/foxclaw/skills/review.py +3 -3
- package/python/frontend/deck/auth.js +0 -7
- package/python/frontend/deck/chat_model_picker.js +0 -54
- package/python/frontend/deck/domain.js +0 -20
- package/python/frontend/deck/main.js +0 -144
- package/python/frontend/deck/markdown.js +0 -4
- package/python/frontend/deck/settings.js +1 -18
- package/python/frontend/deck/state.js +0 -1
- package/python/frontend/deck/util.js +0 -2
- package/python/frontend/deck/visuals.js +0 -22
- package/python/frontend/planet/planet.js +0 -201
- package/python/frontend/v3/assets/index-CQSBdxk4.js +34 -0
- package/python/frontend/v3/assets/index-D5nIsjms.css +1 -0
- package/python/frontend/v3/index.html +3 -3
- package/python/frontend/widget_runtime.js +0 -43
- package/python/frontend/widgets.js +0 -57
- package/python/frontend_src/index.html +1 -1
- package/python/frontend_src/package-lock.json +2 -2
- package/python/frontend_src/package.json +1 -1
- package/python/frontend_src/src/App.vue +3 -7
- package/python/frontend_src/src/api.js +0 -7
- package/python/frontend_src/src/components/ChatDock.vue +27 -39
- package/python/frontend_src/src/components/DocViewer.vue +0 -9
- package/python/frontend_src/src/components/Inspector.vue +0 -6
- package/python/frontend_src/src/components/LeftRail.vue +60 -39
- package/python/frontend_src/src/components/MenuPopover.vue +9 -11
- package/python/frontend_src/src/components/MessageStream.vue +18 -30
- package/python/frontend_src/src/components/ModelPicker.vue +0 -5
- package/python/frontend_src/src/components/QuickIngest.vue +0 -6
- package/python/frontend_src/src/components/Stage.vue +71 -25
- package/python/frontend_src/src/components/ToolMenu.vue +0 -3
- package/python/frontend_src/src/components/TopBar.vue +38 -10
- package/python/frontend_src/src/components/panels/ActivityPanel.vue +0 -2
- package/python/frontend_src/src/components/panels/AgentsPanel.vue +2 -5
- package/python/frontend_src/src/components/panels/KnowledgePanel.vue +0 -16
- package/python/frontend_src/src/components/panels/MemoryPanel.vue +0 -1
- package/python/frontend_src/src/components/panels/PanelBox.vue +6 -7
- package/python/frontend_src/src/components/panels/ReviewPanel.vue +0 -16
- package/python/frontend_src/src/components/panels/SettingsPanel.vue +142 -38
- package/python/frontend_src/src/components/panels/TasksPanel.vue +0 -1
- package/python/frontend_src/src/components/panels/ToolsPanel.vue +125 -19
- package/python/frontend_src/src/lib/fmt.js +0 -4
- package/python/frontend_src/src/lib/i18n.js +53 -0
- package/python/frontend_src/src/lib/locales/en.js +173 -0
- package/python/frontend_src/src/lib/locales/zh.js +174 -0
- package/python/frontend_src/src/lib/md.js +0 -14
- package/python/frontend_src/src/main.js +2 -0
- package/python/frontend_src/src/stores/app.js +17 -32
- package/python/frontend_src/src/stores/chat.js +0 -34
- package/python/frontend_src/src/stores/model.js +0 -8
- package/python/frontend_src/src/stores/panels.js +0 -41
- package/python/frontend_src/src/stores/profile.js +31 -0
- package/python/frontend_src/src/stores/system.js +25 -0
- package/python/frontend_src/vite.config.js +0 -7
- package/python/pyproject.toml +6 -3
- package/python/requirements.txt +311 -0
- package/python/tests/conftest.py +0 -22
- package/python/tests/run_all.py +3 -66
- package/python/tests/test_agent_lifecycle.py +4 -38
- package/python/tests/test_ahs_p2.py +4 -25
- package/python/tests/test_anthropic_tools.py +1 -15
- package/python/tests/test_api.py +19 -9
- package/python/tests/test_api_auth.py +1 -5
- package/python/tests/test_attachments.py +9 -25
- package/python/tests/test_audit_chain.py +2 -7
- package/python/tests/test_audit_sink.py +0 -6
- package/python/tests/test_browser.py +2 -6
- package/python/tests/test_browser_request_guard.py +0 -4
- package/python/tests/test_capabilities.py +0 -6
- package/python/tests/test_channel_signing.py +2 -9
- package/python/tests/test_channels.py +1 -11
- package/python/tests/test_config_layering.py +0 -4
- package/python/tests/test_confirm_nonce.py +1 -12
- package/python/tests/test_context_compaction_wiring.py +1 -7
- package/python/tests/test_dispatch.py +0 -6
- package/python/tests/test_domain_evals.py +11 -80
- package/python/tests/test_domain_import.py +0 -6
- package/python/tests/test_domain_reasoners.py +0 -14
- package/python/tests/test_e2e.py +0 -17
- package/python/tests/test_emb_cache.py +7 -17
- package/python/tests/test_embedding_singleton.py +1 -3
- package/python/tests/test_evals_smoke.py +0 -24
- package/python/tests/test_event_spine.py +10 -18
- package/python/tests/test_evidence_semantic.py +1 -14
- package/python/tests/test_evidence_sse.py +3 -6
- package/python/tests/test_file_ingest.py +0 -11
- package/python/tests/test_finance.py +0 -9
- package/python/tests/test_finance_ledger.py +5 -12
- package/python/tests/test_finance_persist.py +8 -12
- package/python/tests/test_gateway.py +3 -22
- package/python/tests/test_governance.py +5 -52
- package/python/tests/test_governance_audit.py +4 -39
- package/python/tests/test_governance_middleware.py +1 -7
- package/python/tests/test_hkc.py +6 -24
- package/python/tests/test_hkc_index_build_manager.py +0 -33
- package/python/tests/test_hkc_manifest_reuse.py +4 -27
- package/python/tests/test_hkc_search_event_sync.py +3 -54
- package/python/tests/test_import_path_guard.py +0 -6
- package/python/tests/test_injection_guard.py +18 -36
- package/python/tests/test_integration.py +5 -29
- package/python/tests/test_itinerary.py +0 -3
- package/python/tests/test_kee_semantic.py +0 -5
- package/python/tests/test_kernel.py +1 -11
- package/python/tests/test_learning.py +3 -12
- package/python/tests/test_llm_admin.py +4 -19
- package/python/tests/test_llm_planner.py +1 -23
- package/python/tests/test_markdown_skills.py +3 -14
- package/python/tests/test_markdown_skills_openclaw.py +4 -9
- package/python/tests/test_mcp_connectors.py +3 -15
- package/python/tests/test_mcp_guardian_discovery.py +2 -6
- package/python/tests/test_mcp_no_memory.py +3 -3
- package/python/tests/test_mcp_policy_gate.py +0 -1
- package/python/tests/test_mcp_server.py +6 -9
- package/python/tests/test_memory_recall.py +1 -4
- package/python/tests/test_metrics.py +0 -8
- package/python/tests/test_multi_chain.py +1 -13
- package/python/tests/test_multimodal.py +5 -18
- package/python/tests/test_negotiation.py +3 -13
- package/python/tests/test_orchestration.py +1 -11
- package/python/tests/test_orchestration_decoupled.py +0 -6
- package/python/tests/test_p3_p5_hardening.py +3 -17
- package/python/tests/test_parallel_dispatch.py +4 -12
- package/python/tests/test_pipeline.py +17 -58
- package/python/tests/test_plan_visible.py +4 -4
- package/python/tests/test_planner.py +0 -7
- package/python/tests/test_policy_gate.py +2 -3
- package/python/tests/test_profile.py +67 -0
- package/python/tests/test_readme_claims.py +163 -0
- package/python/tests/test_reasoner.py +2 -17
- package/python/tests/test_release_check.py +2 -13
- package/python/tests/test_resume_checkpoint.py +7 -15
- package/python/tests/test_routing.py +2 -12
- package/python/tests/test_scheduler.py +7 -10
- package/python/tests/test_search.py +1 -11
- package/python/tests/test_secrets.py +4 -10
- package/python/tests/test_semantic.py +1 -10
- package/python/tests/test_sessions.py +3 -4
- package/python/tests/test_settings.py +2 -18
- package/python/tests/test_shadow_learning.py +3 -43
- package/python/tests/test_shared_write.py +0 -7
- package/python/tests/test_skill_taint_gate.py +1 -10
- package/python/tests/test_skills.py +2 -18
- package/python/tests/test_skills_fetch.py +282 -0
- package/python/tests/test_stats_and_events.py +2 -5
- package/python/tests/test_streaming.py +2 -7
- package/python/tests/test_supervisor_pipeline.py +4 -5
- package/python/tests/test_supervisor_triage.py +4 -10
- package/python/tests/test_taint.py +2 -7
- package/python/tests/test_task_stream.py +1 -12
- package/python/tests/test_thinking_sse.py +0 -1
- package/python/tests/test_tool_center.py +1 -17
- package/python/tests/test_tool_parallel.py +2 -2
- package/python/tests/test_tool_params.py +1 -5
- package/python/tests/test_tool_use.py +0 -13
- package/python/tests/test_toplevel_concurrency.py +3 -4
- package/python/tests/test_tracing.py +7 -41
- package/python/tests/test_truth_crystallizer.py +4 -6
- package/python/tests/test_truth_flow.py +2 -7
- package/python/tests/test_update_check.py +95 -0
- package/python/tests/test_verify_evidence.py +8 -21
- package/python/tests/test_version_sync.py +35 -6
- package/python/tests/test_web_trigger.py +0 -17
- package/python/tools/check_faiss_type.py +0 -1
- package/python/tools/check_log.ps1 +0 -8
- package/python/tools/mypy_baseline.txt +12 -4
- package/python/tools/mypy_ratchet.py +52 -6
- package/python/tools/pack.py +0 -5
- package/python/tools/release_check.py +7 -7
- package/python/tools/restart_foxclaw.ps1 +5 -8
- package/python/tools/shared_write_audit.py +1 -15
- package/python/tools/size_check.py +1 -1
- package/python/tools/strip_comments.py +228 -0
- package/python/frontend/v3/assets/index-BD7VPbMV.js +0 -33
- package/python/frontend/v3/assets/index-CAvnYWGH.css +0 -1
|
@@ -22,14 +22,14 @@ RequestStatus = Literal["pending", "approved", "rejected", "needs_confirmation"]
|
|
|
22
22
|
class CrossAgentRequest:
|
|
23
23
|
"""跨 Agent 请求 — 编排的基本单元。
|
|
24
24
|
一个 Agent 向另一个发起请求,目标 Agent 用自己的守门人裁决。"""
|
|
25
|
-
from_agent: str
|
|
26
|
-
to_agent: str
|
|
25
|
+
from_agent: str
|
|
26
|
+
to_agent: str
|
|
27
27
|
request_type: RequestType
|
|
28
|
-
payload: dict
|
|
28
|
+
payload: dict
|
|
29
29
|
reason: str = ""
|
|
30
30
|
status: RequestStatus = "pending"
|
|
31
|
-
verdict: str = ""
|
|
32
|
-
negotiation: list = field(default_factory=list)
|
|
31
|
+
verdict: str = ""
|
|
32
|
+
negotiation: list = field(default_factory=list)
|
|
33
33
|
id: str = field(default_factory=lambda: f"req_{uuid.uuid4().hex[:10]}")
|
|
34
34
|
created_at: str = field(default_factory=lambda: datetime.now().isoformat())
|
|
35
35
|
|
|
@@ -40,10 +40,10 @@ class CrossAgentRequest:
|
|
|
40
40
|
@dataclass
|
|
41
41
|
class GlobalSituation:
|
|
42
42
|
"""全局态势 — 主管的驾驶舱视图,汇总各内核关键指标。"""
|
|
43
|
-
finance: dict = field(default_factory=dict)
|
|
44
|
-
learning: dict = field(default_factory=dict)
|
|
45
|
-
itinerary: dict = field(default_factory=dict)
|
|
46
|
-
alerts: list = field(default_factory=list)
|
|
43
|
+
finance: dict = field(default_factory=dict)
|
|
44
|
+
learning: dict = field(default_factory=dict)
|
|
45
|
+
itinerary: dict = field(default_factory=dict)
|
|
46
|
+
alerts: list = field(default_factory=list)
|
|
47
47
|
timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
|
|
48
48
|
|
|
49
49
|
def to_dict(self) -> dict:
|
|
@@ -58,13 +58,8 @@ class Orchestrator:
|
|
|
58
58
|
不再反向直接持有含主管的 agents 字典)。"""
|
|
59
59
|
|
|
60
60
|
def __init__(self, caps, auditor=None):
|
|
61
|
-
self._caps = caps
|
|
62
|
-
# 【P1-3】统一治理审计(可空:standalone 无审计)。裁决事件原本**完全没有**——
|
|
63
|
-
# 跨 Agent 委派的批准/拒绝/转人工,事前无任何可观测记录。
|
|
61
|
+
self._caps = caps
|
|
64
62
|
self._auditor = auditor
|
|
65
|
-
# 【深审修复·长期运行泄漏】原为普通 list、只增不减且**全库从不读**——每次跨 Agent 请求
|
|
66
|
-
# append 一个 req → 无界增长。改有界 deque(保留最近 N 条供未来观测,符合本库 token_meter/
|
|
67
|
-
# HookManager 的有界日志惯例);deque.append 原子 ⇒ 并发路由也安全。
|
|
68
63
|
self.request_log: deque = deque(maxlen=1000)
|
|
69
64
|
|
|
70
65
|
@property
|
|
@@ -72,12 +67,11 @@ class Orchestrator:
|
|
|
72
67
|
"""按名字从注册中枢解析 agents(id -> agent 实例)。"""
|
|
73
68
|
return self._caps.get("agents") or {}
|
|
74
69
|
|
|
75
|
-
# ── 全局态势汇总(P3:通用聚合,不懂任何领域内部 API)────────────
|
|
76
70
|
def gather_situation(self) -> GlobalSituation:
|
|
77
71
|
"""通用聚合各 Agent 自报的领域态势(agent.situation()),形成驾驶舱视图。
|
|
78
72
|
编排层不再直接读 finance/learning/itinerary 内核——领域知识已归属各领域 Agent。"""
|
|
79
73
|
sit = GlobalSituation()
|
|
80
|
-
alerts = []
|
|
74
|
+
alerts: list[dict] = []
|
|
81
75
|
for agent in self.agents.values():
|
|
82
76
|
try:
|
|
83
77
|
s = agent.situation()
|
|
@@ -85,12 +79,10 @@ class Orchestrator:
|
|
|
85
79
|
continue
|
|
86
80
|
if not s:
|
|
87
81
|
continue
|
|
88
|
-
# 一个 Agent 可以自报**多个**领域(拓扑 5→3 后,办公学习 Agent 同时覆盖
|
|
89
|
-
# learning 与 itinerary),故 `domains: {name: status}` 与旧的单 `domain` 都收。
|
|
90
82
|
doms = s.get("domains")
|
|
91
83
|
if isinstance(doms, dict) and doms:
|
|
92
84
|
for d, st in doms.items():
|
|
93
|
-
if d and hasattr(sit, d):
|
|
85
|
+
if d and hasattr(sit, d):
|
|
94
86
|
setattr(sit, d, st or {})
|
|
95
87
|
else:
|
|
96
88
|
domain = s.get("domain")
|
|
@@ -102,24 +94,16 @@ class Orchestrator:
|
|
|
102
94
|
.get(a.get("severity"), 3))
|
|
103
95
|
return sit
|
|
104
96
|
|
|
105
|
-
# ── 跨 Agent 请求路由(P3:通用委托,由目标 Agent 用自己的守门人裁决)──────
|
|
106
97
|
def route_request(self, req: CrossAgentRequest) -> CrossAgentRequest:
|
|
107
98
|
"""把请求路由给目标 Agent,由**目标 Agent**用自己的守门人裁决(编排层不懂领域)。"""
|
|
108
99
|
target = self.agents.get(req.to_agent)
|
|
109
100
|
if target is None:
|
|
110
|
-
# 【P1-0】原为 status="approved"(默认放行)—— 那是 fail-open:发起方会拿到
|
|
111
|
-
# "已批准",但**没有任何 Agent 真的处理过这个请求**。后果是实打实的,例如
|
|
112
|
-
# 行程 Agent 向未注册的 finance 申请差旅预算,会直接拿到批准并据此下单。
|
|
113
|
-
# 与下方「裁决异常 → 转人工确认」同属一条原则:裁决权不在编排层,凡未真正
|
|
114
|
-
# 裁决过的,都不能算通过。改为 needs_confirmation 同样"不崩",只是不再说谎。
|
|
115
101
|
req.status = "needs_confirmation"
|
|
116
102
|
req.verdict = f"目标 Agent '{req.to_agent}' 不存在,无法裁决,转人工确认"
|
|
117
103
|
else:
|
|
118
104
|
try:
|
|
119
105
|
target.handle_cross_request(req)
|
|
120
106
|
except Exception as e:
|
|
121
|
-
# 守门人裁决崩溃(如畸形 payload 让解析抛错)必须 **fail-safe**:转人工确认,
|
|
122
|
-
# 绝不自动放行——对守门人而言,裁决失败时放行=fail-open,是安全隐患。
|
|
123
107
|
req.status = "needs_confirmation"
|
|
124
108
|
req.verdict = f"裁决异常,转人工确认: {e}"
|
|
125
109
|
self.request_log.append(req)
|
|
@@ -143,7 +127,6 @@ class Orchestrator:
|
|
|
143
127
|
"negotiation_round": len(req.negotiation)},
|
|
144
128
|
correlation_id=req.id)
|
|
145
129
|
|
|
146
|
-
# ── 协商闭环:被拒 → 发起方让步 → 再裁决 ──────────────────────────
|
|
147
130
|
def negotiate(self, req: CrossAgentRequest, max_rounds: int = 3) -> CrossAgentRequest:
|
|
148
131
|
"""跑协商协议。原先裁决是**终局**:财务说预算不足→rejected→到此为止,用户只拿到
|
|
149
132
|
一句"被拒绝"。现在:发起方可拿着拒绝理由降标准重来(改高铁/换酒店/减一天)。
|
|
@@ -165,17 +148,17 @@ class Orchestrator:
|
|
|
165
148
|
"verdict": req.verdict,
|
|
166
149
|
"payload": dict(req.payload)})
|
|
167
150
|
if req.status != "rejected":
|
|
168
|
-
break
|
|
151
|
+
break
|
|
169
152
|
requester = self.agents.get(req.from_agent)
|
|
170
153
|
if requester is None:
|
|
171
154
|
break
|
|
172
155
|
try:
|
|
173
|
-
revised = requester.concede(req, req.verdict)
|
|
156
|
+
revised = requester.concede(req, req.verdict)
|
|
174
157
|
except Exception as e:
|
|
175
158
|
req.negotiation.append({"round": rnd, "concede_error": str(e)[:120]})
|
|
176
|
-
break
|
|
159
|
+
break
|
|
177
160
|
if not revised or revised == req.payload:
|
|
178
|
-
break
|
|
161
|
+
break
|
|
179
162
|
req.negotiation[-1]["conceded_to"] = dict(revised)
|
|
180
163
|
req.payload = revised
|
|
181
164
|
return req
|
|
@@ -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
|