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
|
@@ -5,9 +5,13 @@
|
|
|
5
5
|
import os
|
|
6
6
|
|
|
7
7
|
from ._context import _taint_raise
|
|
8
|
+
from typing import Any, Callable
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
class AugmentMixin:
|
|
12
|
+
_think: Callable[..., Any]
|
|
13
|
+
available_abilities: Any
|
|
14
|
+
_call_llm: Callable[..., Any]
|
|
11
15
|
def _web_augment(self, query: str, messages: list[dict]) -> list[dict]:
|
|
12
16
|
"""若问题含时效性,联网搜索并把结果注入 messages(放在用户问题前)。
|
|
13
17
|
DeepSeek/OpenAI 等基础 API 不自带联网,这里用百度 AI 搜索补上。失败则原样返回。"""
|
|
@@ -19,9 +23,6 @@ class AugmentMixin:
|
|
|
19
23
|
payload = _ws.search_answer(query)
|
|
20
24
|
if not payload or (not payload.get("answer") and not payload.get("references")):
|
|
21
25
|
print("[联网搜索] ⚠ 无结果(未配 BAIDU_SEARCH_KEY 或网络不通)")
|
|
22
|
-
# 需要联网但没搜到(未配 key / 网络不通)→ 注入明确指令逼 LLM 如实说,绝不编造。
|
|
23
|
-
# 否则 LLM 被 system_prompt 告知"已接入联网"却拿不到结果,会凭训练数据瞎编版本号/
|
|
24
|
-
# 新闻/价格(用户实测踩到)。合并进最后一条 user 消息(与下方注入同法,不新增 user 消息)。
|
|
25
26
|
note = ("【系统提示:本次问题需要实时/最新信息,但联网搜索未返回任何结果"
|
|
26
27
|
"(可能未配置搜索服务或网络不可用)。请如实告知用户你暂时无法查询实时信息,"
|
|
27
28
|
"**切勿编造或凭记忆给出可能过时的答案**(如版本号、新闻、模型列表、价格等)。】")
|
|
@@ -39,9 +40,6 @@ class AugmentMixin:
|
|
|
39
40
|
content=f"联网搜索「{query}」命中,参考 {n} 条",
|
|
40
41
|
node_type="tool_use", conf=0.9,
|
|
41
42
|
)
|
|
42
|
-
# 合并式注入:把联网上下文并进「最后一条 user 消息」,而不是前插一条独立
|
|
43
|
-
# user 消息 —— 后者会产生连续两条 user,某些 OpenAI-compatible 端点会拒绝,
|
|
44
|
-
# 与「模型无关」铁律冲突。拷贝后改,不动原 messages。
|
|
45
43
|
merged = list(messages)
|
|
46
44
|
for i in range(len(merged) - 1, -1, -1):
|
|
47
45
|
if merged[i].get("role") == "user":
|
|
@@ -49,14 +47,11 @@ class AugmentMixin:
|
|
|
49
47
|
m["content"] = ctx + "\n\n" + (m.get("content") or "")
|
|
50
48
|
merged[i] = m
|
|
51
49
|
return merged
|
|
52
|
-
return [{"role": "user", "content": ctx}] + merged
|
|
50
|
+
return [{"role": "user", "content": ctx}] + merged
|
|
53
51
|
except Exception as e:
|
|
54
52
|
print(f"[联网搜索] 异常: {e}")
|
|
55
53
|
return messages
|
|
56
54
|
|
|
57
|
-
# 注:_model_augment 已删除 —— 模型身份注入规则化到 ModelRouter._runtime_context,
|
|
58
|
-
# 在 LLM 唯一咽喉(complete/stream)对每次调用恒定注入真实 provider/model,
|
|
59
|
-
# 不再靠"模型/大模型/llm"关键词命中才注入,也就没有了漏判/误判两难。
|
|
60
55
|
|
|
61
56
|
def _ability_augment_enabled(self) -> bool:
|
|
62
57
|
"""能力知识增强是否启用。默认**关**(behavior-preserving)。
|
|
@@ -89,17 +84,16 @@ class AugmentMixin:
|
|
|
89
84
|
if not hits:
|
|
90
85
|
return messages
|
|
91
86
|
lines = "\n".join(f"- {h.name}: {h.summary}" for h in hits[:5])
|
|
92
|
-
# 知识由摄入的外部文档蒸馏而来,可能被投毒 → 包裹为不可信数据,防间接注入
|
|
93
87
|
from ...core import injection_guard as _ig
|
|
94
88
|
ctx = (f"【能力知识增强 · {best.get('display_name') or best['ability_key']}】"
|
|
95
89
|
f"以下是知识库中与本任务相关、且该能力已达覆盖的知识,供参考:\n"
|
|
96
90
|
+ _ig.wrap_untrusted(lines, source="knowledge"))
|
|
97
91
|
self._think(content=f"能力增强「{best['ability_key']}」注入 {len(hits)} 条知识",
|
|
98
92
|
node_type="tool_use", conf=0.85)
|
|
99
|
-
_taint_raise(self, _ig.taint_level("knowledge"))
|
|
93
|
+
_taint_raise(self, _ig.taint_level("knowledge"))
|
|
100
94
|
return [{"role": "user", "content": ctx}] + messages
|
|
101
95
|
except Exception:
|
|
102
|
-
return messages
|
|
96
|
+
return messages
|
|
103
97
|
|
|
104
98
|
def _skill_augment(self, query: str, messages: list[dict]) -> list[dict]:
|
|
105
99
|
"""按触发词匹配 SKILL.md 指令技能,把正文注入(合并进最后一条 user 消息)。"""
|
|
@@ -107,7 +101,7 @@ class AugmentMixin:
|
|
|
107
101
|
reg = getattr(self, "md_skills", None)
|
|
108
102
|
if not reg:
|
|
109
103
|
return messages
|
|
110
|
-
matched = reg.match(query)[:3]
|
|
104
|
+
matched = reg.match(query)[:3]
|
|
111
105
|
if not matched:
|
|
112
106
|
return messages
|
|
113
107
|
ctx = "以下是可参照的操作指令(SKILL),请据此执行:\n\n" + \
|
|
@@ -144,6 +138,6 @@ class AugmentMixin:
|
|
|
144
138
|
避免堵死 async 事件循环(否则百度慢/超时时整个服务、前端轮询全部冻住)。"""
|
|
145
139
|
import asyncio
|
|
146
140
|
aug = await asyncio.to_thread(self._web_augment, query, messages)
|
|
147
|
-
aug = await asyncio.to_thread(self._ability_augment, query, aug)
|
|
148
|
-
aug = await asyncio.to_thread(self._skill_augment, query, aug)
|
|
141
|
+
aug = await asyncio.to_thread(self._ability_augment, query, aug)
|
|
142
|
+
aug = await asyncio.to_thread(self._skill_augment, query, aug)
|
|
149
143
|
return await self._call_llm(aug, max_tokens=max_tokens)
|
|
@@ -3,9 +3,13 @@
|
|
|
3
3
|
从 `foxclaw/agents/base.py` 搬移出来的 `_call_llm`。
|
|
4
4
|
"""
|
|
5
5
|
from ._context import _reasoning_inject, _act_stream
|
|
6
|
+
from typing import Any
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
class LlmMixin:
|
|
10
|
+
AGENT_ID: str
|
|
11
|
+
router: Any
|
|
12
|
+
system_prompt: str
|
|
9
13
|
async def _call_llm(
|
|
10
14
|
self,
|
|
11
15
|
messages: list[dict],
|
|
@@ -28,9 +32,6 @@ class LlmMixin:
|
|
|
28
32
|
import asyncio
|
|
29
33
|
inj = _reasoning_inject.get()
|
|
30
34
|
if inj:
|
|
31
|
-
# 合并进**最后一条 user 消息**(与 _web_augment 同法),而非前插独立 user 消息——
|
|
32
|
-
# 后者会产生连续两条 user,某些 OpenAI-compatible 端点会拒绝,违反「模型无关」铁律
|
|
33
|
-
# (B4 审核 A3)。拷贝后改,不动原 messages。
|
|
34
35
|
merged = list(messages)
|
|
35
36
|
for i in range(len(merged) - 1, -1, -1):
|
|
36
37
|
if merged[i].get("role") == "user":
|
|
@@ -38,7 +39,7 @@ class LlmMixin:
|
|
|
38
39
|
merged[i] = m
|
|
39
40
|
break
|
|
40
41
|
else:
|
|
41
|
-
merged = [{"role": "user", "content": str(inj)}] + merged
|
|
42
|
+
merged = [{"role": "user", "content": str(inj)}] + merged
|
|
42
43
|
messages = merged
|
|
43
44
|
sc = _act_stream.get()
|
|
44
45
|
if sc is not None:
|
|
@@ -54,13 +55,13 @@ class LlmMixin:
|
|
|
54
55
|
try:
|
|
55
56
|
on_token(chunk)
|
|
56
57
|
except Exception:
|
|
57
|
-
pass
|
|
58
|
+
pass
|
|
58
59
|
return "".join(parts)
|
|
59
60
|
|
|
60
61
|
try:
|
|
61
62
|
return await asyncio.to_thread(_stream_collect)
|
|
62
63
|
except Exception:
|
|
63
|
-
pass
|
|
64
|
+
pass
|
|
64
65
|
return await asyncio.to_thread(
|
|
65
66
|
self.router.complete,
|
|
66
67
|
self.system_prompt, messages, max_tokens, self.AGENT_ID)
|
|
@@ -2,10 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
从 `foxclaw/agents/base.py` 搬移出来的记忆相关方法。
|
|
4
4
|
"""
|
|
5
|
+
from typing import Any, Callable
|
|
6
|
+
|
|
5
7
|
from ._context import _suppress_memory
|
|
6
8
|
|
|
7
9
|
|
|
8
10
|
class MemoryMixin:
|
|
11
|
+
AGENT_ID: str
|
|
12
|
+
hmr: Any
|
|
13
|
+
_think: Callable[..., Any]
|
|
14
|
+
|
|
9
15
|
def _recall_memory(self, query: str, top_k: int = 5) -> list:
|
|
10
16
|
"""从 HMR 召回相关记忆,记录到 ThoughtChain"""
|
|
11
17
|
from foxclaw.core.tracing import span as _span
|
|
@@ -24,7 +30,7 @@ class MemoryMixin:
|
|
|
24
30
|
def _store_memory(self, content: str, mem_type: str = "concept", **kwargs):
|
|
25
31
|
"""存储新记忆到 HMR"""
|
|
26
32
|
if _suppress_memory.get():
|
|
27
|
-
return
|
|
33
|
+
return
|
|
28
34
|
from foxclaw.core.tracing import span as _span
|
|
29
35
|
with _span("memory.store", agent=self.AGENT_ID, type=mem_type):
|
|
30
36
|
self.hmr.ingest(
|
|
@@ -3,10 +3,14 @@
|
|
|
3
3
|
从 `foxclaw/agents/base.py` 搬移出来的偏好相关方法。
|
|
4
4
|
"""
|
|
5
5
|
import os
|
|
6
|
+
from typing import Any, Callable
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
class PreferenceMixin:
|
|
9
|
-
|
|
10
|
+
AGENT_ID: str
|
|
11
|
+
hmr: Any
|
|
12
|
+
_think: Callable[..., Any]
|
|
13
|
+
_store_memory: Callable[..., Any]
|
|
10
14
|
_PREF_MARKERS = ("我喜欢", "我不喜欢", "我讨厌", "我希望", "别再", "不要再", "以后都",
|
|
11
15
|
"以后请", "记住", "我的偏好", "我习惯", "下次请", "下次记得", "我倾向",
|
|
12
16
|
"请务必", "请一律", "总是用", "我要求")
|
|
@@ -20,16 +24,12 @@ class PreferenceMixin:
|
|
|
20
24
|
t = (task or "").strip()
|
|
21
25
|
if not t or not any(k in t for k in self._PREF_MARKERS):
|
|
22
26
|
return
|
|
23
|
-
# 偏好通道消毒(防投毒):偏好每轮以"一律以此为准"的权威口吻注入,且无法像外部数据那样
|
|
24
|
-
# 包 <untrusted>(它本就该被遵循)。所以只能在**存入侧**拦——若这条"偏好"候选里夹带注入/
|
|
25
|
-
# 越权特征(如用户把含"忽略以上指令/系统覆盖"的文档粘进对话),就不固化,断掉投毒源头。
|
|
26
27
|
from ...core import injection_guard as _ig
|
|
27
28
|
if _ig.scan(t):
|
|
28
29
|
self._think(content="偏好候选含疑似注入特征,已拒绝固化(防记忆投毒)",
|
|
29
30
|
node_type="reflection", conf=0.9)
|
|
30
31
|
return
|
|
31
32
|
try:
|
|
32
|
-
# 用 tag 标记(不受 memory_type 白名单降级);shared=True 跨会话跨 Agent 持久
|
|
33
33
|
self._store_memory(f"用户偏好:{t[:200]}", mem_type="preference",
|
|
34
34
|
tags=["preference"], shared=True)
|
|
35
35
|
except Exception:
|
|
@@ -44,9 +44,6 @@ class PreferenceMixin:
|
|
|
44
44
|
return ""
|
|
45
45
|
if not prefs:
|
|
46
46
|
return ""
|
|
47
|
-
# prefs 已按"新→旧"排序(recall_by_tag)。偏好会随时间改变(如"最喜欢的颜色"从蓝改灰),
|
|
48
|
-
# 语义相似度无法区分同话题偏好(实测"颜色"与"回答风格"偏好都 ~0.9),故不能靠自动删旧;
|
|
49
|
-
# 改由指令让 LLM 以"最新的(最靠前)/当前对话最近说法"为准,避免被已更新的旧偏好带偏。
|
|
50
47
|
lines = "\n".join(f"- {p.get('content', '')}" for p in prefs)
|
|
51
48
|
return ("【用户偏好(按最近→最早排列)】\n" + lines +
|
|
52
49
|
"\n**注意:偏好会随时间改变。若上面几条彼此冲突,或与当前对话中用户最近的说法不一致,"
|
|
@@ -79,3 +76,56 @@ class PreferenceMixin:
|
|
|
79
76
|
return self.hmr.delete_memory(pref_id)
|
|
80
77
|
except Exception:
|
|
81
78
|
return False
|
|
79
|
+
|
|
80
|
+
_NICK_PREFIXES = ("称呼我为", "叫我", "我的名字是", "我的名字叫")
|
|
81
|
+
|
|
82
|
+
def remember_preference(self, text: str) -> bool:
|
|
83
|
+
"""写一条用户主动声明的偏好(设置面板 / 首次引导用)。返回是否落库。
|
|
84
|
+
|
|
85
|
+
与 `_maybe_store_preference`(对话里自动抓)的区别:这条不经过注入扫描,
|
|
86
|
+
因为来源是设置表单而非自由文本;但同样限长 200,避免把整段话塞进偏好。
|
|
87
|
+
"""
|
|
88
|
+
t = (text or "").strip()
|
|
89
|
+
if not t:
|
|
90
|
+
return False
|
|
91
|
+
try:
|
|
92
|
+
self._store_memory(f"用户偏好:{t[:200]}", mem_type="preference",
|
|
93
|
+
tags=["preference"], shared=True)
|
|
94
|
+
return True
|
|
95
|
+
except Exception:
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
def _nick_of(cls, content: str) -> str | None:
|
|
100
|
+
"""从一条 preference 文本里取出称呼;不是称呼类返回 None。"""
|
|
101
|
+
c = (content or "").strip()
|
|
102
|
+
if c.startswith("用户偏好:"):
|
|
103
|
+
c = c[len("用户偏好:"):].strip()
|
|
104
|
+
for prefix in cls._NICK_PREFIXES:
|
|
105
|
+
if c.startswith(prefix):
|
|
106
|
+
return c[len(prefix):].strip(" ::,,。.")
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
def get_nickname(self) -> str:
|
|
110
|
+
"""当前称呼(取最近一条称呼类偏好;没设置过返回空串)。"""
|
|
111
|
+
for p in self.list_preferences(limit=200):
|
|
112
|
+
nick = self._nick_of(p.get("content", ""))
|
|
113
|
+
if nick:
|
|
114
|
+
return nick
|
|
115
|
+
return ""
|
|
116
|
+
|
|
117
|
+
def set_nickname(self, nick: str) -> str:
|
|
118
|
+
"""设置称呼;传空串 = 清除。返回生效后的值。
|
|
119
|
+
|
|
120
|
+
先把旧的称呼条目删掉再写:否则每改一次就多一条「称呼我为 X」,注入时
|
|
121
|
+
几条互相矛盾(虽然 prompt 里约定「以最新为准」,但没必要让它去猜)。
|
|
122
|
+
"""
|
|
123
|
+
value = (nick or "").strip()[:24]
|
|
124
|
+
for p in self.list_preferences(limit=200):
|
|
125
|
+
if self._nick_of(p.get("content", "")):
|
|
126
|
+
pid = p.get("id")
|
|
127
|
+
if pid:
|
|
128
|
+
self.delete_preference(pid)
|
|
129
|
+
if value:
|
|
130
|
+
self.remember_preference(f"称呼我为 {value}")
|
|
131
|
+
return value
|
|
@@ -7,11 +7,6 @@ import os
|
|
|
7
7
|
from abc import ABC, abstractmethod
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
# ── 内核系统提示词(所有 Agent 共享前缀)────────────────────────────────────────
|
|
11
|
-
# 见 docs/internal/FOXCLAW_SYSTEM_PROMPT.md。融合"认知操作系统"身份 + 系统性原理
|
|
12
|
-
# (真相优先 / Principle 07 / 价值记忆)+ 安全边界(指令/数据隔离、最小权限、人在回路)。
|
|
13
|
-
# 一处定义,base.system_prompt 统一叠加到各 Agent 角色职责之前 → 一处改全体生效。
|
|
14
|
-
# §4 的 <untrusted> 规则与 core/injection_guard.wrap_untrusted 的打标配套,缺一不可。
|
|
15
10
|
KERNEL_PROMPT = """你是 Foxclaw —— 一个面向 Agent 时代的认知操作系统,不是一次性问答机器人。
|
|
16
11
|
你的使命:让信息成为知识,让知识成为能力;守护用户的知识火种,保持认知的连续性。
|
|
17
12
|
以下是所有 Agent 共享的内核准则;你的具体角色职责见随后的说明。
|
|
@@ -59,7 +54,6 @@ KERNEL_PROMPT = """你是 Foxclaw —— 一个面向 Agent 时代的认知操
|
|
|
59
54
|
|
|
60
55
|
|
|
61
56
|
class PromptMixin(ABC):
|
|
62
|
-
# ── 子类必须实现 ───────────────────────────────────────
|
|
63
57
|
|
|
64
58
|
@property
|
|
65
59
|
@abstractmethod
|
|
@@ -4,8 +4,12 @@
|
|
|
4
4
|
助手降级为指令技能后,工具箱不能再绑在某个 Agent 上 —— 否则主管/财务/办公全都用不了技能,
|
|
5
5
|
而"技能"恰恰是那两个下放的 Agent 的新家。故上移到 mixin,由基类统一混入。
|
|
6
6
|
"""
|
|
7
|
+
from typing import Any, Callable
|
|
8
|
+
|
|
7
9
|
|
|
8
10
|
class SkillBoxMixin:
|
|
11
|
+
_session_id: str | None
|
|
12
|
+
_think: Callable[..., Any]
|
|
9
13
|
def attach_skills(self, registry, adapter):
|
|
10
14
|
"""挂载 skill 接入层(注册表 + 执行适配器)。"""
|
|
11
15
|
self.skill_registry = registry
|
|
@@ -16,19 +20,18 @@ class SkillBoxMixin:
|
|
|
16
20
|
reg = getattr(self, "skill_registry", None)
|
|
17
21
|
return reg.catalog() if reg else []
|
|
18
22
|
|
|
19
|
-
def use_skill(self, task: str, args: dict = None, confirmed: bool = False) -> dict:
|
|
23
|
+
def use_skill(self, task: str, args: dict | None = None, confirmed: bool = False) -> dict:
|
|
20
24
|
"""按任务匹配 skill 并调用,结果回填 ThoughtChain,过守门人。"""
|
|
21
25
|
reg = getattr(self, "skill_registry", None)
|
|
22
26
|
adp = getattr(self, "skill_adapter", None)
|
|
23
27
|
if not (reg and adp):
|
|
24
28
|
return {"error": "skill 接入层未挂载"}
|
|
25
|
-
# 用升级版 router(三步打分 + 否定词检测),优于老的 reg.match 纯子串匹配
|
|
26
29
|
try:
|
|
27
30
|
from ...skills.pipeline import route as ahs_route
|
|
28
31
|
r = ahs_route(task, reg)
|
|
29
32
|
skill = reg.get(r.primary) if r.primary else None
|
|
30
33
|
except Exception:
|
|
31
|
-
skill = reg.match(task)
|
|
34
|
+
skill = reg.match(task)
|
|
32
35
|
if not skill:
|
|
33
36
|
return {"matched": None, "message": "未匹配到合适的 skill"}
|
|
34
37
|
self._think(
|
|
@@ -40,9 +43,6 @@ class SkillBoxMixin:
|
|
|
40
43
|
from ._context import _taint_get, _untrusted_get
|
|
41
44
|
token = set_loop_session(self._session_id)
|
|
42
45
|
try:
|
|
43
|
-
# 会话已取回敏感数据(知识库/记忆/网页)后,技能的外发/副作用动作同样要二次确认——
|
|
44
|
-
# 此前技能路径不带污点,permission.evaluate 的 Stage 3.5 只对工具生效。
|
|
45
|
-
# 来源不可信(外部聊天渠道)时按同一口径抬到 TAINT_PERSONAL。
|
|
46
46
|
_taint = _taint_get(self)
|
|
47
47
|
if _untrusted_get(self):
|
|
48
48
|
_taint = max(_taint, _ig.TAINT_PERSONAL)
|
|
@@ -15,8 +15,8 @@ import time
|
|
|
15
15
|
from ._context import _taint_get, _taint_raise, _untrusted_get
|
|
16
16
|
from ...core import settings
|
|
17
17
|
from ...core.injection_guard import redact_obj, redact_secrets
|
|
18
|
+
from typing import Any
|
|
18
19
|
|
|
19
|
-
#: 同轮最多并发几个工具(再多也是网络等待,收益递减;避免打爆 provider/搜索服务)
|
|
20
20
|
MAX_PARALLEL = 4
|
|
21
21
|
|
|
22
22
|
|
|
@@ -46,7 +46,7 @@ class _Spawn:
|
|
|
46
46
|
def _run():
|
|
47
47
|
try:
|
|
48
48
|
self.box["v"] = fn()
|
|
49
|
-
except BaseException as e:
|
|
49
|
+
except BaseException as e:
|
|
50
50
|
self.box["e"] = e
|
|
51
51
|
finally:
|
|
52
52
|
self.done.set()
|
|
@@ -83,29 +83,19 @@ def _guard_tool(agent, name: str, args: dict):
|
|
|
83
83
|
_reg = _tools.default_registry()
|
|
84
84
|
if not _reg.has(name):
|
|
85
85
|
return f"(未知工具 {name},未执行)", {}
|
|
86
|
-
# ── 参数校验(用注册表里同一份 JSON Schema)────────────────────
|
|
87
|
-
# 修复前:schema 只发给模型,执行侧不校验 → 模型给错类型/漏参数会直接流到
|
|
88
|
-
# 下游。且下方 permission Stage 1 因 params={} 恒不触发,只靠手工 if-not 兜。
|
|
89
|
-
# 新增校验闸后,错误**回注模型**让其自修正,而不是静默用空参数继续
|
|
90
|
-
# (那会让模型误以为工具已执行,进而编造基于"工具结果"的结论)。
|
|
91
86
|
_errs = _reg.validate_args(name, args or {})
|
|
92
87
|
if _errs:
|
|
93
88
|
agent._think(content=f"工具 {name} 入参校验失败: {'; '.join(_errs)[:120]}",
|
|
94
89
|
node_type="tool_intent", conf=0.9)
|
|
95
90
|
return (f"(工具 {name} 入参不合法: {'; '.join(_errs)}。"
|
|
96
91
|
f"请按工具声明修正参数后重试。)"), {}
|
|
97
|
-
# ── 权限管线:动作级闸门。构造 tool-spec 交四阶段裁决 ──
|
|
98
92
|
cap = _tools.tool_capability(name)
|
|
99
|
-
# 传入真实 params schema,使 permission Stage 1(必填校验)真正生效,
|
|
100
|
-
# 不再依赖下游手工检查兜底。兼容 permission 的两种 params 格式。
|
|
101
93
|
_schema = _reg.param_schema(name)
|
|
102
94
|
_req = set(_schema.get("required") or [])
|
|
103
95
|
_params = {k: {"required": k in _req, "type": v.get("type")}
|
|
104
96
|
for k, v in (_schema.get("properties") or {}).items()
|
|
105
97
|
if isinstance(v, dict)}
|
|
106
98
|
spec = _types.SimpleNamespace(id=f"tool:{name}", params=_params, **cap)
|
|
107
|
-
# 来源不可信(外部聊天渠道 / 远程驱动)→ 按「已接触敏感数据」处理:写/发送/外发强制 HITL。
|
|
108
|
-
# 只读工具不受影响(污点分支只作用于副作用动作),所以读操作不会被这一条拖慢。
|
|
109
99
|
_taint = _taint_get(agent)
|
|
110
100
|
if _untrusted_get(agent):
|
|
111
101
|
_taint = max(_taint, _ig.TAINT_PERSONAL)
|
|
@@ -115,7 +105,6 @@ def _guard_tool(agent, name: str, args: dict):
|
|
|
115
105
|
if decision.decision == "deny":
|
|
116
106
|
return f"(工具 {name} 被权限策略拒绝:{decision.reason},未执行)", cap
|
|
117
107
|
if decision.decision == "ask":
|
|
118
|
-
# 红线/副作用/未声明工具 → 人在回路:不自动执行,交回由用户确认(KERNEL §6 同旨)
|
|
119
108
|
agent._think(content=f"工具 {name} 需用户确认({decision.reason}),已暂停执行",
|
|
120
109
|
node_type="decision", conf=0.9)
|
|
121
110
|
return (f"(工具 {name} 属于需确认的动作:{decision.reason}。按安全策略我不会自动执行——"
|
|
@@ -144,19 +133,17 @@ def _invoke_tool(agent, name: str, args: dict, taint_sink: list | None = None) -
|
|
|
144
133
|
q = str((args or {}).get("query", "")).strip()
|
|
145
134
|
if not q:
|
|
146
135
|
return "(web_search 缺少 query 参数)"
|
|
147
|
-
if _ws.is_personal_query(q):
|
|
136
|
+
if _ws.is_personal_query(q):
|
|
148
137
|
return "(该问题属于用户本地数据,已跳过联网;请依据本地领域信息回答)"
|
|
149
138
|
payload = _ws.search_answer(q)
|
|
150
139
|
if not payload or (not payload.get("answer") and not payload.get("references")):
|
|
151
140
|
return ("联网搜索未返回结果(可能未配置搜索服务或网络不可用);"
|
|
152
141
|
"请如实告知用户暂时无法查询实时信息,切勿编造版本号/新闻/价格等。")
|
|
153
142
|
agent._think(content=f"工具 web_search「{q}」命中", node_type="tool_use", conf=0.9)
|
|
154
|
-
_raise(_ig.taint_level("web_search"))
|
|
143
|
+
_raise(_ig.taint_level("web_search"))
|
|
155
144
|
return _ws.format_for_prompt(q, payload)
|
|
156
145
|
if name == "recall_memory":
|
|
157
146
|
q = str((args or {}).get("query", "")).strip()
|
|
158
|
-
# top_k 此前硬编码 5,模型传值无效 → 加边界校验后尊重模型意图
|
|
159
|
-
# (schema 已声明 top_k,1-20;越界回落默认,防一次召回拉爆上下文)
|
|
160
147
|
try:
|
|
161
148
|
top_k = max(1, min(20, int((args or {}).get("top_k", 5))))
|
|
162
149
|
except Exception:
|
|
@@ -165,8 +152,7 @@ def _invoke_tool(agent, name: str, args: dict, taint_sink: list | None = None) -
|
|
|
165
152
|
if not res:
|
|
166
153
|
return "(未召回到相关记忆)"
|
|
167
154
|
lines = "\n".join(f"- {r.get('content', '')}" for r in res[:top_k])
|
|
168
|
-
|
|
169
|
-
_raise(_ig.taint_level("memory")) # 个人记忆 → 会话污点(高敏感)
|
|
155
|
+
_raise(_ig.taint_level("memory"))
|
|
170
156
|
return "【召回的相关记忆】\n" + _ig.wrap_untrusted(lines, source="memory")
|
|
171
157
|
except Exception as e:
|
|
172
158
|
return f"(工具 {name} 执行异常:{type(e).__name__})"
|
|
@@ -179,7 +165,7 @@ def _timeout_text(name: str) -> str:
|
|
|
179
165
|
|
|
180
166
|
|
|
181
167
|
def _merge_taint(agent, sink: list) -> None:
|
|
182
|
-
for lvl in sink:
|
|
168
|
+
for lvl in sink:
|
|
183
169
|
try:
|
|
184
170
|
_taint_raise(agent, lvl)
|
|
185
171
|
except Exception:
|
|
@@ -221,8 +207,6 @@ def _run_tools_batch(agent, calls: list) -> list:
|
|
|
221
207
|
if p[2] is not None:
|
|
222
208
|
out.append(p[2])
|
|
223
209
|
continue
|
|
224
|
-
# 串行走 agent._dispatch_tool(保留「实例级覆盖」这一扩展点,单测常这么替身);
|
|
225
|
-
# 没有该方法时才直接执行。
|
|
226
210
|
_disp = getattr(agent, "_dispatch_tool", None)
|
|
227
211
|
out.append(_disp(p[0], p[1]) if callable(_disp)
|
|
228
212
|
else _exec_guarded(agent, p[0], p[1]))
|
|
@@ -237,7 +221,7 @@ def _run_tools_batch(agent, calls: list) -> list:
|
|
|
237
221
|
for n, sp, sink in spawns:
|
|
238
222
|
left = max(0.0, timeout - (time.monotonic() - started))
|
|
239
223
|
try:
|
|
240
|
-
ok, val = sp.wait(left)
|
|
224
|
+
ok, val = sp.wait(left)
|
|
241
225
|
except Exception as e:
|
|
242
226
|
out.append(f"(工具 {n} 执行异常:{type(e).__name__})")
|
|
243
227
|
continue
|
|
@@ -249,7 +233,6 @@ def _run_tools_batch(agent, calls: list) -> list:
|
|
|
249
233
|
return out
|
|
250
234
|
|
|
251
235
|
|
|
252
|
-
#: checkpoint 里单步结果最多存多少字符(超出即标记 truncated,续跑时不复用、改为重跑)
|
|
253
236
|
_STEP_RESULT_MAX = 4000
|
|
254
237
|
|
|
255
238
|
|
|
@@ -337,8 +320,6 @@ def _settle(agent, call: dict, result: str) -> str:
|
|
|
337
320
|
text, kind = _apply_failure_policy(agent, call, result)
|
|
338
321
|
try:
|
|
339
322
|
cap = _tools.tool_capability(name)
|
|
340
|
-
# 核验看的是**工具原始返回**,不是我们追加过提示的文本 —— 否则提示里的
|
|
341
|
-
# "超时/未完成"字样会反过来影响判定。
|
|
342
323
|
v = _verify.verify(name, args, result, cap)
|
|
343
324
|
note = _verify.note_for_model(v, read_only=bool(cap.get("read_only")))
|
|
344
325
|
if note:
|
|
@@ -347,12 +328,10 @@ def _settle(agent, call: dict, result: str) -> str:
|
|
|
347
328
|
"tool": name, "kind": kind, "status": v.status,
|
|
348
329
|
"reason": v.reason, "evidence": [e.to_dict() for e in v.evidence],
|
|
349
330
|
})
|
|
350
|
-
# 判定"可复用"以核验结论为准,而不是文本开头是不是 "(" ——
|
|
351
|
-
# 失败文本追加提示后就不再以 "(" 开头,靠形态判会把它当成功存进断点。
|
|
352
331
|
if v.status != _verify.FAILED and _step_ok(result):
|
|
353
332
|
_record_step(agent, call, result)
|
|
354
333
|
except Exception:
|
|
355
|
-
pass
|
|
334
|
+
pass
|
|
356
335
|
return text
|
|
357
336
|
|
|
358
337
|
|
|
@@ -381,8 +360,9 @@ def _run_tool_calls(agent, calls: list) -> list:
|
|
|
381
360
|
|
|
382
361
|
|
|
383
362
|
class ToolExecMixin:
|
|
384
|
-
|
|
385
|
-
|
|
363
|
+
AGENT_ID: str
|
|
364
|
+
router: Any
|
|
365
|
+
system_prompt: str
|
|
386
366
|
def _guard_tool(self, name: str, args: dict):
|
|
387
367
|
return _guard_tool(self, name, args)
|
|
388
368
|
|
|
@@ -414,30 +394,24 @@ class ToolExecMixin:
|
|
|
414
394
|
return None
|
|
415
395
|
conv = list(messages)
|
|
416
396
|
for _ in range(max_rounds):
|
|
417
|
-
# 上下文预算:工具轮每轮追加 assistant.tool_calls + tool 结果,不压会撑爆窗口
|
|
418
|
-
# (Compactor 保证 tool_calls 与其结果同进同出,缺口见 core/compaction.py:100)。
|
|
419
|
-
# getattr 兜底:本 mixin 也被只实现 router 的轻量对象复用(_compact_context
|
|
420
|
-
# 定义在 CognitiveAgent 上),缺该方法时退化为不压缩。
|
|
421
397
|
_compact = getattr(self, "_compact_context", None)
|
|
422
398
|
if _compact is not None:
|
|
423
399
|
conv = _compact(conv, "tool_loop")
|
|
424
400
|
if stop_event is not None and stop_event.is_set():
|
|
425
|
-
return None
|
|
401
|
+
return None
|
|
426
402
|
res = self.router.call_tools(self.system_prompt, conv, schemas,
|
|
427
403
|
agent_id=self.AGENT_ID)
|
|
428
404
|
if not res.get("supported"):
|
|
429
|
-
return None
|
|
405
|
+
return None
|
|
430
406
|
calls = res.get("tool_calls") or []
|
|
431
407
|
if not calls:
|
|
432
|
-
return res.get("text") or None
|
|
408
|
+
return res.get("text") or None
|
|
433
409
|
conv.append(res["assistant_msg"])
|
|
434
|
-
# 只读并发 / 写串行 / 单工具带超时 + 每步 checkpoint + 续跑复用已完成步骤
|
|
435
410
|
results = _run_tool_calls(self, calls)
|
|
436
411
|
for c, result in zip(calls, results):
|
|
437
412
|
conv.append(self.router.format_tool_result(c, result))
|
|
438
|
-
# 轮数超限 → 强制不再调工具、收敛出文本答案
|
|
439
413
|
final = self.router.call_tools(self.system_prompt, conv, schemas,
|
|
440
414
|
tool_choice="none", agent_id=self.AGENT_ID)
|
|
441
415
|
if not final.get("supported"):
|
|
442
|
-
return None
|
|
443
|
-
return final.get("text") or None
|
|
416
|
+
return None
|
|
417
|
+
return final.get("text") or None
|