loki-mode 7.78.0 → 7.80.0
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/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/lib/checkpoint_sync.py +189 -0
- package/autonomy/lib/config-map.sh +955 -0
- package/autonomy/loki +273 -8
- package/autonomy/run.sh +230 -195
- package/autonomy/sandbox.sh +98 -0
- package/autonomy/trigger-server.py +419 -43
- package/dashboard/__init__.py +1 -1
- package/dashboard/registry.py +212 -0
- package/dashboard/server.py +299 -7
- package/dashboard/static/index.html +383 -150
- package/docs/CONFIG-FILE-PLAN.md +459 -0
- package/docs/ENTERPRISE-IDENTITY-ROADMAP.md +206 -0
- package/docs/INSTALLATION.md +2 -2
- package/loki-ts/dist/loki.js +2 -2
- package/lokistore/__init__.py +65 -0
- package/lokistore/base.py +172 -0
- package/lokistore/cloud.py +305 -0
- package/lokistore/factory.py +187 -0
- package/lokistore/local.py +219 -0
- package/mcp/__init__.py +1 -1
- package/memory/retrieval.py +147 -0
- package/memory/tree_index.py +499 -0
- package/memory/tree_search.py +305 -0
- package/package.json +2 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Loki Mode Memory System - LLM-Reasoning Tree Search
|
|
3
|
+
|
|
4
|
+
Navigates the structure-aware TOC tree (memory/tree_index.py) to locate the
|
|
5
|
+
nodes most relevant to a query. This is the PageIndex IDEA: reason DOWN the
|
|
6
|
+
tree (pick relevant children at each level) instead of comparing embedding
|
|
7
|
+
vectors. The reasoning step is delegated to an injected LLM callable so this
|
|
8
|
+
module imports no provider SDK and stays dependency-free.
|
|
9
|
+
|
|
10
|
+
Graceful degradation is a first-class requirement:
|
|
11
|
+
- If an LLM callable is provided, it is asked at each level which children
|
|
12
|
+
to descend into.
|
|
13
|
+
- If NO LLM callable is available (the common local case), the search falls
|
|
14
|
+
back to a deterministic keyword scorer over node titles/summaries/paths.
|
|
15
|
+
The result is a best-effort structure-aware ranking with zero LLM cost.
|
|
16
|
+
|
|
17
|
+
Either way the function returns a ranked list of leaf-ish nodes (as plain
|
|
18
|
+
dicts), so callers can map them back to files/symbols/spec ranges.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import logging
|
|
25
|
+
import re
|
|
26
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
27
|
+
|
|
28
|
+
from .tree_index import TreeNode
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
# Type of the optional LLM callable. Given a prompt string it returns the raw
|
|
33
|
+
# model response string. The callable owns provider selection, auth, timeouts.
|
|
34
|
+
LLMCallable = Callable[[str], str]
|
|
35
|
+
|
|
36
|
+
_WORD_RE = re.compile(r"[a-z0-9]+")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _tokenize(text: str) -> List[str]:
|
|
40
|
+
"""Tokenize into lowercase word tokens.
|
|
41
|
+
|
|
42
|
+
Splits on every non-alphanumeric boundary INCLUDING underscores, so a
|
|
43
|
+
symbol like "retrieve_task_aware" yields ["retrieve", "task", "aware"]
|
|
44
|
+
and a natural-language query ("retrieve task aware") matches it. This
|
|
45
|
+
sub-word matching is what lets structure-aware search work without
|
|
46
|
+
embeddings.
|
|
47
|
+
"""
|
|
48
|
+
return _WORD_RE.findall((text or "").lower())
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _node_text(node: TreeNode) -> str:
|
|
52
|
+
"""Concatenate the searchable text of a node (title, summary, path)."""
|
|
53
|
+
return " ".join([node.title or "", node.summary or "", node.path or ""])
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# -----------------------------------------------------------------------------
|
|
57
|
+
# Keyword (no-LLM) fallback scorer
|
|
58
|
+
# -----------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _keyword_score(node: TreeNode, query_tokens: List[str]) -> float:
|
|
62
|
+
"""Deterministic relevance score for a node against query tokens.
|
|
63
|
+
|
|
64
|
+
Title matches weigh most, then path, then summary. Used both as the
|
|
65
|
+
no-LLM fallback and as a tie-breaker when ranking the descended nodes.
|
|
66
|
+
"""
|
|
67
|
+
if not query_tokens:
|
|
68
|
+
return 0.0
|
|
69
|
+
title_tokens = set(_tokenize(node.title))
|
|
70
|
+
path_tokens = set(_tokenize(node.path))
|
|
71
|
+
summary_tokens = set(_tokenize(node.summary))
|
|
72
|
+
score = 0.0
|
|
73
|
+
for tok in query_tokens:
|
|
74
|
+
if tok in title_tokens:
|
|
75
|
+
score += 2.0
|
|
76
|
+
if tok in path_tokens:
|
|
77
|
+
score += 1.0
|
|
78
|
+
if tok in summary_tokens:
|
|
79
|
+
score += 0.5
|
|
80
|
+
return score
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _keyword_search(
|
|
84
|
+
root: TreeNode, query: str, top_k: int
|
|
85
|
+
) -> List[Dict[str, Any]]:
|
|
86
|
+
"""Rank every leaf-ish node by keyword score (no LLM)."""
|
|
87
|
+
query_tokens = _tokenize(query)
|
|
88
|
+
scored: List[tuple] = []
|
|
89
|
+
for node in root.walk():
|
|
90
|
+
# Leaf-ish: a node with no children (symbol/section/file leaf) is a
|
|
91
|
+
# retrieval target. Files with children are skipped in favor of their
|
|
92
|
+
# symbols; files without indexed symbols remain targets themselves.
|
|
93
|
+
if node.children:
|
|
94
|
+
continue
|
|
95
|
+
if node.kind == "root":
|
|
96
|
+
continue
|
|
97
|
+
score = _keyword_score(node, query_tokens)
|
|
98
|
+
if score > 0:
|
|
99
|
+
scored.append((score, node))
|
|
100
|
+
scored.sort(key=lambda x: x[0], reverse=True)
|
|
101
|
+
return [
|
|
102
|
+
_node_result(node, score, via="keyword")
|
|
103
|
+
for score, node in scored[:top_k]
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _node_result(node: TreeNode, score: float, via: str) -> Dict[str, Any]:
|
|
108
|
+
"""Map a TreeNode to a retrieval result dict.
|
|
109
|
+
|
|
110
|
+
The shape intentionally mirrors keyword retrieval results in retrieval.py
|
|
111
|
+
(_score / _source fields) so downstream merge/budget code can consume it.
|
|
112
|
+
"""
|
|
113
|
+
result: Dict[str, Any] = {
|
|
114
|
+
"title": node.title,
|
|
115
|
+
"summary": node.summary,
|
|
116
|
+
"path": node.path,
|
|
117
|
+
"kind": node.kind,
|
|
118
|
+
"_score": float(score),
|
|
119
|
+
"_source": "tree",
|
|
120
|
+
"_retrieval": via,
|
|
121
|
+
}
|
|
122
|
+
if node.range is not None:
|
|
123
|
+
result["range"] = list(node.range)
|
|
124
|
+
return result
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# -----------------------------------------------------------------------------
|
|
128
|
+
# LLM-reasoning descent
|
|
129
|
+
# -----------------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _build_descent_prompt(
|
|
133
|
+
query: str, node: TreeNode, children: List[TreeNode]
|
|
134
|
+
) -> str:
|
|
135
|
+
"""Build the prompt asking the model which children to descend into."""
|
|
136
|
+
lines = [
|
|
137
|
+
"You are navigating a table-of-contents tree to find the nodes most",
|
|
138
|
+
"relevant to a query. Choose which child nodes are worth exploring.",
|
|
139
|
+
"",
|
|
140
|
+
f"QUERY: {query}",
|
|
141
|
+
"",
|
|
142
|
+
f"CURRENT NODE: {node.title} ({node.kind})",
|
|
143
|
+
"CHILDREN (index: title -- summary):",
|
|
144
|
+
]
|
|
145
|
+
for i, child in enumerate(children):
|
|
146
|
+
summary = (child.summary or "").strip()
|
|
147
|
+
if len(summary) > 160:
|
|
148
|
+
summary = summary[:157] + "..."
|
|
149
|
+
lines.append(f" {i}: {child.title} -- {summary}")
|
|
150
|
+
lines += [
|
|
151
|
+
"",
|
|
152
|
+
"Respond with ONLY a JSON array of the indexes worth exploring,",
|
|
153
|
+
'most relevant first, e.g. [2, 0]. Return [] if none are relevant.',
|
|
154
|
+
]
|
|
155
|
+
return "\n".join(lines)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _parse_indexes(response: str, n_children: int) -> List[int]:
|
|
159
|
+
"""Parse a JSON array of child indexes from an LLM response.
|
|
160
|
+
|
|
161
|
+
Tolerant: extracts the first [...] block, ignores out-of-range / non-int
|
|
162
|
+
entries, dedupes while preserving order. Returns [] when nothing parses
|
|
163
|
+
(the caller then treats this branch as not-descended).
|
|
164
|
+
"""
|
|
165
|
+
if not response:
|
|
166
|
+
return []
|
|
167
|
+
match = re.search(r"\[.*?\]", response, re.DOTALL)
|
|
168
|
+
if not match:
|
|
169
|
+
return []
|
|
170
|
+
try:
|
|
171
|
+
parsed = json.loads(match.group(0))
|
|
172
|
+
except (ValueError, TypeError):
|
|
173
|
+
return []
|
|
174
|
+
if not isinstance(parsed, list):
|
|
175
|
+
return []
|
|
176
|
+
seen: set = set()
|
|
177
|
+
out: List[int] = []
|
|
178
|
+
for item in parsed:
|
|
179
|
+
if isinstance(item, bool):
|
|
180
|
+
continue
|
|
181
|
+
if isinstance(item, int) and 0 <= item < n_children and item not in seen:
|
|
182
|
+
seen.add(item)
|
|
183
|
+
out.append(item)
|
|
184
|
+
return out
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _llm_search(
|
|
188
|
+
root: TreeNode,
|
|
189
|
+
query: str,
|
|
190
|
+
top_k: int,
|
|
191
|
+
llm: LLMCallable,
|
|
192
|
+
max_nodes: int,
|
|
193
|
+
beam: int,
|
|
194
|
+
) -> List[Dict[str, Any]]:
|
|
195
|
+
"""Descend the tree using the LLM to pick relevant children per level.
|
|
196
|
+
|
|
197
|
+
A bounded breadth-limited descent: at each internal node the LLM ranks its
|
|
198
|
+
children; the top `beam` are queued. Leaf nodes reached this way are the
|
|
199
|
+
results, ordered by descent depth-rank. Bounded by max_nodes LLM calls so
|
|
200
|
+
a pathological tree cannot run away. Any LLM error at a node degrades that
|
|
201
|
+
branch to the keyword scorer rather than aborting the whole search.
|
|
202
|
+
"""
|
|
203
|
+
query_tokens = _tokenize(query)
|
|
204
|
+
results: List[Dict[str, Any]] = []
|
|
205
|
+
# Queue of (node, depth_rank) to expand. depth_rank seeds result ordering.
|
|
206
|
+
queue: List[tuple] = [(root, 0.0)]
|
|
207
|
+
llm_calls = 0
|
|
208
|
+
visited = 0
|
|
209
|
+
|
|
210
|
+
while queue and len(results) < top_k * 3:
|
|
211
|
+
node, rank = queue.pop(0)
|
|
212
|
+
visited += 1
|
|
213
|
+
if not node.children:
|
|
214
|
+
if node.kind != "root":
|
|
215
|
+
# Score the leaf with keyword relevance as a stable secondary
|
|
216
|
+
# signal; descent rank is the primary order.
|
|
217
|
+
kw = _keyword_score(node, query_tokens)
|
|
218
|
+
results.append(
|
|
219
|
+
_node_result(node, score=rank + kw, via="llm")
|
|
220
|
+
)
|
|
221
|
+
continue
|
|
222
|
+
|
|
223
|
+
children = node.children
|
|
224
|
+
chosen: List[int]
|
|
225
|
+
if llm_calls < max_nodes:
|
|
226
|
+
llm_calls += 1
|
|
227
|
+
try:
|
|
228
|
+
response = llm(_build_descent_prompt(query, node, children))
|
|
229
|
+
chosen = _parse_indexes(response, len(children))
|
|
230
|
+
except Exception as exc: # noqa: BLE001 - degrade, never abort
|
|
231
|
+
logger.warning(
|
|
232
|
+
"tree-search LLM call failed at node %s: %s; "
|
|
233
|
+
"falling back to keyword scoring for this branch",
|
|
234
|
+
node.title,
|
|
235
|
+
exc,
|
|
236
|
+
)
|
|
237
|
+
chosen = []
|
|
238
|
+
else:
|
|
239
|
+
chosen = []
|
|
240
|
+
|
|
241
|
+
if not chosen:
|
|
242
|
+
# No LLM guidance (budget exhausted or empty/failed response):
|
|
243
|
+
# rank children by keyword score so the branch still progresses.
|
|
244
|
+
ranked = sorted(
|
|
245
|
+
range(len(children)),
|
|
246
|
+
key=lambda i: _keyword_score(children[i], query_tokens),
|
|
247
|
+
reverse=True,
|
|
248
|
+
)
|
|
249
|
+
positive = [
|
|
250
|
+
i for i in ranked if _keyword_score(children[i], query_tokens) > 0
|
|
251
|
+
]
|
|
252
|
+
if positive:
|
|
253
|
+
chosen = positive[:beam]
|
|
254
|
+
else:
|
|
255
|
+
# No child matches by keyword. Intermediate structural nodes
|
|
256
|
+
# (directories) rarely share tokens with a query, so dead-ending
|
|
257
|
+
# here would lose every descendant. Descend the top-`beam`
|
|
258
|
+
# children anyway so leaf symbols below still get scored. Leaf
|
|
259
|
+
# nodes (no grandchildren to redeem a 0 score) are NOT forced.
|
|
260
|
+
has_grandchildren = any(c.children for c in children)
|
|
261
|
+
chosen = ranked[:beam] if has_grandchildren else []
|
|
262
|
+
|
|
263
|
+
# Enqueue chosen children, highest-priority first. The descent rank
|
|
264
|
+
# decays with position so earlier picks rank higher in the output.
|
|
265
|
+
for position, idx in enumerate(chosen[:beam]):
|
|
266
|
+
child_rank = rank + (beam - position)
|
|
267
|
+
queue.append((children[idx], child_rank))
|
|
268
|
+
|
|
269
|
+
results.sort(key=lambda r: r.get("_score", 0.0), reverse=True)
|
|
270
|
+
return results[:top_k]
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
# -----------------------------------------------------------------------------
|
|
274
|
+
# Public entry point
|
|
275
|
+
# -----------------------------------------------------------------------------
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def tree_search(
|
|
279
|
+
root: TreeNode,
|
|
280
|
+
query: str,
|
|
281
|
+
top_k: int = 5,
|
|
282
|
+
llm: Optional[LLMCallable] = None,
|
|
283
|
+
max_llm_nodes: int = 32,
|
|
284
|
+
beam: int = 3,
|
|
285
|
+
) -> List[Dict[str, Any]]:
|
|
286
|
+
"""Locate the tree nodes most relevant to a query.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
root: the TOC tree root (from memory/tree_index.py).
|
|
290
|
+
query: the natural-language query.
|
|
291
|
+
top_k: maximum number of result nodes to return.
|
|
292
|
+
llm: optional LLM callable (prompt -> response). When None, a
|
|
293
|
+
deterministic keyword scorer is used (graceful degradation).
|
|
294
|
+
max_llm_nodes: cap on LLM calls during descent (cost guard).
|
|
295
|
+
beam: how many children to descend into per level.
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
A ranked list of result dicts (see _node_result). Each carries a
|
|
299
|
+
"_source": "tree" field and "_retrieval" of "llm" or "keyword".
|
|
300
|
+
"""
|
|
301
|
+
if root is None:
|
|
302
|
+
return []
|
|
303
|
+
if llm is None:
|
|
304
|
+
return _keyword_search(root, query, top_k)
|
|
305
|
+
return _llm_search(root, query, top_k, llm, max_llm_nodes, beam)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loki-mode",
|
|
3
3
|
"mcpName": "io.github.asklokesh/loki-mode",
|
|
4
|
-
"version": "7.
|
|
4
|
+
"version": "7.80.0",
|
|
5
5
|
"description": "Loki Mode by Autonomi. Autonomous spec-to-product system: takes a PRD, GitHub issue, OpenAPI/JSON/YAML, or one-line brief to a deployed app via the RARV-C closure loop with 8 quality gates. Provider-agnostic (Claude Code, OpenAI Codex, Cline, Aider).",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"agent",
|
|
@@ -83,6 +83,7 @@
|
|
|
83
83
|
"api/",
|
|
84
84
|
"events/",
|
|
85
85
|
"memory/",
|
|
86
|
+
"lokistore/",
|
|
86
87
|
"learning/",
|
|
87
88
|
"magic/",
|
|
88
89
|
"templates/",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "loki-mode",
|
|
4
4
|
"displayName": "Loki Mode",
|
|
5
|
-
"version": "7.
|
|
5
|
+
"version": "7.80.0",
|
|
6
6
|
"description": "Autonomous spec-to-product build system with a built-in trust layer (RARV-C closure loop, 8 quality gates, completion council). Ships Loki's spec-hardening, drift-detection, and deterministic PR verification commands plus the Loki MCP server.",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "Autonomi",
|