@feelingmindful/thinking-graph 1.15.1 → 1.20.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/dist/config.d.ts +16 -0
- package/dist/config.js +39 -0
- package/dist/engine/dedup.d.ts +10 -0
- package/dist/engine/dedup.js +19 -0
- package/dist/engine/fusion.d.ts +75 -0
- package/dist/engine/fusion.js +144 -0
- package/dist/engine/graph.d.ts +35 -1
- package/dist/engine/graph.js +147 -1
- package/dist/engine/intent.d.ts +14 -0
- package/dist/engine/intent.js +19 -0
- package/dist/engine/types.d.ts +4 -0
- package/dist/storage/adapter.d.ts +30 -1
- package/dist/storage/jsonl.d.ts +30 -1
- package/dist/storage/jsonl.js +196 -5
- package/dist/storage/memory.d.ts +15 -2
- package/dist/storage/memory.js +54 -1
- package/dist/storage/sqlite.d.ts +1 -0
- package/dist/storage/sqlite.js +26 -0
- package/dist/storage/vector-index.d.ts +8 -1
- package/dist/storage/vector-index.js +10 -1
- package/dist/tools/execute-skills.d.ts +6 -6
- package/dist/tools/learn.d.ts +2 -2
- package/dist/tools/learn.js +25 -8
- package/dist/tools/recall.js +67 -14
- package/dist/tools/research.js +23 -3
- package/dist/tools/think.d.ts +1 -1
- package/dist/vault/bridge.d.ts +5 -4
- package/dist/vault/bridge.js +7 -5
- package/package.json +1 -1
package/dist/tools/recall.js
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { NODE_TYPES, EDGE_TYPES } from '../engine/types.js';
|
|
3
|
+
import { classifyIntent } from '../engine/intent.js';
|
|
4
|
+
import { config } from '../config.js';
|
|
5
|
+
// Hybrid (semantic + lexical + recency) recall is on by default; set
|
|
6
|
+
// THINKING_GRAPH_HYBRID_RECALL=false to force the legacy substring path.
|
|
7
|
+
const HYBRID_RECALL_ENABLED = process.env.THINKING_GRAPH_HYBRID_RECALL !== 'false';
|
|
8
|
+
// Graph expansion (multi-hop recall) is on by default; set
|
|
9
|
+
// THINKING_GRAPH_GRAPH_EXPAND=false to disable the 1-hop typed-edge walk.
|
|
10
|
+
const GRAPH_EXPAND_ENABLED = process.env.THINKING_GRAPH_GRAPH_EXPAND !== 'false';
|
|
3
11
|
const coerceBool = z.preprocess((v) => (v === 'true' ? true : v === 'false' ? false : v), z.boolean());
|
|
4
12
|
export const recallSchema = z.object({
|
|
5
13
|
query: z.string().optional().describe('Full-text search'),
|
|
@@ -32,23 +40,63 @@ export async function recallHandler(graph, input, vault, projectSlug) {
|
|
|
32
40
|
nodes: enriched.filter(Boolean),
|
|
33
41
|
totalCount: nodes.length,
|
|
34
42
|
hasMore: nodes.length > (input.limit ?? 20),
|
|
43
|
+
intent: classifyIntent(input.query ?? ''),
|
|
35
44
|
}),
|
|
36
45
|
}],
|
|
37
46
|
};
|
|
38
47
|
}
|
|
39
|
-
// Standard query
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
// Standard query. Use ranked hybrid recall (semantic + lexical + recency)
|
|
49
|
+
// when there's a text query and no metadata filter (hybrid does not filter on
|
|
50
|
+
// metadata — fall back to the substring path to preserve that semantics).
|
|
51
|
+
const useHybrid = HYBRID_RECALL_ENABLED && !!input.query && !input.metadata;
|
|
52
|
+
const result = useHybrid
|
|
53
|
+
? await graph.recallHybrid({
|
|
54
|
+
query: input.query,
|
|
55
|
+
type: input.type,
|
|
56
|
+
sessionId: input.sessionId,
|
|
57
|
+
projectId: input.projectId,
|
|
58
|
+
crossProject: input.crossProject,
|
|
59
|
+
since: input.since,
|
|
60
|
+
limit: input.limit,
|
|
61
|
+
offset: input.offset,
|
|
62
|
+
})
|
|
63
|
+
: await graph.findNodes({
|
|
64
|
+
query: input.query,
|
|
65
|
+
type: input.type,
|
|
66
|
+
sessionId: input.sessionId,
|
|
67
|
+
projectId: input.projectId,
|
|
68
|
+
crossProject: input.crossProject,
|
|
69
|
+
since: input.since,
|
|
70
|
+
metadata: input.metadata,
|
|
71
|
+
limit: input.limit,
|
|
72
|
+
offset: input.offset,
|
|
73
|
+
});
|
|
74
|
+
// Enrich with edges; carry the fused score through when present (hybrid path).
|
|
75
|
+
const enriched = await Promise.all(result.items.map(async (n) => {
|
|
76
|
+
const withEdges = await graph.getNodeWithEdges(n.id);
|
|
77
|
+
if (!withEdges)
|
|
78
|
+
return null;
|
|
79
|
+
const score = n.score;
|
|
80
|
+
return score === undefined ? withEdges : { ...withEdges, score };
|
|
81
|
+
}));
|
|
82
|
+
const enrichedNodes = enriched.filter((n) => n !== null);
|
|
83
|
+
// Graph expansion (gated). For relationship/causation queries, pull in 1-hop
|
|
84
|
+
// typed-edge neighbours of the top content hits as structural context. Plain
|
|
85
|
+
// single-entity lookups skip this so they stay clean and cheap.
|
|
86
|
+
const intent = classifyIntent(input.query ?? '');
|
|
87
|
+
const related = [];
|
|
88
|
+
if (useHybrid && GRAPH_EXPAND_ENABLED && intent.multiHop && enrichedNodes.length > 0) {
|
|
89
|
+
const present = new Set(enrichedNodes.map(n => n.id));
|
|
90
|
+
const seedIds = enrichedNodes.slice(0, config.graphSeedCount).map(n => n.id);
|
|
91
|
+
const neighbors = await graph.expandNeighbors(seedIds, config.graphExpandLimit);
|
|
92
|
+
for (const nb of neighbors) {
|
|
93
|
+
if (present.has(nb.id))
|
|
94
|
+
continue;
|
|
95
|
+
const withEdges = await graph.getNodeWithEdges(nb.id);
|
|
96
|
+
if (withEdges)
|
|
97
|
+
related.push({ ...withEdges, graphExpanded: true });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
52
100
|
// Also search Obsidian vault if query text is provided
|
|
53
101
|
let vaultResults = [];
|
|
54
102
|
if (vault && projectSlug && input.query) {
|
|
@@ -64,9 +112,14 @@ export async function recallHandler(graph, input, vault, projectSlug) {
|
|
|
64
112
|
content: [{
|
|
65
113
|
type: 'text',
|
|
66
114
|
text: JSON.stringify({
|
|
67
|
-
nodes:
|
|
115
|
+
nodes: enrichedNodes,
|
|
68
116
|
totalCount: result.totalCount,
|
|
69
117
|
hasMore: result.hasMore,
|
|
118
|
+
intent,
|
|
119
|
+
...(related.length > 0 && {
|
|
120
|
+
related,
|
|
121
|
+
relatedCount: related.length,
|
|
122
|
+
}),
|
|
70
123
|
...(vaultResults.length > 0 && {
|
|
71
124
|
vault: vaultResults,
|
|
72
125
|
vaultCount: vaultResults.length,
|
package/dist/tools/research.js
CHANGED
|
@@ -65,6 +65,18 @@ function searchFlags(input, recencyDefault) {
|
|
|
65
65
|
}
|
|
66
66
|
return flags.length ? ` ${flags.join(' ')}` : '';
|
|
67
67
|
}
|
|
68
|
+
// `parallel-cli research run` has no recency/domain flags (unlike `search`), so
|
|
69
|
+
// fold those constraints into the prompt text to avoid silently dropping them.
|
|
70
|
+
function withResearchConstraints(query, input) {
|
|
71
|
+
const parts = [];
|
|
72
|
+
const afterDate = afterDateFor(input.recencyFilter);
|
|
73
|
+
if (afterDate)
|
|
74
|
+
parts.push(`Only use sources published on or after ${afterDate}.`);
|
|
75
|
+
if (input.domainFilter?.length) {
|
|
76
|
+
parts.push(`Restrict sources to these domains: ${input.domainFilter.join(', ')}.`);
|
|
77
|
+
}
|
|
78
|
+
return parts.length ? `${query} ${parts.join(' ')}` : query;
|
|
79
|
+
}
|
|
68
80
|
function buildGroundedSteps(input) {
|
|
69
81
|
const steps = [];
|
|
70
82
|
const query = input.query;
|
|
@@ -103,8 +115,16 @@ function buildActionPlan(input) {
|
|
|
103
115
|
if (shouldPrependGrounded) {
|
|
104
116
|
steps.push(...buildGroundedSteps(input));
|
|
105
117
|
}
|
|
106
|
-
// grounded_qa
|
|
118
|
+
// grounded_qa prefers NotebookLM, but must still work when NotebookLM isn't
|
|
119
|
+
// installed or has no matching notebook — append a web-grounded fallback.
|
|
107
120
|
if (input.intent === 'grounded_qa') {
|
|
121
|
+
steps.push({
|
|
122
|
+
tool: 'Bash',
|
|
123
|
+
description: 'Fallback (use only if NotebookLM is unavailable or returned no matching notebook): cited answer via parallel-cli research run',
|
|
124
|
+
args: {
|
|
125
|
+
command: `parallel-cli research run --text ${shq(withResearchConstraints(query, input))} --processor pro -o .premium/research/research-grounded`,
|
|
126
|
+
},
|
|
127
|
+
});
|
|
108
128
|
return steps;
|
|
109
129
|
}
|
|
110
130
|
// Choose the parallel-cli command based on intent.
|
|
@@ -124,7 +144,7 @@ function buildActionPlan(input) {
|
|
|
124
144
|
tool: 'Bash',
|
|
125
145
|
description: 'Step-by-step comparison with web grounding via parallel-cli research run',
|
|
126
146
|
args: {
|
|
127
|
-
command: `parallel-cli research run --text ${shq(query)} --text-description ${shq('step-by-step reasoning')} --processor pro -o .premium/research/research-compare`,
|
|
147
|
+
command: `parallel-cli research run --text ${shq(withResearchConstraints(query, input))} --text-description ${shq('step-by-step reasoning')} --processor pro -o .premium/research/research-compare`,
|
|
128
148
|
},
|
|
129
149
|
});
|
|
130
150
|
break;
|
|
@@ -133,7 +153,7 @@ function buildActionPlan(input) {
|
|
|
133
153
|
tool: 'Bash',
|
|
134
154
|
description: 'Deep multi-source research (30s+) via parallel-cli research run',
|
|
135
155
|
args: {
|
|
136
|
-
command: `parallel-cli research run --text ${shq(query)} --processor pro -o .premium/research/research-explore`,
|
|
156
|
+
command: `parallel-cli research run --text ${shq(withResearchConstraints(query, input))} --processor pro -o .premium/research/research-explore`,
|
|
137
157
|
},
|
|
138
158
|
});
|
|
139
159
|
break;
|
package/dist/tools/think.d.ts
CHANGED
|
@@ -28,8 +28,8 @@ export declare const thinkSchema: z.ZodObject<{
|
|
|
28
28
|
branchId: z.ZodOptional<z.ZodString>;
|
|
29
29
|
needsMoreThoughts: z.ZodOptional<z.ZodEffects<z.ZodBoolean, boolean, unknown>>;
|
|
30
30
|
}, "strip", z.ZodTypeAny, {
|
|
31
|
-
thought: string;
|
|
32
31
|
type: "thought" | "decision" | "insight" | "code_fact" | "assumption" | "detection" | "tech_debt" | "principle" | "pattern" | "skill_result" | "research";
|
|
32
|
+
thought: string;
|
|
33
33
|
thoughtNumber: number;
|
|
34
34
|
totalThoughts: number;
|
|
35
35
|
nextThoughtNeeded: boolean;
|
package/dist/vault/bridge.d.ts
CHANGED
|
@@ -46,10 +46,11 @@ export declare class VaultBridge {
|
|
|
46
46
|
*/
|
|
47
47
|
write(opts: VaultWriteOpts): string;
|
|
48
48
|
/**
|
|
49
|
-
* Drop a sentinel at the vault root so the knowledge-graph indexer
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
49
|
+
* Drop a sentinel at the vault root so the knowledge-graph indexer flushes
|
|
50
|
+
* these pending writes on the next `kg_search()` — it auto-indexes
|
|
51
|
+
* (incrementally) when the sentinel is present, so search is never stale.
|
|
52
|
+
* Best-effort: a failure here must not break the write path; it only delays
|
|
53
|
+
* freshness until a later write re-drops the sentinel or `kg_index()` runs.
|
|
53
54
|
*/
|
|
54
55
|
private markPendingIndex;
|
|
55
56
|
/** Read a single note by relative path. */
|
package/dist/vault/bridge.js
CHANGED
|
@@ -130,10 +130,11 @@ export class VaultBridge {
|
|
|
130
130
|
return relative(this.vaultRoot, absPath);
|
|
131
131
|
}
|
|
132
132
|
/**
|
|
133
|
-
* Drop a sentinel at the vault root so the knowledge-graph indexer
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
133
|
+
* Drop a sentinel at the vault root so the knowledge-graph indexer flushes
|
|
134
|
+
* these pending writes on the next `kg_search()` — it auto-indexes
|
|
135
|
+
* (incrementally) when the sentinel is present, so search is never stale.
|
|
136
|
+
* Best-effort: a failure here must not break the write path; it only delays
|
|
137
|
+
* freshness until a later write re-drops the sentinel or `kg_index()` runs.
|
|
137
138
|
*/
|
|
138
139
|
markPendingIndex() {
|
|
139
140
|
try {
|
|
@@ -141,7 +142,8 @@ export class VaultBridge {
|
|
|
141
142
|
writeFileSync(sentinel, new Date().toISOString(), 'utf-8');
|
|
142
143
|
}
|
|
143
144
|
catch {
|
|
144
|
-
// Non-fatal —
|
|
145
|
+
// Non-fatal — a later write re-drops the sentinel; `kg_index()` is a
|
|
146
|
+
// manual backstop.
|
|
145
147
|
}
|
|
146
148
|
}
|
|
147
149
|
// ─── Read ───────────────────────────────────────────
|
package/package.json
CHANGED