arkaos 4.0.2 → 4.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/VERSION +1 -1
- package/arka/SKILL.md +16 -22
- package/arka/skills/flow/SKILL.md +108 -234
- package/config/claude-agents/eduardo-copy.md +53 -0
- package/config/claude-agents/francisca-tech.md +52 -0
- package/config/claude-agents/marta-cqo.md +52 -0
- package/config/claude-agents/paulo-tech-lead.md +50 -0
- package/config/cognition/schedules.yaml +15 -0
- package/config/constitution.yaml +17 -21
- package/config/hooks/_lib/workflow-classifier.sh +1 -1
- package/config/hooks/post-tool-use.sh +52 -536
- package/config/hooks/pre-tool-use.sh +46 -280
- package/config/hooks/session-start.sh +8 -10
- package/config/hooks/stop.sh +44 -424
- package/config/hooks/user-prompt-submit.sh +55 -457
- package/core/__pycache__/__init__.cpython-314.pyc +0 -0
- package/core/cognition/__pycache__/dreaming.cpython-313.pyc +0 -0
- package/core/cognition/dreaming.py +5 -0
- package/core/cognition/scheduler/__pycache__/daemon.cpython-313.pyc +0 -0
- package/core/cognition/scheduler/daemon.py +10 -0
- package/core/forge/__init__.py +2 -0
- package/core/forge/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/forge/__pycache__/orchestrator.cpython-313.pyc +0 -0
- package/core/forge/__pycache__/persistence.cpython-313.pyc +0 -0
- package/core/forge/__pycache__/renderer.cpython-313.pyc +0 -0
- package/core/forge/__pycache__/runtime_dispatcher.cpython-313.pyc +0 -0
- package/core/forge/__pycache__/schema.cpython-313.pyc +0 -0
- package/core/forge/orchestrator.py +50 -27
- package/core/forge/persistence.py +16 -0
- package/core/forge/renderer.py +16 -0
- package/core/forge/runtime_dispatcher.py +34 -19
- package/core/forge/schema.py +12 -0
- package/core/governance/__pycache__/__init__.cpython-312.pyc +0 -0
- package/core/governance/__pycache__/__init__.cpython-314.pyc +0 -0
- package/core/governance/__pycache__/closing_marker_check.cpython-313.pyc +0 -0
- package/core/governance/__pycache__/closing_marker_check.cpython-314.pyc +0 -0
- package/core/governance/__pycache__/compliance_telemetry.cpython-313.pyc +0 -0
- package/core/governance/__pycache__/constitution.cpython-312.pyc +0 -0
- package/core/governance/__pycache__/constitution.cpython-314.pyc +0 -0
- package/core/governance/__pycache__/evidence_checks.cpython-313.pyc +0 -0
- package/core/governance/__pycache__/kb_cite_check.cpython-314.pyc +0 -0
- package/core/governance/__pycache__/meta_tag_check.cpython-313.pyc +0 -0
- package/core/governance/__pycache__/meta_tag_check.cpython-314.pyc +0 -0
- package/core/governance/__pycache__/qg_verdict.cpython-313.pyc +0 -0
- package/core/governance/__pycache__/review_workflow.cpython-313.pyc +0 -0
- package/core/governance/__pycache__/skill_proposer.cpython-313.pyc +0 -0
- package/core/governance/__pycache__/skill_proposer.cpython-314.pyc +0 -0
- package/core/governance/__pycache__/sycophancy_detector.cpython-314.pyc +0 -0
- package/core/governance/closing_marker_check.py +9 -5
- package/core/governance/compliance_telemetry.py +1 -1
- package/core/governance/compliance_telemetry_cli.py +1 -1
- package/core/governance/evidence_checks.py +421 -0
- package/core/governance/meta_tag_check.py +36 -0
- package/core/governance/qg_verdict.py +78 -0
- package/core/governance/review_workflow.py +34 -1
- package/core/governance/skill_proposer.py +2 -1
- package/core/hooks/__init__.py +12 -0
- package/core/hooks/__pycache__/__init__.cpython-312.pyc +0 -0
- package/core/hooks/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/hooks/__pycache__/__init__.cpython-314.pyc +0 -0
- package/core/hooks/__pycache__/_shared.cpython-312.pyc +0 -0
- package/core/hooks/__pycache__/_shared.cpython-313.pyc +0 -0
- package/core/hooks/__pycache__/_shared.cpython-314.pyc +0 -0
- package/core/hooks/__pycache__/post_tool_use.cpython-312.pyc +0 -0
- package/core/hooks/__pycache__/post_tool_use.cpython-313.pyc +0 -0
- package/core/hooks/__pycache__/pre_tool_use.cpython-312.pyc +0 -0
- package/core/hooks/__pycache__/pre_tool_use.cpython-313.pyc +0 -0
- package/core/hooks/__pycache__/pre_tool_use.cpython-314.pyc +0 -0
- package/core/hooks/__pycache__/stop.cpython-312.pyc +0 -0
- package/core/hooks/__pycache__/stop.cpython-313.pyc +0 -0
- package/core/hooks/__pycache__/stop.cpython-314.pyc +0 -0
- package/core/hooks/__pycache__/user_prompt_submit.cpython-312.pyc +0 -0
- package/core/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
- package/core/hooks/_shared.py +110 -0
- package/core/hooks/post_tool_use.py +689 -0
- package/core/hooks/pre_tool_use.py +267 -0
- package/core/hooks/stop.py +395 -0
- package/core/hooks/user_prompt_submit.py +600 -0
- package/core/jobs/__pycache__/__init__.cpython-312.pyc +0 -0
- package/core/jobs/__pycache__/__init__.cpython-314.pyc +0 -0
- package/core/jobs/__pycache__/auto_doc_worker.cpython-312.pyc +0 -0
- package/core/jobs/__pycache__/auto_doc_worker.cpython-314.pyc +0 -0
- package/core/jobs/__pycache__/manager.cpython-312.pyc +0 -0
- package/core/jobs/__pycache__/manager.cpython-314.pyc +0 -0
- package/core/knowledge/__pycache__/embedder.cpython-312.pyc +0 -0
- package/core/knowledge/__pycache__/embedder.cpython-313.pyc +0 -0
- package/core/knowledge/__pycache__/vector_store.cpython-312.pyc +0 -0
- package/core/knowledge/__pycache__/vector_store.cpython-313.pyc +0 -0
- package/core/knowledge/embedder.py +118 -11
- package/core/knowledge/vector_store.py +111 -14
- package/core/memory/__pycache__/session_store.cpython-314.pyc +0 -0
- package/core/obsidian/__pycache__/relator.cpython-313.pyc +0 -0
- package/core/obsidian/relator.py +18 -8
- package/core/runtime/__pycache__/__init__.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/__init__.cpython-314.pyc +0 -0
- package/core/runtime/__pycache__/base.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/base.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/base.cpython-314.pyc +0 -0
- package/core/runtime/__pycache__/capabilities_cli.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/capabilities_cli.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/claude_code.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/claude_code.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/claude_code.cpython-314.pyc +0 -0
- package/core/runtime/__pycache__/codex_cli.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/codex_cli.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/codex_cli.cpython-314.pyc +0 -0
- package/core/runtime/__pycache__/cost_governor.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/cost_governor.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/cursor.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/cursor.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/cursor.cpython-314.pyc +0 -0
- package/core/runtime/__pycache__/gemini_cli.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/gemini_cli.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/gemini_cli.cpython-314.pyc +0 -0
- package/core/runtime/__pycache__/llm_cost_telemetry.cpython-314.pyc +0 -0
- package/core/runtime/__pycache__/llm_provider.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/llm_provider.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/llm_provider.cpython-314.pyc +0 -0
- package/core/runtime/__pycache__/llm_retry.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/llm_retry.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/llm_retry.cpython-314.pyc +0 -0
- package/core/runtime/__pycache__/native_usage.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/native_usage.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/native_usage.cpython-314.pyc +0 -0
- package/core/runtime/__pycache__/ollama_provider.cpython-314.pyc +0 -0
- package/core/runtime/__pycache__/path_resolver.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/pricing.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/pricing.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/pricing.cpython-314.pyc +0 -0
- package/core/runtime/__pycache__/registry.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/registry.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/registry.cpython-314.pyc +0 -0
- package/core/runtime/__pycache__/squad_orchestrator.cpython-312.pyc +0 -0
- package/core/runtime/base.py +29 -0
- package/core/runtime/capabilities_cli.py +74 -0
- package/core/runtime/claude_code.py +8 -0
- package/core/runtime/codex_cli.py +205 -37
- package/core/runtime/cost_governor.py +184 -0
- package/core/runtime/cursor.py +8 -0
- package/core/runtime/gemini_cli.py +8 -0
- package/core/runtime/llm_provider.py +32 -13
- package/core/runtime/llm_retry.py +233 -0
- package/core/runtime/native_usage.py +186 -0
- package/core/runtime/pricing.py +9 -0
- package/core/runtime/registry.py +3 -0
- package/core/shared/__pycache__/__init__.cpython-314.pyc +0 -0
- package/core/shared/__pycache__/safe_session_id.cpython-314.pyc +0 -0
- package/core/shared/__pycache__/test_evidence.cpython-313-pytest-9.1.1.pyc +0 -0
- package/core/shared/__pycache__/test_evidence.cpython-313.pyc +0 -0
- package/core/shared/test_evidence.py +34 -0
- package/core/synapse/__pycache__/__init__.cpython-314.pyc +0 -0
- package/core/synapse/__pycache__/agent_experiences_layer.cpython-312.pyc +0 -0
- package/core/synapse/__pycache__/cache.cpython-314.pyc +0 -0
- package/core/synapse/__pycache__/engine.cpython-312.pyc +0 -0
- package/core/synapse/__pycache__/engine.cpython-313.pyc +0 -0
- package/core/synapse/__pycache__/engine.cpython-314.pyc +0 -0
- package/core/synapse/__pycache__/graph_context_layer.cpython-313.pyc +0 -0
- package/core/synapse/__pycache__/kb_cache.cpython-312.pyc +0 -0
- package/core/synapse/__pycache__/kb_cache.cpython-313.pyc +0 -0
- package/core/synapse/__pycache__/kb_cache.cpython-314.pyc +0 -0
- package/core/synapse/__pycache__/layers.cpython-312.pyc +0 -0
- package/core/synapse/__pycache__/layers.cpython-313.pyc +0 -0
- package/core/synapse/__pycache__/layers.cpython-314.pyc +0 -0
- package/core/synapse/engine.py +7 -0
- package/core/synapse/graph_context_layer.py +212 -0
- package/core/synapse/kb_cache.py +3 -0
- package/core/synapse/layers.py +104 -20
- package/core/workflow/__pycache__/__init__.cpython-314.pyc +0 -0
- package/core/workflow/__pycache__/enforcer.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/engine.cpython-314.pyc +0 -0
- package/core/workflow/__pycache__/flow_enforcer.cpython-312.pyc +0 -0
- package/core/workflow/__pycache__/flow_enforcer.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/flow_enforcer.cpython-314.pyc +0 -0
- package/core/workflow/__pycache__/gate_checkpoint.cpython-312.pyc +0 -0
- package/core/workflow/__pycache__/gate_checkpoint.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/gate_checkpoint.cpython-314.pyc +0 -0
- package/core/workflow/__pycache__/loader.cpython-314.pyc +0 -0
- package/core/workflow/__pycache__/marker_cache.cpython-314.pyc +0 -0
- package/core/workflow/__pycache__/research_gate.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/research_gate.cpython-314.pyc +0 -0
- package/core/workflow/__pycache__/rules_registry.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/schema.cpython-314.pyc +0 -0
- package/core/workflow/__pycache__/specialist_enforcer.cpython-312.pyc +0 -0
- package/core/workflow/__pycache__/specialist_enforcer.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/state.cpython-314.pyc +0 -0
- package/core/workflow/enforcer.py +5 -16
- package/core/workflow/flow_enforcer.py +36 -11
- package/core/workflow/gate_checkpoint.py +149 -0
- package/core/workflow/research_gate.py +15 -3
- package/core/workflow/rules_registry.py +137 -389
- package/core/workflow/specialist_enforcer.py +21 -4
- package/dashboard/app/pages/knowledge/index.vue +4 -1
- package/departments/ops/skills/n8n-flow/SKILL.md +13 -0
- package/departments/ops/skills/workflow-automate/SKILL.md +16 -0
- package/departments/quality/SKILL.md +45 -5
- package/departments/quality/agents/copy-director.yaml +3 -1
- package/departments/quality/agents/tech-director.yaml +3 -1
- package/installer/adapters/claude-code.js +46 -2
- package/installer/config-seed.js +39 -14
- package/installer/doctor.js +8 -0
- package/installer/graphify.js +153 -0
- package/installer/index.js +20 -0
- package/installer/init.js +11 -0
- package/installer/update.js +19 -0
- package/package.json +1 -1
- package/pyproject.toml +3 -1
- package/scripts/__pycache__/dashboard-api.cpython-313.pyc +0 -0
- package/scripts/__pycache__/knowledge-index.cpython-313.pyc +0 -0
- package/scripts/__pycache__/synapse-bridge.cpython-312.pyc +0 -0
- package/scripts/__pycache__/synapse-bridge.cpython-313.pyc +0 -0
- package/scripts/knowledge-index.py +8 -2
- package/scripts/synapse-bridge.py +102 -26
|
@@ -1,29 +1,136 @@
|
|
|
1
1
|
"""Embedding wrapper — local embeddings via fastembed.
|
|
2
2
|
|
|
3
3
|
Graceful degradation: if fastembed is not installed, returns None
|
|
4
|
-
and the vector store falls back to keyword matching
|
|
4
|
+
and the vector store falls back to keyword matching (which the store
|
|
5
|
+
now labels as ``retrieval: "keyword-degraded"`` — see vector_store.py).
|
|
6
|
+
|
|
7
|
+
Model selection (PR-3 v4.1 — RAG honesty):
|
|
8
|
+
1. ``ARKA_EMBED_MODEL`` env var
|
|
9
|
+
2. ``knowledge.embedModel`` in ``~/.arkaos/config.json``
|
|
10
|
+
3. default ``BAAI/bge-small-en-v1.5`` (backward compatible, 384 dims)
|
|
11
|
+
|
|
12
|
+
Recommendation for PT-heavy corpora:
|
|
13
|
+
``sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2``
|
|
14
|
+
(384 dims — same dimension as the default, so switching does NOT
|
|
15
|
+
require re-creating the vec0 table). ``intfloat/multilingual-e5-small``
|
|
16
|
+
is NOT in fastembed's supported model list (fastembed only ships the
|
|
17
|
+
``intfloat/multilingual-e5-large`` variant, 1024 dims, which WOULD
|
|
18
|
+
force a re-index); the MiniLM multilingual model is the supported
|
|
19
|
+
drop-in. Verify with ``TextEmbedding.list_supported_models()``.
|
|
20
|
+
|
|
21
|
+
Dimensions are derived from the model (``embedding_dims()``), never
|
|
22
|
+
hardcoded at call sites. The vector store guards against a stored vec0
|
|
23
|
+
table whose dimension differs from the configured model — it logs and
|
|
24
|
+
keeps the stored dimension instead of corrupting the table.
|
|
5
25
|
"""
|
|
6
26
|
|
|
27
|
+
import json
|
|
28
|
+
import logging
|
|
29
|
+
import os
|
|
30
|
+
from pathlib import Path
|
|
7
31
|
from typing import Optional
|
|
8
32
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
DEFAULT_MODEL = "BAAI/bge-small-en-v1.5" # 384 dims, fast, good quality
|
|
36
|
+
RECOMMENDED_MULTILINGUAL_MODEL = (
|
|
37
|
+
"sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# Known dims for models we document — used when fastembed is missing so
|
|
41
|
+
# dimension resolution stays deterministic in degraded environments.
|
|
42
|
+
_KNOWN_DIMS: dict[str, int] = {
|
|
43
|
+
"BAAI/bge-small-en-v1.5": 384,
|
|
44
|
+
"sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2": 384,
|
|
45
|
+
"sentence-transformers/paraphrase-multilingual-mpnet-base-v2": 768,
|
|
46
|
+
"intfloat/multilingual-e5-large": 1024,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# Legacy constant (dims of the default model). Prefer embedding_dims().
|
|
12
50
|
EMBEDDING_DIMS = 384
|
|
13
51
|
|
|
52
|
+
# Lazy singletons — fastembed is optional.
|
|
53
|
+
_model = None
|
|
54
|
+
_loaded_model_name: Optional[str] = None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def get_model_name() -> str:
|
|
58
|
+
"""Resolve the configured embedding model name (env > config > default)."""
|
|
59
|
+
env_name = os.environ.get("ARKA_EMBED_MODEL", "").strip()
|
|
60
|
+
if env_name:
|
|
61
|
+
return env_name
|
|
62
|
+
config_path = Path.home() / ".arkaos" / "config.json"
|
|
63
|
+
try:
|
|
64
|
+
data = json.loads(config_path.read_text(encoding="utf-8"))
|
|
65
|
+
name = (data.get("knowledge") or {}).get("embedModel", "")
|
|
66
|
+
if isinstance(name, str) and name.strip():
|
|
67
|
+
return name.strip()
|
|
68
|
+
except (OSError, json.JSONDecodeError, AttributeError):
|
|
69
|
+
pass
|
|
70
|
+
return DEFAULT_MODEL
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def embedding_dims(model_name: Optional[str] = None) -> int:
|
|
74
|
+
"""Dimension of the active embedding model — derived, not hardcoded.
|
|
75
|
+
|
|
76
|
+
Prefers the actually-loaded model (which may have fallen back to the
|
|
77
|
+
default when a configured name failed to load), then fastembed's own
|
|
78
|
+
supported-models metadata, then the documented known-dims map.
|
|
79
|
+
"""
|
|
80
|
+
name = model_name or _loaded_model_name or get_model_name()
|
|
81
|
+
dim = _dims_from_fastembed(name)
|
|
82
|
+
if dim is not None:
|
|
83
|
+
return dim
|
|
84
|
+
return _KNOWN_DIMS.get(name, EMBEDDING_DIMS)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _dims_from_fastembed(name: str) -> Optional[int]:
|
|
88
|
+
try:
|
|
89
|
+
from fastembed import TextEmbedding
|
|
90
|
+
for entry in TextEmbedding.list_supported_models():
|
|
91
|
+
info = entry if isinstance(entry, dict) else vars(entry)
|
|
92
|
+
if info.get("model") == name and info.get("dim"):
|
|
93
|
+
return int(info["dim"])
|
|
94
|
+
except Exception: # noqa: BLE001 — degraded envs must never raise here
|
|
95
|
+
pass
|
|
96
|
+
return None
|
|
97
|
+
|
|
14
98
|
|
|
15
99
|
def get_model():
|
|
16
|
-
"""Get or create the embedding model (lazy singleton)."""
|
|
17
|
-
global _model
|
|
18
|
-
|
|
100
|
+
"""Get or create the embedding model (lazy singleton, config-aware)."""
|
|
101
|
+
global _model, _loaded_model_name
|
|
102
|
+
name = get_model_name()
|
|
103
|
+
if _model is not None and _loaded_model_name == name:
|
|
104
|
+
return _model
|
|
105
|
+
try:
|
|
106
|
+
from fastembed import TextEmbedding
|
|
107
|
+
except ImportError:
|
|
108
|
+
return None
|
|
109
|
+
try:
|
|
110
|
+
_model = TextEmbedding(name)
|
|
111
|
+
_loaded_model_name = name
|
|
112
|
+
except Exception as exc: # noqa: BLE001 — unsupported model name, etc.
|
|
113
|
+
logger.warning(
|
|
114
|
+
"embed model %r failed to load (%s) — falling back to %s",
|
|
115
|
+
name, exc, DEFAULT_MODEL,
|
|
116
|
+
)
|
|
117
|
+
if name == DEFAULT_MODEL:
|
|
118
|
+
return None
|
|
19
119
|
try:
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
except
|
|
120
|
+
_model = TextEmbedding(DEFAULT_MODEL)
|
|
121
|
+
_loaded_model_name = DEFAULT_MODEL
|
|
122
|
+
except Exception: # noqa: BLE001
|
|
23
123
|
return None
|
|
24
124
|
return _model
|
|
25
125
|
|
|
26
126
|
|
|
127
|
+
def reset_model_cache() -> None:
|
|
128
|
+
"""Drop the cached model so the next call re-resolves configuration."""
|
|
129
|
+
global _model, _loaded_model_name
|
|
130
|
+
_model = None
|
|
131
|
+
_loaded_model_name = None
|
|
132
|
+
|
|
133
|
+
|
|
27
134
|
def embed(text: str) -> Optional[list[float]]:
|
|
28
135
|
"""Embed a single text. Returns None if fastembed unavailable."""
|
|
29
136
|
model = get_model()
|
|
@@ -46,7 +153,7 @@ def embed_batch(texts: list[str]) -> Optional[list[list[float]]]:
|
|
|
46
153
|
def is_available() -> bool:
|
|
47
154
|
"""Check if embedding model is available."""
|
|
48
155
|
try:
|
|
49
|
-
from fastembed import TextEmbedding
|
|
156
|
+
from fastembed import TextEmbedding # noqa: F401
|
|
50
157
|
return True
|
|
51
158
|
except ImportError:
|
|
52
159
|
return False
|
|
@@ -1,19 +1,29 @@
|
|
|
1
1
|
"""Vector store — SQLite-vec backed semantic search.
|
|
2
2
|
|
|
3
3
|
Stores document chunks with embeddings for fast similarity search.
|
|
4
|
-
Graceful degradation: works without sqlite-vec (
|
|
4
|
+
Graceful degradation: works without sqlite-vec (keyword fallback) — but
|
|
5
|
+
HONESTLY (PR-3 v4.1): degraded results carry ``retrieval:
|
|
6
|
+
"keyword-degraded"`` and ``score: None`` instead of a fake similarity
|
|
7
|
+
score, and the first degraded search per process emits a visible
|
|
8
|
+
stderr warning explaining why semantic search is unavailable.
|
|
5
9
|
"""
|
|
6
10
|
|
|
7
11
|
import json
|
|
12
|
+
import logging
|
|
13
|
+
import re
|
|
8
14
|
import sqlite3
|
|
15
|
+
import sys
|
|
9
16
|
import time
|
|
10
17
|
from pathlib import Path
|
|
11
18
|
from typing import Any, Optional
|
|
12
19
|
|
|
13
|
-
from core.knowledge.embedder import embed, embed_batch,
|
|
20
|
+
from core.knowledge.embedder import embed, embed_batch, embedding_dims
|
|
14
21
|
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
15
23
|
|
|
16
24
|
_VEC_UNAVAILABLE_REASON: str = "" # set by _load_vec on failure
|
|
25
|
+
_DEGRADED_WARNED: bool = False # one-time per-process degradation warning
|
|
26
|
+
_VEC_DIMS_RE = re.compile(r"float\[(\d+)\]")
|
|
17
27
|
|
|
18
28
|
|
|
19
29
|
def _load_vec(db: sqlite3.Connection) -> bool:
|
|
@@ -61,6 +71,32 @@ def vec_unavailable_reason() -> str:
|
|
|
61
71
|
return _VEC_UNAVAILABLE_REASON
|
|
62
72
|
|
|
63
73
|
|
|
74
|
+
def _parse_vec_dims(create_sql: str | None) -> Optional[int]:
|
|
75
|
+
"""Extract the float[N] dimension from a vec0 CREATE statement."""
|
|
76
|
+
if not create_sql:
|
|
77
|
+
return None
|
|
78
|
+
match = _VEC_DIMS_RE.search(create_sql)
|
|
79
|
+
return int(match.group(1)) if match else None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _warn_degraded_once(reason: str) -> None:
|
|
83
|
+
"""Emit the one-time visible degradation warning (stderr + log)."""
|
|
84
|
+
global _DEGRADED_WARNED
|
|
85
|
+
if _DEGRADED_WARNED:
|
|
86
|
+
return
|
|
87
|
+
_DEGRADED_WARNED = True
|
|
88
|
+
msg = (
|
|
89
|
+
f"[arka:kb-degraded] semantic search unavailable ({reason}) — "
|
|
90
|
+
"results are keyword matches, NOT similarity-ranked. "
|
|
91
|
+
"Fix: pip install fastembed sqlite-vec (or: npx arkaos doctor)."
|
|
92
|
+
)
|
|
93
|
+
try:
|
|
94
|
+
sys.stderr.write(msg + "\n")
|
|
95
|
+
except OSError:
|
|
96
|
+
pass
|
|
97
|
+
logger.warning(msg)
|
|
98
|
+
|
|
99
|
+
|
|
64
100
|
class VectorStore:
|
|
65
101
|
"""SQLite-vec backed vector store for knowledge retrieval.
|
|
66
102
|
|
|
@@ -108,15 +144,40 @@ class VectorStore:
|
|
|
108
144
|
CREATE INDEX IF NOT EXISTS idx_chunks_hash ON chunks(file_hash);
|
|
109
145
|
""")
|
|
110
146
|
self._migrate_vss_to_vec()
|
|
147
|
+
self._vec_dims = embedding_dims() # derived from the active model
|
|
111
148
|
if self._vec_available:
|
|
112
149
|
try:
|
|
113
|
-
self.
|
|
114
|
-
|
|
115
|
-
|
|
150
|
+
stored = self._stored_vec_dims()
|
|
151
|
+
if stored is not None and stored != self._vec_dims:
|
|
152
|
+
# An existing index was built with a different model
|
|
153
|
+
# dimension. NEVER corrupt it — keep the stored dim
|
|
154
|
+
# and let dimension guards route mismatched vectors
|
|
155
|
+
# to the degraded path until the user re-indexes.
|
|
156
|
+
logger.warning(
|
|
157
|
+
"vec_chunks dimension %d differs from configured "
|
|
158
|
+
"embed model dimension %d — keeping the stored "
|
|
159
|
+
"index. Re-index (/arka index) to switch models.",
|
|
160
|
+
stored, self._vec_dims,
|
|
161
|
+
)
|
|
162
|
+
self._vec_dims = stored
|
|
163
|
+
else:
|
|
164
|
+
self._db.execute(
|
|
165
|
+
f"CREATE VIRTUAL TABLE IF NOT EXISTS vec_chunks USING vec0(embedding float[{self._vec_dims}])"
|
|
166
|
+
)
|
|
116
167
|
except Exception:
|
|
117
168
|
self._vec_available = False
|
|
118
169
|
self._db.commit()
|
|
119
170
|
|
|
171
|
+
def _stored_vec_dims(self) -> Optional[int]:
|
|
172
|
+
"""Dimension of an existing vec_chunks table, if any."""
|
|
173
|
+
try:
|
|
174
|
+
row = self._db.execute(
|
|
175
|
+
"SELECT sql FROM sqlite_master WHERE name = 'vec_chunks'"
|
|
176
|
+
).fetchone()
|
|
177
|
+
except sqlite3.Error:
|
|
178
|
+
return None
|
|
179
|
+
return _parse_vec_dims(row["sql"] if row else None)
|
|
180
|
+
|
|
120
181
|
def _migrate_vss_to_vec(self) -> None:
|
|
121
182
|
"""Drop legacy sqlite-vss tables if present.
|
|
122
183
|
|
|
@@ -176,7 +237,15 @@ class VectorStore:
|
|
|
176
237
|
(text, heading, source, file_hash, meta_json, emb_blob),
|
|
177
238
|
)
|
|
178
239
|
|
|
179
|
-
|
|
240
|
+
# Dimension guard: a vector produced by a differently-sized
|
|
241
|
+
# model must never be inserted into the vec0 table (it
|
|
242
|
+
# would poison distance queries). The raw embedding is
|
|
243
|
+
# still stored on the chunk row for a later re-index.
|
|
244
|
+
if (
|
|
245
|
+
self._vec_available
|
|
246
|
+
and emb_blob
|
|
247
|
+
and len(embeddings[i]) == self._vec_dims
|
|
248
|
+
):
|
|
180
249
|
self._db.execute(
|
|
181
250
|
"INSERT INTO vec_chunks (rowid, embedding) VALUES (?, ?)",
|
|
182
251
|
(cursor.lastrowid, emb_blob),
|
|
@@ -199,13 +268,24 @@ class VectorStore:
|
|
|
199
268
|
query_emb = embed(query)
|
|
200
269
|
|
|
201
270
|
if query_emb and self._vec_available:
|
|
271
|
+
if len(query_emb) != self._vec_dims:
|
|
272
|
+
return self._keyword_search(
|
|
273
|
+
query, top_k,
|
|
274
|
+
reason="embed model dimension differs from the stored "
|
|
275
|
+
"index — re-index to switch models",
|
|
276
|
+
)
|
|
202
277
|
try:
|
|
203
278
|
return self._vec_search(query_emb, top_k)
|
|
204
279
|
except Exception:
|
|
205
|
-
return self._keyword_search(query, top_k)
|
|
280
|
+
return self._keyword_search(query, top_k, reason="vector query failed")
|
|
206
281
|
|
|
207
|
-
# Fallback: keyword search
|
|
208
|
-
return self._keyword_search(query, top_k)
|
|
282
|
+
# Fallback: keyword search — labeled, never presented as semantic.
|
|
283
|
+
return self._keyword_search(query, top_k, reason=self._degradation_reason(query_emb))
|
|
284
|
+
|
|
285
|
+
def _degradation_reason(self, query_emb: Optional[list[float]]) -> str:
|
|
286
|
+
if query_emb is None:
|
|
287
|
+
return "fastembed missing or embedding failed"
|
|
288
|
+
return vec_unavailable_reason() or "sqlite-vec unavailable"
|
|
209
289
|
|
|
210
290
|
def _vec_search(self, query_emb: list[float], top_k: int) -> list[dict]:
|
|
211
291
|
"""Vector similarity search via sqlite-vec."""
|
|
@@ -224,19 +304,31 @@ class VectorStore:
|
|
|
224
304
|
"heading": r["heading"],
|
|
225
305
|
"source": r["source"],
|
|
226
306
|
"score": 1.0 / (1.0 + r["distance"]), # Convert distance to similarity
|
|
307
|
+
"retrieval": "semantic",
|
|
227
308
|
"metadata": json.loads(r["metadata"]),
|
|
228
309
|
}
|
|
229
310
|
for r in rows
|
|
230
311
|
]
|
|
231
312
|
|
|
232
|
-
def _keyword_search(self, query: str, top_k: int) -> list[dict]:
|
|
233
|
-
"""Fallback keyword search when
|
|
234
|
-
|
|
313
|
+
def _keyword_search(self, query: str, top_k: int, reason: str = "") -> list[dict]:
|
|
314
|
+
"""Fallback keyword search when semantic retrieval is unavailable.
|
|
315
|
+
|
|
316
|
+
HONESTY CONTRACT (PR-3 v4.1): these results are substring matches,
|
|
317
|
+
not similarity-ranked. ``score`` is None (there is no similarity)
|
|
318
|
+
and ``retrieval`` is ``"keyword-degraded"`` so every consumer can
|
|
319
|
+
label them. The previous behaviour — a hardcoded fake ``0.5``
|
|
320
|
+
score — silently impersonated semantic search.
|
|
321
|
+
"""
|
|
322
|
+
words = query.lower().split()[:5] # Max 5 keywords
|
|
235
323
|
if not words:
|
|
236
324
|
return []
|
|
237
325
|
|
|
326
|
+
_warn_degraded_once(reason or "semantic retrieval unavailable")
|
|
327
|
+
|
|
328
|
+
# Placeholders and params MUST come from the same capped list —
|
|
329
|
+
# previously a 6+ word query produced more `?` than bindings.
|
|
238
330
|
conditions = " OR ".join(["lower(text) LIKE ?" for _ in words])
|
|
239
|
-
params = [f"%{w}%" for w in words
|
|
331
|
+
params = [f"%{w}%" for w in words]
|
|
240
332
|
|
|
241
333
|
rows = self._db.execute(
|
|
242
334
|
f"SELECT text, heading, source, metadata FROM chunks WHERE {conditions} LIMIT ?",
|
|
@@ -248,7 +340,8 @@ class VectorStore:
|
|
|
248
340
|
"text": r["text"],
|
|
249
341
|
"heading": r["heading"],
|
|
250
342
|
"source": r["source"],
|
|
251
|
-
"score":
|
|
343
|
+
"score": None, # keyword match — NO similarity score exists
|
|
344
|
+
"retrieval": "keyword-degraded",
|
|
252
345
|
"metadata": json.loads(r["metadata"]),
|
|
253
346
|
}
|
|
254
347
|
for r in rows
|
|
@@ -274,12 +367,16 @@ class VectorStore:
|
|
|
274
367
|
|
|
275
368
|
def get_stats(self) -> dict:
|
|
276
369
|
"""Get store statistics."""
|
|
370
|
+
from core.knowledge.embedder import is_available as _embedder_available
|
|
371
|
+
|
|
277
372
|
total = self._db.execute("SELECT COUNT(*) as cnt FROM chunks").fetchone()["cnt"]
|
|
278
373
|
sources = self._db.execute("SELECT COUNT(DISTINCT source) as cnt FROM chunks").fetchone()["cnt"]
|
|
374
|
+
semantic = self._vec_available and _embedder_available()
|
|
279
375
|
return {
|
|
280
376
|
"total_chunks": total,
|
|
281
377
|
"total_files": sources,
|
|
282
378
|
"vec_available": self._vec_available,
|
|
379
|
+
"retrieval_mode": "semantic" if semantic else "keyword-degraded",
|
|
283
380
|
"db_path": self._db_path,
|
|
284
381
|
}
|
|
285
382
|
|
|
Binary file
|
|
Binary file
|
package/core/obsidian/relator.py
CHANGED
|
@@ -36,6 +36,13 @@ _STOPWORDS = frozenset({
|
|
|
36
36
|
})
|
|
37
37
|
|
|
38
38
|
|
|
39
|
+
# Jaccard between full documents rarely exceeds ~0.3 even for clearly
|
|
40
|
+
# related notes, while cosine similarity of embeddings sits comfortably
|
|
41
|
+
# above 0.5. A single threshold calibrated for cosine silently disabled
|
|
42
|
+
# the relator on every machine without fastembed (v4.1.1 fix).
|
|
43
|
+
_KEYWORD_MIN_SIMILARITY = 0.15
|
|
44
|
+
|
|
45
|
+
|
|
39
46
|
def find_related(
|
|
40
47
|
content: str,
|
|
41
48
|
vault: Path,
|
|
@@ -48,8 +55,9 @@ def find_related(
|
|
|
48
55
|
candidates = _collect_notes(vault, exclude)
|
|
49
56
|
if not candidates:
|
|
50
57
|
return []
|
|
51
|
-
scored = _score_candidates(content, candidates)
|
|
52
|
-
|
|
58
|
+
scored, method = _score_candidates(content, candidates)
|
|
59
|
+
threshold = min_similarity if method == "semantic" else _KEYWORD_MIN_SIMILARITY
|
|
60
|
+
filtered = [r for r in scored if r.score >= threshold]
|
|
53
61
|
filtered.sort(key=lambda r: r.score, reverse=True)
|
|
54
62
|
return filtered[:top_n]
|
|
55
63
|
|
|
@@ -70,22 +78,24 @@ def _collect_notes(vault: Path, exclude: Optional[Path]) -> list[tuple[Path, str
|
|
|
70
78
|
|
|
71
79
|
def _score_candidates(
|
|
72
80
|
content: str, candidates: list[tuple[Path, str]]
|
|
73
|
-
) -> list[RelatedNote]:
|
|
81
|
+
) -> tuple[list[RelatedNote], str]:
|
|
82
|
+
"""Score candidates, returning (results, method) so the caller can
|
|
83
|
+
apply a threshold appropriate to the scoring space."""
|
|
74
84
|
try:
|
|
75
85
|
return _semantic_score(content, candidates)
|
|
76
86
|
except Exception:
|
|
77
|
-
return _keyword_score(content, candidates)
|
|
87
|
+
return _keyword_score(content, candidates), "keyword"
|
|
78
88
|
|
|
79
89
|
|
|
80
90
|
def _semantic_score(
|
|
81
91
|
content: str, candidates: list[tuple[Path, str]]
|
|
82
|
-
) -> list[RelatedNote]:
|
|
92
|
+
) -> tuple[list[RelatedNote], str]:
|
|
83
93
|
from core.knowledge.embedder import embed, is_available
|
|
84
94
|
if not is_available():
|
|
85
|
-
return _keyword_score(content, candidates)
|
|
95
|
+
return _keyword_score(content, candidates), "keyword"
|
|
86
96
|
query_emb = embed(content)
|
|
87
97
|
if query_emb is None:
|
|
88
|
-
return _keyword_score(content, candidates)
|
|
98
|
+
return _keyword_score(content, candidates), "keyword"
|
|
89
99
|
results = []
|
|
90
100
|
for path, text in candidates:
|
|
91
101
|
note_emb = embed(text[:2000])
|
|
@@ -96,7 +106,7 @@ def _semantic_score(
|
|
|
96
106
|
path=path, title=_title_from_path(path),
|
|
97
107
|
score=score, excerpt=text[:160],
|
|
98
108
|
))
|
|
99
|
-
return results
|
|
109
|
+
return results, "semantic"
|
|
100
110
|
|
|
101
111
|
|
|
102
112
|
def _keyword_score(
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/core/runtime/base.py
CHANGED
|
@@ -145,6 +145,35 @@ class RuntimeAdapter(ABC):
|
|
|
145
145
|
}
|
|
146
146
|
return feature_map.get(feature, False)
|
|
147
147
|
|
|
148
|
+
def capabilities(self) -> dict[str, bool]:
|
|
149
|
+
"""Honest, static support matrix for this adapter (PR-6 v4.1.0).
|
|
150
|
+
|
|
151
|
+
Single source of truth for the multi-runtime story — consumed by
|
|
152
|
+
``python -m core.runtime.capabilities_cli`` and wiki/08.
|
|
153
|
+
|
|
154
|
+
Keys (all bool):
|
|
155
|
+
- ``agent_dispatch``: the runtime can dispatch ArkaOS agents /
|
|
156
|
+
subagents natively (Claude Code Agent tool). False when
|
|
157
|
+
``dispatch_agent`` only returns a descriptor for a runtime that
|
|
158
|
+
cannot actually spawn ArkaOS agents.
|
|
159
|
+
- ``headless``: the adapter implements a real one-shot
|
|
160
|
+
``headless_complete`` against the runtime's CLI (static — use
|
|
161
|
+
``headless_supported()`` for binary-on-PATH detection).
|
|
162
|
+
- ``file_ops``: the runtime performs file operations natively
|
|
163
|
+
when driven interactively (single-agent use counts).
|
|
164
|
+
- ``hooks``: the runtime exposes lifecycle hooks that ArkaOS
|
|
165
|
+
enforcement (PreToolUse/Stop/...) can attach to.
|
|
166
|
+
|
|
167
|
+
Default: conservative all-False except hooks, which mirrors the
|
|
168
|
+
runtime config — unknown adapters must not overclaim.
|
|
169
|
+
"""
|
|
170
|
+
return {
|
|
171
|
+
"agent_dispatch": False,
|
|
172
|
+
"headless": False,
|
|
173
|
+
"file_ops": False,
|
|
174
|
+
"hooks": self.get_config().supports_hooks,
|
|
175
|
+
}
|
|
176
|
+
|
|
148
177
|
def headless_supported(self) -> bool:
|
|
149
178
|
"""Whether this adapter can perform a headless CLI completion.
|
|
150
179
|
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Runtime capabilities matrix CLI (PR-6 v4.1.0 — multi-runtime honesty).
|
|
2
|
+
|
|
3
|
+
Single source of truth for what each runtime adapter actually supports.
|
|
4
|
+
Prints the static ``capabilities()`` matrix of all registered adapters,
|
|
5
|
+
plus live binary detection for the headless CLIs.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
python -m core.runtime.capabilities_cli # table
|
|
9
|
+
python -m core.runtime.capabilities_cli --json # machine-readable
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import json
|
|
16
|
+
import sys
|
|
17
|
+
|
|
18
|
+
from core.runtime.registry import RUNTIME_ADAPTERS
|
|
19
|
+
|
|
20
|
+
_CAP_KEYS = ("agent_dispatch", "headless", "file_ops", "hooks")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def build_matrix() -> dict[str, dict]:
|
|
24
|
+
"""Collect capabilities + live headless availability per runtime."""
|
|
25
|
+
matrix: dict[str, dict] = {}
|
|
26
|
+
for runtime_id, adapter_cls in RUNTIME_ADAPTERS.items():
|
|
27
|
+
adapter = adapter_cls()
|
|
28
|
+
caps = adapter.capabilities()
|
|
29
|
+
row = {key: bool(caps.get(key, False)) for key in _CAP_KEYS}
|
|
30
|
+
try:
|
|
31
|
+
row["headless_available"] = bool(adapter.headless_supported())
|
|
32
|
+
except Exception: # noqa: BLE001 — detection must never crash
|
|
33
|
+
row["headless_available"] = False
|
|
34
|
+
row["name"] = adapter.get_config().name
|
|
35
|
+
matrix[runtime_id] = row
|
|
36
|
+
return matrix
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _render_table(matrix: dict[str, dict]) -> str:
|
|
40
|
+
header = (
|
|
41
|
+
f"{'runtime':<14}{'agent_dispatch':<16}{'headless':<10}"
|
|
42
|
+
f"{'file_ops':<10}{'hooks':<7}{'binary on PATH'}"
|
|
43
|
+
)
|
|
44
|
+
lines = [header, "-" * len(header)]
|
|
45
|
+
for runtime_id, row in matrix.items():
|
|
46
|
+
def mark(key: str) -> str:
|
|
47
|
+
return "yes" if row.get(key) else "no"
|
|
48
|
+
lines.append(
|
|
49
|
+
f"{runtime_id:<14}{mark('agent_dispatch'):<16}"
|
|
50
|
+
f"{mark('headless'):<10}{mark('file_ops'):<10}"
|
|
51
|
+
f"{mark('hooks'):<7}{mark('headless_available')}"
|
|
52
|
+
)
|
|
53
|
+
return "\n".join(lines)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def main(argv: list[str] | None = None) -> int:
|
|
57
|
+
parser = argparse.ArgumentParser(
|
|
58
|
+
prog="python -m core.runtime.capabilities_cli",
|
|
59
|
+
description="Print the multi-runtime support matrix.",
|
|
60
|
+
)
|
|
61
|
+
parser.add_argument(
|
|
62
|
+
"--json", action="store_true", help="Emit JSON instead of a table"
|
|
63
|
+
)
|
|
64
|
+
args = parser.parse_args(argv)
|
|
65
|
+
matrix = build_matrix()
|
|
66
|
+
if args.json:
|
|
67
|
+
print(json.dumps(matrix, indent=2))
|
|
68
|
+
else:
|
|
69
|
+
print(_render_table(matrix))
|
|
70
|
+
return 0
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
if __name__ == "__main__":
|
|
74
|
+
sys.exit(main())
|