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
|
@@ -21,17 +21,18 @@ import threading
|
|
|
21
21
|
import time
|
|
22
22
|
import uuid
|
|
23
23
|
from dataclasses import dataclass, field
|
|
24
|
+
from typing import Any
|
|
24
25
|
|
|
25
26
|
|
|
26
27
|
@dataclass
|
|
27
28
|
class SteeringItem:
|
|
28
29
|
item_id: str
|
|
29
|
-
requester: str
|
|
30
|
-
source_agent: str
|
|
31
|
-
payload: dict
|
|
30
|
+
requester: str
|
|
31
|
+
source_agent: str
|
|
32
|
+
payload: dict
|
|
32
33
|
created: float = field(default_factory=time.time)
|
|
33
|
-
leased_until: float = 0.0
|
|
34
|
-
lease_id: str = ""
|
|
34
|
+
leased_until: float = 0.0
|
|
35
|
+
lease_id: str = ""
|
|
35
36
|
|
|
36
37
|
@property
|
|
37
38
|
def is_leased(self) -> bool:
|
|
@@ -60,7 +61,7 @@ class SteeringQueue:
|
|
|
60
61
|
source_agent=source_agent, payload=payload)
|
|
61
62
|
return item_id
|
|
62
63
|
|
|
63
|
-
def lease(self, requester: str = None, max_items: int = 10) -> list[SteeringItem]:
|
|
64
|
+
def lease(self, requester: str | None = None, max_items: int = 10) -> list[SteeringItem]:
|
|
64
65
|
"""租走一批待处理结果(可按 requester 过滤)。
|
|
65
66
|
|
|
66
67
|
原子地:挑出未被租用(或租约已过期)的项,标记为 leased,返回。
|
|
@@ -69,7 +70,7 @@ class SteeringQueue:
|
|
|
69
70
|
with self._lock:
|
|
70
71
|
self._reclaim_expired()
|
|
71
72
|
now = time.time()
|
|
72
|
-
leased = []
|
|
73
|
+
leased: list[Any] = []
|
|
73
74
|
lease_id = uuid.uuid4().hex[:12]
|
|
74
75
|
for item in self._items.values():
|
|
75
76
|
if len(leased) >= max_items:
|
|
@@ -83,7 +84,7 @@ class SteeringQueue:
|
|
|
83
84
|
leased.append(item)
|
|
84
85
|
return leased
|
|
85
86
|
|
|
86
|
-
def ack(self, item_id: str, lease_id: str = None) -> bool:
|
|
87
|
+
def ack(self, item_id: str, lease_id: str | None = None) -> bool:
|
|
87
88
|
"""消费者确认处理完 → 移除该项。
|
|
88
89
|
|
|
89
90
|
校验 lease_id:只有持有当前租约的消费者能 ack(防止过期租约误删
|
|
@@ -94,7 +95,7 @@ class SteeringQueue:
|
|
|
94
95
|
if item is None:
|
|
95
96
|
return False
|
|
96
97
|
if lease_id is not None and item.lease_id != lease_id:
|
|
97
|
-
return False
|
|
98
|
+
return False
|
|
98
99
|
del self._items[item_id]
|
|
99
100
|
return True
|
|
100
101
|
|
|
@@ -114,7 +115,7 @@ class SteeringQueue:
|
|
|
114
115
|
item.leased_until = 0.0
|
|
115
116
|
item.lease_id = ""
|
|
116
117
|
|
|
117
|
-
def pending_count(self, requester: str = None) -> int:
|
|
118
|
+
def pending_count(self, requester: str | None = None) -> int:
|
|
118
119
|
"""待处理(未被有效租用)的项数。"""
|
|
119
120
|
with self._lock:
|
|
120
121
|
self._reclaim_expired()
|
|
@@ -145,7 +146,7 @@ class SteeringQueue:
|
|
|
145
146
|
if item.requester != requester:
|
|
146
147
|
continue
|
|
147
148
|
if item.is_leased:
|
|
148
|
-
continue
|
|
149
|
+
continue
|
|
149
150
|
out.append(item.payload)
|
|
150
151
|
to_del.append(item_id)
|
|
151
152
|
for item_id in to_del:
|
|
@@ -73,7 +73,7 @@ class SystemBus:
|
|
|
73
73
|
def publish(self, channel: str, payload: dict):
|
|
74
74
|
return self.spine.publish(channel, payload, source="system_bus")
|
|
75
75
|
|
|
76
|
-
def history(self, channel: str = None, limit: int = 50,
|
|
76
|
+
def history(self, channel: str | None = None, limit: int = 50,
|
|
77
77
|
exact: bool = False) -> list:
|
|
78
78
|
"""历史事件(dict 列表)。exact=True 时只返回 topic 完全匹配的事件
|
|
79
79
|
(默认前缀匹配,与 EventSpine 原生一致)。"""
|
|
@@ -16,16 +16,16 @@ from pathlib import Path
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
NodeType = Literal[
|
|
19
|
-
"goal",
|
|
20
|
-
"reasoning",
|
|
21
|
-
"memory_retrieval",
|
|
22
|
-
"tool_intent",
|
|
23
|
-
"tool_use",
|
|
24
|
-
"plan",
|
|
25
|
-
"decision",
|
|
26
|
-
"policy_update",
|
|
27
|
-
"consolidation",
|
|
28
|
-
"reflection",
|
|
19
|
+
"goal",
|
|
20
|
+
"reasoning",
|
|
21
|
+
"memory_retrieval",
|
|
22
|
+
"tool_intent",
|
|
23
|
+
"tool_use",
|
|
24
|
+
"plan",
|
|
25
|
+
"decision",
|
|
26
|
+
"policy_update",
|
|
27
|
+
"consolidation",
|
|
28
|
+
"reflection",
|
|
29
29
|
]
|
|
30
30
|
|
|
31
31
|
HarnessStatus = Literal["BASELINE", "PASS", "WARN", "BLOCK", "PENDING", "REGRESSION"]
|
|
@@ -39,15 +39,15 @@ class ThoughtNode:
|
|
|
39
39
|
agent_id: str
|
|
40
40
|
type: NodeType
|
|
41
41
|
content: str
|
|
42
|
-
conf: float
|
|
43
|
-
mem_refs: list[str]
|
|
44
|
-
parent_ids: list[str]
|
|
42
|
+
conf: float
|
|
43
|
+
mem_refs: list[str]
|
|
44
|
+
parent_ids: list[str]
|
|
45
45
|
harness_status: HarnessStatus
|
|
46
|
-
harness_reason: Optional[str]
|
|
47
|
-
harness_delta: Optional[float]
|
|
48
|
-
dominant_dim: Optional[str]
|
|
49
|
-
tool_name: Optional[str]
|
|
50
|
-
entities: list[str]
|
|
46
|
+
harness_reason: Optional[str]
|
|
47
|
+
harness_delta: Optional[float]
|
|
48
|
+
dominant_dim: Optional[str]
|
|
49
|
+
tool_name: Optional[str]
|
|
50
|
+
entities: list[str]
|
|
51
51
|
timestamp: str
|
|
52
52
|
metadata: dict
|
|
53
53
|
|
|
@@ -59,11 +59,11 @@ class ThoughtNode:
|
|
|
59
59
|
type: NodeType,
|
|
60
60
|
content: str,
|
|
61
61
|
conf: float = 1.0,
|
|
62
|
-
mem_refs: list[str] = None,
|
|
63
|
-
parent_ids: list[str] = None,
|
|
64
|
-
tool_name: str = None,
|
|
65
|
-
entities: list[str] = None,
|
|
66
|
-
metadata: dict = None,
|
|
62
|
+
mem_refs: list[str] | None = None,
|
|
63
|
+
parent_ids: list[str] | None = None,
|
|
64
|
+
tool_name: str | None = None,
|
|
65
|
+
entities: list[str] | None = None,
|
|
66
|
+
metadata: dict | None = None,
|
|
67
67
|
) -> "ThoughtNode":
|
|
68
68
|
return cls(
|
|
69
69
|
id=f"tn_{uuid.uuid4().hex[:12]}",
|
|
@@ -150,7 +150,7 @@ class ThoughtChain:
|
|
|
150
150
|
try:
|
|
151
151
|
conn.execute("PRAGMA journal_mode=WAL")
|
|
152
152
|
except sqlite3.OperationalError:
|
|
153
|
-
pass
|
|
153
|
+
pass
|
|
154
154
|
try:
|
|
155
155
|
yield conn
|
|
156
156
|
conn.commit()
|
|
@@ -183,9 +183,9 @@ class ThoughtChain:
|
|
|
183
183
|
self,
|
|
184
184
|
node_id: str,
|
|
185
185
|
status: HarnessStatus,
|
|
186
|
-
reason: str = None,
|
|
187
|
-
delta: float = None,
|
|
188
|
-
dominant_dim: str = None,
|
|
186
|
+
reason: str | None = None,
|
|
187
|
+
delta: float | None = None,
|
|
188
|
+
dominant_dim: str | None = None,
|
|
189
189
|
):
|
|
190
190
|
"""Harness 校验完成后更新节点标注"""
|
|
191
191
|
with self._conn() as conn:
|
|
@@ -202,7 +202,7 @@ class ThoughtChain:
|
|
|
202
202
|
).fetchone()
|
|
203
203
|
return self._row_to_node(row) if row else None
|
|
204
204
|
|
|
205
|
-
def get_chain(self, session_id: str, agent_id: str = None) -> list[ThoughtNode]:
|
|
205
|
+
def get_chain(self, session_id: str, agent_id: str | None = None) -> list[ThoughtNode]:
|
|
206
206
|
"""获取一个会话的完整推理链,按时间排序"""
|
|
207
207
|
with self._conn() as conn:
|
|
208
208
|
if agent_id:
|
|
@@ -217,7 +217,7 @@ class ThoughtChain:
|
|
|
217
217
|
).fetchall()
|
|
218
218
|
return [self._row_to_node(r) for r in rows]
|
|
219
219
|
|
|
220
|
-
def get_blocked_nodes(self, agent_id: str = None, limit: int = 50) -> list[ThoughtNode]:
|
|
220
|
+
def get_blocked_nodes(self, agent_id: str | None = None, limit: int = 50) -> list[ThoughtNode]:
|
|
221
221
|
"""获取被 Harness 截断的节点,用于审计"""
|
|
222
222
|
with self._conn() as conn:
|
|
223
223
|
if agent_id:
|
|
@@ -232,7 +232,7 @@ class ThoughtChain:
|
|
|
232
232
|
).fetchall()
|
|
233
233
|
return [self._row_to_node(r) for r in rows]
|
|
234
234
|
|
|
235
|
-
def search_nodes(self, keyword: str, agent_id: str = None,
|
|
235
|
+
def search_nodes(self, keyword: str, agent_id: str | None = None,
|
|
236
236
|
limit: int = 20) -> list[ThoughtNode]:
|
|
237
237
|
"""按关键词在推理节点内容里检索(LIKE 子串匹配)。供 HSR 的 AgentSourcePort
|
|
238
238
|
把「各 Agent 想过/判过什么」连通进内部搜索。只读。"""
|
|
@@ -307,8 +307,6 @@ class ThoughtChain:
|
|
|
307
307
|
"timestamp", "metadata"
|
|
308
308
|
]
|
|
309
309
|
d = dict(zip(cols, row))
|
|
310
|
-
# JSON 字段容错:单行某字段损坏时降级为默认值,而非让整个审计链读取崩溃。
|
|
311
|
-
# 审计系统的可靠性要求"一条坏数据不拖垮全部"。
|
|
312
310
|
for field, default in (("mem_refs", []), ("parent_ids", []),
|
|
313
311
|
("entities", []), ("metadata", {})):
|
|
314
312
|
try:
|
|
@@ -36,7 +36,6 @@ def estimate_tokens(text: str, model: str = "") -> int:
|
|
|
36
36
|
enc = tiktoken.get_encoding("cl100k_base")
|
|
37
37
|
return len(enc.encode(text))
|
|
38
38
|
except Exception:
|
|
39
|
-
# 字符估算:中文(非ASCII)按 1.1 字符/token,英文(ASCII)按 4 字符/token。
|
|
40
39
|
ascii_chars = sum(1 for c in text if ord(c) < 128)
|
|
41
40
|
cjk_chars = len(text) - ascii_chars
|
|
42
41
|
return int(ascii_chars / 4 + cjk_chars * 1.1) + 1
|
|
@@ -50,8 +49,8 @@ class CallRecord:
|
|
|
50
49
|
agent_id: str
|
|
51
50
|
input_tokens: int
|
|
52
51
|
output_tokens: int
|
|
53
|
-
estimated: bool
|
|
54
|
-
task_kind: str = ""
|
|
52
|
+
estimated: bool
|
|
53
|
+
task_kind: str = ""
|
|
55
54
|
|
|
56
55
|
@property
|
|
57
56
|
def total_tokens(self) -> int:
|
|
@@ -66,14 +65,14 @@ class CallRecord:
|
|
|
66
65
|
class TokenMeter:
|
|
67
66
|
"""Token 计量器。线程安全,可聚合,可落盘。"""
|
|
68
67
|
|
|
69
|
-
def __init__(self, log_path: str = None, max_records: int = 10000,
|
|
68
|
+
def __init__(self, log_path: str | None = None, max_records: int = 10000,
|
|
70
69
|
max_log_mb: float = 10.0):
|
|
71
70
|
self.log_path = log_path
|
|
72
71
|
self.max_records = max_records
|
|
73
72
|
self.max_log_bytes = int(max_log_mb * 1024 * 1024)
|
|
74
73
|
self._records: list[CallRecord] = []
|
|
75
74
|
self._lock = threading.RLock()
|
|
76
|
-
self._append_count = 0
|
|
75
|
+
self._append_count = 0
|
|
77
76
|
if log_path:
|
|
78
77
|
os.makedirs(os.path.dirname(log_path) or ".", exist_ok=True)
|
|
79
78
|
|
|
@@ -94,7 +93,7 @@ class TokenMeter:
|
|
|
94
93
|
|
|
95
94
|
def measure_call(self, *, system: str, messages: list, output: str,
|
|
96
95
|
provider: str, model: str, agent_id: str = "",
|
|
97
|
-
usage: dict = None, task_kind: str = "") -> CallRecord:
|
|
96
|
+
usage: dict | None = None, task_kind: str = "") -> CallRecord:
|
|
98
97
|
"""计量一次调用。优先用 API 返回的 usage,否则估算。
|
|
99
98
|
|
|
100
99
|
usage 形如 {"input_tokens": N, "output_tokens": M}(已由 router 归一化)。
|
|
@@ -104,7 +103,6 @@ class TokenMeter:
|
|
|
104
103
|
usage.get("input_tokens", 0),
|
|
105
104
|
usage.get("output_tokens", 0),
|
|
106
105
|
estimated=False, task_kind=task_kind)
|
|
107
|
-
# 估算:输入 = system + 所有 message content,输出 = output
|
|
108
106
|
input_text = (system or "") + "\n".join(
|
|
109
107
|
str(m.get("content", "")) for m in (messages or []))
|
|
110
108
|
in_tok = estimate_tokens(input_text, model)
|
|
@@ -112,7 +110,6 @@ class TokenMeter:
|
|
|
112
110
|
return self.record(provider, model, agent_id, in_tok, out_tok,
|
|
113
111
|
estimated=True, task_kind=task_kind)
|
|
114
112
|
|
|
115
|
-
# ── 聚合查询 ──────────────────────────────────────────
|
|
116
113
|
def summary(self) -> dict:
|
|
117
114
|
"""总体统计。"""
|
|
118
115
|
with self._lock:
|
|
@@ -211,8 +208,7 @@ class TokenMeter:
|
|
|
211
208
|
with open(self.log_path, "a", encoding="utf-8") as f:
|
|
212
209
|
f.write(json.dumps(rec.to_dict(), ensure_ascii=False) + "\n")
|
|
213
210
|
except Exception:
|
|
214
|
-
pass
|
|
215
|
-
# 每 100 次追加检查一次文件大小,超限则轮转(防磁盘无限膨胀)
|
|
211
|
+
pass
|
|
216
212
|
self._append_count += 1
|
|
217
213
|
if self._append_count % 100 == 0:
|
|
218
214
|
self._rotate_if_needed()
|
|
@@ -226,10 +222,10 @@ class TokenMeter:
|
|
|
226
222
|
return
|
|
227
223
|
with open(self.log_path, encoding="utf-8") as f:
|
|
228
224
|
lines = f.readlines()
|
|
229
|
-
keep = lines[len(lines) // 2:]
|
|
225
|
+
keep = lines[len(lines) // 2:]
|
|
230
226
|
tmp = self.log_path + ".tmp"
|
|
231
227
|
with open(tmp, "w", encoding="utf-8") as f:
|
|
232
228
|
f.writelines(keep)
|
|
233
|
-
os.replace(tmp, self.log_path)
|
|
229
|
+
os.replace(tmp, self.log_path)
|
|
234
230
|
except Exception:
|
|
235
|
-
pass
|
|
231
|
+
pass
|
|
@@ -15,6 +15,7 @@ Foxclaw — 工具注册表(ToolRegistry / P2 function-calling)
|
|
|
15
15
|
"""
|
|
16
16
|
from __future__ import annotations
|
|
17
17
|
import os
|
|
18
|
+
from typing import Any
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
def _tool(name: str, description: str, properties: dict, required: list) -> dict:
|
|
@@ -33,7 +34,6 @@ def _tool(name: str, description: str, properties: dict, required: list) -> dict
|
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
|
|
36
|
-
# ── 首批工具声明(高价值、正好替掉现有关键词坑)──────────────────────────────
|
|
37
37
|
WEB_SEARCH = _tool(
|
|
38
38
|
"web_search",
|
|
39
39
|
"联网搜索实时/时效性的**外部**信息:新闻、产品的版本与型号、发布动态、价格、汇率、"
|
|
@@ -113,15 +113,15 @@ class ToolRegistry:
|
|
|
113
113
|
for k in required:
|
|
114
114
|
if k not in args or args[k] in (None, ""):
|
|
115
115
|
errs.append(f"缺少必填参数 {k}")
|
|
116
|
-
_tmap = {"string": str, "integer": int, "number": (int, float),
|
|
116
|
+
_tmap: dict[str, Any] = {"string": str, "integer": int, "number": (int, float),
|
|
117
117
|
"boolean": bool, "array": list, "object": dict}
|
|
118
118
|
for k, v in args.items():
|
|
119
119
|
spec = props.get(k)
|
|
120
120
|
if not isinstance(spec, dict):
|
|
121
121
|
continue
|
|
122
|
-
|
|
122
|
+
stype = spec.get("type")
|
|
123
|
+
want = _tmap.get(stype) if isinstance(stype, str) else None
|
|
123
124
|
if not want or not isinstance(v, want):
|
|
124
|
-
# bool 是 int 的子类,须排除,否则 True 会被当成合法 integer
|
|
125
125
|
if not (want is not bool and isinstance(v, bool)):
|
|
126
126
|
errs.append(f"参数 {k} 类型应为 {spec.get('type')},"
|
|
127
127
|
f"实为 {type(v).__name__}")
|
|
@@ -143,20 +143,12 @@ def default_registry() -> ToolRegistry:
|
|
|
143
143
|
return ToolRegistry().register(WEB_SEARCH).register(RECALL_MEMORY)
|
|
144
144
|
|
|
145
145
|
|
|
146
|
-
# ── 工具能力分级(供 permission.evaluate 四阶段管线判定)────────────────────────
|
|
147
|
-
# 病根提醒:prompt 级隔离挡不住弱模型被块内强指令诱导(实测 flash ~2/3 被劫持),真正的
|
|
148
|
-
# 安全阀在**动作级**——工具执行前过权限管线,写/副作用/不可逆动作走人在回路(HITL)。
|
|
149
|
-
# 字段对齐 permission.evaluate:side_effect(read/write/send/pay)/ read_only / external / destructive。
|
|
150
|
-
# parallel_safe 不在权限语义里,只用于**同轮多工具是否可并发**:置 True 表示"纯读、可与其他
|
|
151
|
-
# parallel_safe 工具同时跑"(见 agents/mixins/tool_exec.py::_run_tools)。未声明 = 串行。
|
|
152
|
-
# **新增任何工具都应在此声明能力;未声明者 fail-closed 当作有副作用 → 强制 HITL(见 _DEFAULT_CAP)。**
|
|
153
146
|
TOOL_CAPABILITIES: dict[str, dict] = {
|
|
154
147
|
"web_search": {"side_effect": "read", "read_only": True, "external": False,
|
|
155
148
|
"destructive": False, "parallel_safe": True},
|
|
156
149
|
"recall_memory": {"side_effect": "read", "read_only": True, "external": False,
|
|
157
150
|
"destructive": False, "parallel_safe": True},
|
|
158
151
|
}
|
|
159
|
-
# 未声明工具的兜底:当有副作用 + 外部 → permission 管线判 ask → 强制人在回路。宁误拦不漏放。
|
|
160
152
|
_DEFAULT_CAP = {"side_effect": "write", "read_only": False, "external": True, "destructive": False}
|
|
161
153
|
|
|
162
154
|
|
|
@@ -29,14 +29,11 @@ import contextvars
|
|
|
29
29
|
from collections import deque
|
|
30
30
|
from contextlib import contextmanager
|
|
31
31
|
from . import settings
|
|
32
|
+
from typing import Any
|
|
32
33
|
|
|
33
34
|
logger = logging.getLogger(__name__)
|
|
34
35
|
|
|
35
36
|
|
|
36
|
-
# ── 采样策略 ────────────────────────────────────────────────
|
|
37
|
-
# 内存环存只留最近 N 条、重启即失 → 查不了"昨天那次慢请求"。持久化解决它,但不能无脑全存:
|
|
38
|
-
# 正常请求的 trace 数量大、价值低。故:**慢的 / 出错的必存**(那才是你要查的),
|
|
39
|
-
# 其余按采样率(默认 0 —— 个人 OS 留下的每条都值得看)。
|
|
40
37
|
def _slow_ms() -> float:
|
|
41
38
|
return float(settings.get_safe("FOXCLAW_TRACE_SLOW_MS"))
|
|
42
39
|
|
|
@@ -50,14 +47,9 @@ def _retain() -> int:
|
|
|
50
47
|
|
|
51
48
|
_current: contextvars.ContextVar = contextvars.ContextVar("foxclaw_trace", default=None)
|
|
52
49
|
|
|
53
|
-
MAX_SPANS_PER_TRACE = 200
|
|
50
|
+
MAX_SPANS_PER_TRACE = 200
|
|
54
51
|
|
|
55
52
|
|
|
56
|
-
# ── W3C Trace Context(零依赖跨进程传播)────────────────────────
|
|
57
|
-
# 为什么不上 opentelemetry-sdk:Foxclaw 是单进程 + local-first,为它装 SDK/exporter/grpc、
|
|
58
|
-
# 再要求跑个 Jaeger,代价远大于收益。但它确实有一条真实跨进程边界——**MCP 双向**。
|
|
59
|
-
# traceparent 是 W3C 标准、纯字符串、零依赖,恰好覆盖这条边界:
|
|
60
|
-
# 00-{trace_id:32hex}-{span_id:16hex}-{flags:2hex}
|
|
61
53
|
_TP_RE = __import__("re").compile(r"^00-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$")
|
|
62
54
|
|
|
63
55
|
|
|
@@ -69,12 +61,12 @@ def parse_traceparent(header: str):
|
|
|
69
61
|
if not m:
|
|
70
62
|
return None
|
|
71
63
|
tid = m.group(1)
|
|
72
|
-
if tid == "0" * 32:
|
|
64
|
+
if tid == "0" * 32:
|
|
73
65
|
return None
|
|
74
66
|
return tid
|
|
75
67
|
|
|
76
68
|
|
|
77
|
-
def to_traceparent(trace_id: str, span_id: str = None) -> str:
|
|
69
|
+
def to_traceparent(trace_id: str, span_id: str | None = None) -> str:
|
|
78
70
|
"""给出站请求生成 traceparent。span_id 缺省随机(本进程作为对端的 parent)。"""
|
|
79
71
|
tid = (trace_id or uuid.uuid4().hex)[:32].rjust(32, "0")
|
|
80
72
|
sid = (span_id or uuid.uuid4().hex[:16])[:16].rjust(16, "0")
|
|
@@ -88,13 +80,12 @@ def current_traceparent():
|
|
|
88
80
|
|
|
89
81
|
|
|
90
82
|
class Trace:
|
|
91
|
-
def __init__(self, name: str, trace_id: str = None):
|
|
92
|
-
# 32 hex:直接符合 W3C traceparent 的 trace-id 规格(跨进程可对齐)
|
|
83
|
+
def __init__(self, name: str, trace_id: str | None = None):
|
|
93
84
|
self.trace_id = trace_id or uuid.uuid4().hex
|
|
94
85
|
self.name = name
|
|
95
86
|
self.start_ts = time.time()
|
|
96
87
|
self._t0 = time.perf_counter()
|
|
97
|
-
self.duration_ms = None
|
|
88
|
+
self.duration_ms: float | None = None
|
|
98
89
|
self.spans: list = []
|
|
99
90
|
self._lock = threading.Lock()
|
|
100
91
|
self._dropped = 0
|
|
@@ -135,9 +126,8 @@ class TraceStore:
|
|
|
135
126
|
def __init__(self, limit: int = 200):
|
|
136
127
|
self._d: deque = deque(maxlen=limit)
|
|
137
128
|
self._lock = threading.Lock()
|
|
138
|
-
self._db_path = None
|
|
129
|
+
self._db_path: str | None = None
|
|
139
130
|
|
|
140
|
-
# ── 持久化 ───────────────────────────────────────────
|
|
141
131
|
def attach_db(self, path):
|
|
142
132
|
"""开启持久化并建表。连接**即建即弃、不跨线程复用**(与 ThoughtChain 同款):
|
|
143
133
|
SQLite 连接不可跨线程共享,而 trace 写入发生在请求线程/to_thread 工作线程。"""
|
|
@@ -154,19 +144,21 @@ class TraceStore:
|
|
|
154
144
|
self._db_path = None
|
|
155
145
|
|
|
156
146
|
def _conn(self):
|
|
147
|
+
if self._db_path is None:
|
|
148
|
+
raise RuntimeError("trace 持久化未启用:先 attach_db")
|
|
157
149
|
conn = sqlite3.connect(self._db_path, timeout=5.0)
|
|
158
150
|
try:
|
|
159
|
-
conn.execute("PRAGMA journal_mode=WAL")
|
|
151
|
+
conn.execute("PRAGMA journal_mode=WAL")
|
|
160
152
|
except sqlite3.OperationalError:
|
|
161
153
|
pass
|
|
162
154
|
return conn
|
|
163
155
|
|
|
164
156
|
def _should_persist(self, tr: Trace) -> bool:
|
|
165
157
|
if tr.has_error():
|
|
166
|
-
return True
|
|
158
|
+
return True
|
|
167
159
|
if (tr.duration_ms or 0) >= _slow_ms():
|
|
168
|
-
return True
|
|
169
|
-
return random.random() < _sample_rate()
|
|
160
|
+
return True
|
|
161
|
+
return random.random() < _sample_rate()
|
|
170
162
|
|
|
171
163
|
def _persist(self, tr: Trace):
|
|
172
164
|
d = tr.to_dict()
|
|
@@ -176,7 +168,6 @@ class TraceStore:
|
|
|
176
168
|
(tr.trace_id, tr.name, tr.start_ts, tr.duration_ms,
|
|
177
169
|
d["span_count"], 0 if tr.has_error() else 1,
|
|
178
170
|
json.dumps(d["spans"], ensure_ascii=False)))
|
|
179
|
-
# 保留上限:超出删最旧 → 有界增长(不会把磁盘吃光)
|
|
180
171
|
c.execute("DELETE FROM traces WHERE trace_id IN ("
|
|
181
172
|
"SELECT trace_id FROM traces ORDER BY start_ts DESC "
|
|
182
173
|
"LIMIT -1 OFFSET ?)", (_retain(),))
|
|
@@ -187,10 +178,10 @@ class TraceStore:
|
|
|
187
178
|
with self._lock:
|
|
188
179
|
self._d.append(tr)
|
|
189
180
|
if self._db_path and self._should_persist(tr):
|
|
190
|
-
self._persist(tr)
|
|
181
|
+
self._persist(tr)
|
|
191
182
|
try:
|
|
192
|
-
OTLP.submit(tr)
|
|
193
|
-
except Exception:
|
|
183
|
+
OTLP.submit(tr)
|
|
184
|
+
except Exception:
|
|
194
185
|
pass
|
|
195
186
|
|
|
196
187
|
def get(self, trace_id: str):
|
|
@@ -219,7 +210,7 @@ class TraceStore:
|
|
|
219
210
|
"duration_ms": row[3], "span_count": row[4],
|
|
220
211
|
"spans": json.loads(row[5] or "[]"), "persisted": True}
|
|
221
212
|
|
|
222
|
-
def query(self, limit: int = 30, slow_ms: float = None, errors_only: bool = False):
|
|
213
|
+
def query(self, limit: int = 30, slow_ms: float | None = None, errors_only: bool = False):
|
|
223
214
|
"""查已落库的历史 trace(内存环之外)。这才是"找出昨天那次慢请求"的入口。"""
|
|
224
215
|
if not self._db_path:
|
|
225
216
|
return []
|
|
@@ -250,10 +241,6 @@ class TraceStore:
|
|
|
250
241
|
self._d.clear()
|
|
251
242
|
|
|
252
243
|
|
|
253
|
-
# ── OTLP/HTTP-JSON 导出(可选、零依赖)────────────────────────
|
|
254
|
-
# 想接 Jaeger/Tempo/collector 的人设 FOXCLAW_OTLP_ENDPOINT 即可;不设则完全不启用、零成本。
|
|
255
|
-
# 刻意**不引 opentelemetry-sdk**:OTLP 有 HTTP+JSON 传输,stdlib urllib 就能发。
|
|
256
|
-
# 铁律:遥测**绝不能拖慢请求** —— 后台守护线程 + 有界队列,满了就丢(宁丢遥测不拖请求)。
|
|
257
244
|
def _otlp_endpoint() -> str:
|
|
258
245
|
return os.environ.get("FOXCLAW_OTLP_ENDPOINT", "").strip().rstrip("/")
|
|
259
246
|
|
|
@@ -272,16 +259,16 @@ def _trace_to_otlp(tr: "Trace") -> dict:
|
|
|
272
259
|
root_id = uuid.uuid4().hex[:16]
|
|
273
260
|
dur = (tr.duration_ms or 0.0) / 1000.0
|
|
274
261
|
spans = [{
|
|
275
|
-
"traceId": tr.trace_id, "spanId": root_id, "name": tr.name, "kind": 2,
|
|
262
|
+
"traceId": tr.trace_id, "spanId": root_id, "name": tr.name, "kind": 2,
|
|
276
263
|
"startTimeUnixNano": t_ns(tr.start_ts),
|
|
277
264
|
"endTimeUnixNano": t_ns(tr.start_ts + dur),
|
|
278
265
|
"status": {"code": 2 if tr.has_error() else 1},
|
|
279
266
|
}]
|
|
280
267
|
for s in tr.to_dict()["spans"]:
|
|
281
|
-
st = s.get("ts", tr.start_ts)
|
|
268
|
+
st = s.get("ts", tr.start_ts)
|
|
282
269
|
spans.append({
|
|
283
270
|
"traceId": tr.trace_id, "spanId": uuid.uuid4().hex[:16], "parentSpanId": root_id,
|
|
284
|
-
"name": s.get("name", "span"), "kind": 1,
|
|
271
|
+
"name": s.get("name", "span"), "kind": 1,
|
|
285
272
|
"startTimeUnixNano": t_ns(st),
|
|
286
273
|
"endTimeUnixNano": t_ns(st + (s.get("ms", 0.0) / 1000.0)),
|
|
287
274
|
"attributes": _attrs(s.get("meta")),
|
|
@@ -298,8 +285,8 @@ class _OtlpExporter:
|
|
|
298
285
|
|
|
299
286
|
def __init__(self, maxsize: int = 512):
|
|
300
287
|
import queue
|
|
301
|
-
self._q = queue.Queue(maxsize=maxsize)
|
|
302
|
-
self._th = None
|
|
288
|
+
self._q: queue.Queue[Any] = queue.Queue(maxsize=maxsize)
|
|
289
|
+
self._th: threading.Thread | None = None
|
|
303
290
|
self._lock = threading.Lock()
|
|
304
291
|
self.dropped = 0
|
|
305
292
|
self.sent = 0
|
|
@@ -319,7 +306,7 @@ class _OtlpExporter:
|
|
|
319
306
|
try:
|
|
320
307
|
self._q.put_nowait(tr)
|
|
321
308
|
except queue.Full:
|
|
322
|
-
self.dropped += 1
|
|
309
|
+
self.dropped += 1
|
|
323
310
|
|
|
324
311
|
def _run(self):
|
|
325
312
|
import urllib.request
|
|
@@ -338,7 +325,7 @@ class _OtlpExporter:
|
|
|
338
325
|
self.sent += 1
|
|
339
326
|
except Exception as e:
|
|
340
327
|
self.failed += 1
|
|
341
|
-
if self.failed <= 3:
|
|
328
|
+
if self.failed <= 3:
|
|
342
329
|
logger.warning("OTLP 导出失败(不影响请求): %s", e)
|
|
343
330
|
|
|
344
331
|
def stats(self) -> dict:
|
|
@@ -351,7 +338,7 @@ OTLP = _OtlpExporter()
|
|
|
351
338
|
TRACES = TraceStore()
|
|
352
339
|
|
|
353
340
|
|
|
354
|
-
def start_trace(name: str, trace_id: str = None) -> Trace:
|
|
341
|
+
def start_trace(name: str, trace_id: str | None = None) -> Trace:
|
|
355
342
|
"""开一条 trace 并绑到当前上下文(由 API 中间件按请求调用)。"""
|
|
356
343
|
tr = Trace(name, trace_id)
|
|
357
344
|
_current.set(tr)
|
|
@@ -375,8 +362,6 @@ def span(name: str, **meta):
|
|
|
375
362
|
yield None
|
|
376
363
|
return
|
|
377
364
|
t0 = time.perf_counter()
|
|
378
|
-
# ts:span 开始的 epoch 秒。内部只需 ms 就够,但 OTLP 要求每个 span 有 start/end 绝对时刻,
|
|
379
|
-
# 故一并记下(开销可忽略;缺失时导出侧会优雅兜底)。
|
|
380
365
|
rec = {"name": name, "ts": time.time(), "ok": True,
|
|
381
366
|
**({"meta": meta} if meta else {})}
|
|
382
367
|
try:
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"""
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
from datetime import datetime
|
|
19
|
+
from typing import Any
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
class TruthPipeline:
|
|
@@ -34,13 +35,10 @@ class TruthPipeline:
|
|
|
34
35
|
|
|
35
36
|
def __init__(self, bus, hmr=None):
|
|
36
37
|
self.bus = bus
|
|
37
|
-
self.hmr = hmr
|
|
38
|
+
self.hmr = hmr
|
|
38
39
|
self.last_trace = None
|
|
39
40
|
self.last_memory_update = None
|
|
40
41
|
|
|
41
|
-
# 订阅事件链。【P4-1】三个都是精确 channel 名 → exact=True:
|
|
42
|
-
# 门面化后 SystemBus 默认是前缀匹配,不锁定的话 "execution.completed.extra"
|
|
43
|
-
# 之类的前缀事件也会触发真值链(语义漂移)。
|
|
44
42
|
bus.subscribe("execution.completed", self._on_execution_completed, exact=True)
|
|
45
43
|
bus.subscribe("trace.recorded", self._on_trace_recorded, exact=True)
|
|
46
44
|
bus.subscribe("truth.appended", self._on_truth_appended, exact=True)
|
|
@@ -72,7 +70,6 @@ class TruthPipeline:
|
|
|
72
70
|
self.last_memory_update = {"persisted": False, "reason": "blocked"}
|
|
73
71
|
self.bus.publish("truth.skipped", {**p, "reason": "blocked"})
|
|
74
72
|
return
|
|
75
|
-
# 真值 = "任务被如此处理"的事实 + 产出要点(信息量足够供日后召回)
|
|
76
73
|
result_brief = str(p.get("result", ""))[:200]
|
|
77
74
|
truth = {
|
|
78
75
|
"fact": (f"任务「{p.get('task')}」由 {p.get('agent_id')} 处理。"
|
|
@@ -93,18 +90,17 @@ class TruthPipeline:
|
|
|
93
90
|
【P3-5】双保险:_on_trace_recorded 已挡 blocked,这里再检查一次。
|
|
94
91
|
"""
|
|
95
92
|
p = msg.payload
|
|
96
|
-
update = {"persisted": False}
|
|
93
|
+
update: dict[str, Any] = {"persisted": False}
|
|
97
94
|
if self.hmr is not None and not p.get("blocked"):
|
|
98
95
|
try:
|
|
99
|
-
# 沉淀到 HMR 共享层(所有 Agent 可见),用真实的 ingest 接口
|
|
100
96
|
content = p.get("truth", {}).get("fact", "")
|
|
101
97
|
origin_agent = p.get("agent_id", "unknown")
|
|
102
98
|
self.hmr.ingest(
|
|
103
99
|
content,
|
|
104
100
|
type="execution",
|
|
105
|
-
agent_id="system",
|
|
101
|
+
agent_id="system",
|
|
106
102
|
title=f"真值_{datetime.now().strftime('%Y%m%d_%H%M%S')}",
|
|
107
|
-
tags=["truth_pipeline", f"origin:{origin_agent}"],
|
|
103
|
+
tags=["truth_pipeline", f"origin:{origin_agent}"],
|
|
108
104
|
shared=True,
|
|
109
105
|
)
|
|
110
106
|
update = {"persisted": True, "content": content}
|